Skip to content

Upgrade to .NET 11 and EF Core 11 (preview 7), and xunit v3 - #294

Open
ChrisJollyAU wants to merge 48 commits into
CirrusRedOrg:masterfrom
ChrisJollyAU:ef11
Open

Upgrade to .NET 11 and EF Core 11 (preview 7), and xunit v3#294
ChrisJollyAU wants to merge 48 commits into
CirrusRedOrg:masterfrom
ChrisJollyAU:ef11

Conversation

@ChrisJollyAU

Copy link
Copy Markdown
Member

Moves src to net11.0 and EF Core 11 preview 7, the test projects to xunit v3, and works through the resulting behaviour changes in both providers.

Provider changes

Decimal conversion. dotnet/runtime#130566 replaced Convert.ToDecimal(double)'s 15-significant-digit rounding — inherited from OLE Automation's VarDecFromR8 — with a correctly-rounded full-precision conversion. Jet's ROUND is the VBA function, so it widens Currency to Double and SUM(ROUND(UnitPrice, 2)) came back as 58.600000000000001421085471520. JetDecimalConverter restores the old algorithm; verified against the .NET 10 runtime over 3.5 million values.

VARIANT_BOOL conversions. bool maps to smallint, so EF now elides Convert.ToInt16(bool) as a no-op, taking with it the * -1 flip that turns Jet's -1 into .NET's 1 — WHERE Bool = 1 matched nothing. The flip moved to JetSqlExpressionFactory.Convert, the single funnel for both Convert.ToXxx and plain casts.

NULL arguments to VBA functions. MID raises "Invalid use of Null" on a NULL length rather than propagating, and the conversion that used to guard it is likewise gone. Guarded at SQL generation, because dotnet/efcore#34127 removes such a check from the query tree — correctly, for any dialect where these functions do propagate.

LEFT JOIN predicates. dotnet/efcore#38449 keeps outer-key null checks on compound join predicates; Jet's ON accepts only comparisons between the joined tables, so the redundant ones are dropped at generation.

LibRed matches ACE's VBA conversion semantics (CStr at 15/7 significant digits, VARIANT_BOOL for CInt/CLng/CDbl, CBool on numeric strings) and compares dates by OLE Automation serial, which keeps its evaluator and index ordering consistent.

Infrastructure

ARM64 CI legs for the LibRed jobs; large-address-aware x86 test hosts; shard rebalancing and crash retry; encoding normalised to UTF-8 without BOM with charset now stated in .editorconfig.

Notes

ACE behaviour is recorded as executable probes under LibRed.Core.Tests rather than prose — VBA conversions, pre-epoch dates, join null guards, and which function arguments reject NULL. Green-list entries were removed for tests that are discovered but never execute (dotnet/efcore#38766, fixed for rc1).

ChrisJollyAU and others added 30 commits July 28, 2026 22:00
Three references carried their own version and so bypassed Dependencies.targets:
LibRed.EFCore pinned Microsoft.EntityFrameworkCore.Relational at 10.0.0-rc.2.*, and EFCore.Jet.Tests
pinned Design, Relational and Relational.Specification.Tests at 10.0.*. A central version bump would
have silently left all three behind on the old major.

Dropping the Version attribute lets the `Update=` items in Dependencies.targets supply it, which is how
every other reference in the repo already works. No functional change — verified building against EF 10
before the bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SDK 11.0.100-preview.6.26359.118, TFMs net10.0 -> net11.0, and the three central version properties
pinned exactly at 11.0.0-preview.6.26359.118. Pinned rather than a floating range as elsewhere: while
migrating, a silent float to preview 7 would move results underneath the pre-upgrade baseline and make
failure attribution meaningless.

The move is not optional in parts. Microsoft.EntityFrameworkCore.Relational.Specification.Tests 11
ships net11.0 ONLY and depends on Microsoft.DotNet.XUnitV3Extensions, so the framework bump, the EF
bump and the xunit v2->v3 migration are one atomic change; none can land alone.

Four EF 11 API breaks in EFCore.Jet, 18 errors:

- JsonTypeMapping -> StructuralJsonTypeMapping. Worth eight of the errors on its own: EF 11 reduced
  JsonTypeMapping to an obsolete stub that no longer derives from RelationalTypeMapping, which took
  GetDataReaderMethod, CustomizeDataReaderExpression, GenerateNonNullSqlLiteral and the nested
  RelationalTypeMappingParameters out of scope together. The members themselves are unchanged.
- ValidateValueGeneration moved from ModelValidator to RelationalModelValidator and dropped its
  IEntityType parameter, which the key already carries.
- SqlExpressionVisitor was removed outright, with no replacement base. Both visitors now derive from
  ExpressionVisitor with a VisitExtension that dispatches to the same 36 per-node methods, kept verbatim
  so no query-translation behaviour moves. Unrecognised nodes fall through to the base, which visits
  children — the default the removed class applied.
- IReadOnlyIndex.Properties widened to IReadOnlyPropertyBase. JetIndexConvention now gives up on the
  index filter for a non-property member, which is how it already treats an unmapped column.

All five LibRed projects — Core, Sql, Engine, Ado, EFCore — build against .NET 11 and EF 11 with no
changes at all. The engine is EF-version-agnostic as intended; only the Jet provider it borrows
plumbing from needed work.

The test projects do NOT build yet: ~5,700 errors dominated by 2,068 CS0433 "type exists in both" and
492 CS0104 ambiguous references, i.e. xunit v2 and v3 both in the graph. That is the next commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Forced rather than chosen: EF 11's Relational.Specification.Tests depends on
Microsoft.DotNet.XUnitV3Extensions, so inheriting from its test bases requires v3. It was already in the
graph transitively alongside the explicit v2 references, which is what produced 2,068 CS0433
"type exists in both" and 492 CS0104 ambiguity errors. Swapping the packages removes the collision.

xunit.v3.core / .assert / .extensibility.core pinned at 4.0.0-pre.108 — the same build EF resolves, so
the two cannot disagree about which xunit is present. Test projects become OutputType=Exe, as v3 requires;
xunit.runner.console (v2-only) is dropped; xunit.runner.visualstudio stays and already supports v3.

ConditionalFact/ConditionalTheory become plain Fact/Theory — 1,378 attributes across 277 files. The
obsolescence message suggests adding a typeof() argument, but that would be wrong here: NOT ONE call site
in the repo passes a condition member name. Every one is either bare or `Skip = "reason"`, both of which
route through the same obsolete params-string constructor. A conditional fact with no condition is a fact,
so the attribute was buying nothing. That took CS0618 from 2,610 to zero.

Three spellings had to be covered, found only by re-measuring after each pass: `[ConditionalFact]` and
`[ConditionalFact(Skip = ...)]`, the namespace-qualified `[Xunit.ConditionalFact]`, and combined lists
like `[ConditionalTheory, MemberData(...)]` where a comma follows the name.

Xunit.Abstractions is deleted in v3, so its using is removed from 243 files and `using Xunit;` added to the
26 that had been relying on it to reach ITestOutputHelper, which now lives in Xunit itself.

Errors across the solution: ~5,700 -> ~740. What remains is two unrelated jobs, neither of them package
work: the 12 shared framework files under test/Shared/TestUtilities/Xunit still target v2's deleted
reflection model (IMessageBus, ExceptionAggregator, RunSummary, IXunitTestCase, ITypeInfo), and separately
EF 11 changed its test bases (TestStoreFactory no longer overridable, AssertSql and TestSqlLoggerFactory
moved onto the base, IsAsyncData gone). The tree does not build yet.

Note for the framework rewrite: the 4 JetConditional* files exist to discover ConditionalFact/Theory and
now have nothing to discover, but the 20 JetCondition/LibRedCondition usages they evaluate still need a
home — v3's native SkipUnless/SkipWhen/SkipType is the likely replacement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two of the three jobs in test/Shared/TestUtilities/Xunit. The crash-detection runner chain is untouched
and still the last thing standing between the tree and a build.

Orderers. v3 takes and returns IReadOnlyCollection rather than IEnumerable, and the Xunit.Abstractions
reflection wrappers it used to navigate — TestMethod.TestClass.Class.Name, TestMethod.Method.Name,
DisplayName — are gone in favour of flat metadata on the test case itself: TestClassName, TestMethodName,
TestCaseDisplayName, and TestCollectionDisplayName for collections. Behaviour is unchanged; only the
route to the sort keys is.

Conditions. Every one of the 20 usages is class-level and every one is the same condition, IsNotCI, so
they move to the .NET team's ConditionalClass, which is v3-native and already in the graph through EF's
spec-tests package. It runs a class when its named member is TRUE, hence the new TestEnvironment.IsNotCI
beside the existing IsCI rather than inverting at each call site.

That leaves the four JetConditional* discoverers and test cases with nothing to discover — the previous
commit turned every [ConditionalFact]/[ConditionalTheory] into a plain [Fact]/[Theory] — and the four
JetCondition/LibRedCondition attributes and enums with no one to evaluate them. Eight files deleted, 275
lines, none of it replaced by anything I had to write.

One case has no v3 equivalent and is deliberately kept rather than quietly dropped: each provider applies
an assembly-level condition declaring that nothing can run without a configured test database. v3 covers
per-method (SkipWhen/SkipUnless) and per-class (ConditionalClass) but not per-assembly, and EF 11 deleted
the ITestCondition those attributes implement, so a local one now lives in test/Shared. It is currently
inert — the rewritten framework is where it gets evaluated.

Framework files still failing: 140 errors across JetXunitTestCaseRunner, JetXunitTheoryTestCaseRunner,
JetXunitTestFrameworkDiscoverer, JetXunitTestRunner, JetXunitTestFramework and TestRunnerCrashAttribute.
Both features they carry now have a v3 landing point: Assert.Skip(reason) is the dynamic-skip primitive,
and TestRunner<,> still exposes InvokeTest/PreInvoke/PostInvoke to trigger it from.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last of test/Shared/TestUtilities/Xunit. Five v2 classes replaced by two, 473 lines deleted against
207 added, because most of what was there existed only to reach a point either side of the test body —
and v3 offers that directly.

The v2 arrangement needed a custom test framework, a framework discoverer, a test-case runner and a
theory-case runner purely to interpose on each test. All four are gone: BeforeAfterTestAttribute's
Before/After, applied once at assembly level, is the same interception point. No custom test framework
remains, so the [assembly: TestFramework(...)] declarations go too.

The crash protocol itself is unchanged, because it has to be: it survives a process that never gets to
run any more of its own code. A marker naming the in-flight test is written before the test and deleted
after; a marker still present at startup can only mean the runner died mid-test, so it is promoted to
TestsKnownToCrashTestRunner.txt and skipped from then on — now via Assert.Skip, which is v3's dynamic-skip
primitive. Markers are keyed per test rather than by "most recent", since tests run concurrently and each
must delete its own.

UnsupportedExpressionSkipPolicy lifts out the other half — the message matching that reports a test as
skipped when it failed only because the provider has no SQL for APPLY, row skipping or sequences, rather
than as a failure. That logic is provider knowledge, not xunit plumbing, so it survives verbatim.

NOT YET WIRED, and it will show up in the first baseline diff: that policy has nowhere to hang. In v2 it
sat on the custom test runner; v3 exposes only Run on XunitTestCaseRunner, so reaching it means either
reimplementing the runner chain or intercepting at the message sink via XunitTestFramework.CreateExecutor.
Until then those tests report as failures, not skips, and the count will read high against the 223-name
pre-upgrade baseline. The assembly-level ITestCondition ("no test database configured") is inert for the
same reason.

Also: v3 made the collection orderer generic and constrains it to ITestCollectionMetadata rather than
ITestCollection — the case and collection orderers differ there — and its orderer attributes take a Type
instead of a type-name/assembly-name pair.

Remaining errors are EF 11 test-base churn, not xunit: TestStoreFactory no longer overridable, AssertSql
and TestSqlLoggerFactory moved onto the base, IsAsyncData gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EF 11 renamed NonSharedModelTestBase's hook from TestStoreFactory to NonSharedTestStoreFactory. Both
still exist — the shared-store bases keep TestStoreFactory — so this is a rename on one family, not a
general one, which is why the 44 sites were patched by compiler-reported file and line rather than by
matching the text everywhere it appears.

Every affected class is a NonSharedModelTestBase derivative: the AdHoc* query suites plus CompiledModel
and EntitySplitting on both providers.

Clears 88 CS0115 and, with them, 88 of the 92 CS0534 "does not implement abstract member" errors, which
were cascades from the same cause. Solution errors: ~460 -> ~332.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EF 11 moved its inheritance test bases out of the flat namespaces into feature sub-namespaces —
Microsoft.EntityFrameworkCore.BulkUpdates.Inheritance and Microsoft.EntityFrameworkCore.Query.Inheritance.
The types were never removed, which is why the errors were "could not be found" rather than anything
about changed members.

95 files: 55 needed the BulkUpdates namespace, 40 the Query one. Clears all 208 CS0246 and the CS0103 for
IsAsyncData, which came from the same reorganisation.

Worth recording how this was found, because it was not by me: the XML docs shipped with these packages
document almost nothing — not these types, not TestStoreFactory — so I had been reading type names out of
assembly strings, which gives names but not namespaces. The user pointed VS at one file and took its
"add missing using" suggestion. The IDE resolves against the real compilation, which is exactly the
lookup being reconstructed by hand. Worth reaching for first next time an EF major moves things.

Solution errors: ~5,700 at the start of the upgrade -> 184.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The errors that needed no judgement, 184 -> 128.

IAsyncLifetime and IAsyncDisposable return ValueTask in xunit v3, not Task — 14 methods across 8 files.

The `new` keyword had gone wrong in both directions, because EF 11 moved members onto some bases and off
others: 8 removed where nothing is hidden any more, 4 added where the base now declares what we redeclare.

ExecuteSqlInterpolated is obsolete in favour of ExecuteSql — 4 call sites.

AscendingTestCollectionOrderer becomes an explicit interface implementation. Restating a generic
constraint requires it to match the interface's exactly, and the interface's constraint is not visible in
the shipped XML docs, so ITestCollection and ITestCollectionMetadata were both guesses and both wrong.
Explicit implementation inherits the constraint instead of restating it, which is what CS0425 suggests.

Also correcting an earlier claim: this file was reported as compiling when the framework rewrite landed.
It was not. That check filtered the build output with a pattern that did not match the path format, and an
empty result was read as success rather than as a grep that matched nothing.

Remaining 128 all need judgement: 124 CS0115 across roughly 30 base methods that EF 11 renamed or removed
— each one a choice between fixing a signature and dropping an override, where dropping loses a
provider-specific assertion — plus 4 CS0534 for SetParameterizedCollectionMode, a genuinely NEW abstract
member on PrimitiveCollectionsQueryRelationalTestBase that needs a real decision about which parameter
translation modes Jet and LibRed support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EF 11 deleted a swathe of BuiltInDataTypesTestBase tests and changed several test-infrastructure
signatures, leaving both provider suites unbuildable. A solution build stops at the first failing
project and a wave of CS0115 masks everything behind it, so this was swept per project and
rebuilt until the list stopped changing rather than driven off one build's output.

Remove the overrides EF no longer declares, including Columns_have_expected_data_types, whose
entity types (NullableBackedDataTypes, BuiltInNullableDataTypes and friends) no longer exist.
Where a member changed shape rather than going away, follow it instead of deleting: CleanAsync
now takes createTables, PrimitiveCollectionsQuery gains SetParameterizedCollectionMode, and
ExecuteSqlInterpolated* is now ExecuteSql*. AddOptions genuinely was removed, so that one goes.

The Jet suite is a near-mirror of LibRed, so its half was derived from the LibRed fixes and then
compared member by member for parity. That comparison is what caught QueryBuiltInDataTypesTest,
which survives a clean build because an uncalled private method is not an error.

Test projects drop Microsoft.NET.Test.Sdk and MSTest.TestFramework. xunit v3 is self-executing
under Microsoft.Testing.Platform, and the .NET 10+ SDK refuses the VSTest target those brought
in, which is why no tests were discoverable at all.

Finally, the two mechanisms the xunit v2 removal left inert are wired up again.
UnsupportedExpressionSkipPolicy gains a string-based core, since a failure is intercepted as a
reported message carrying exception type names rather than live exceptions; that also drops its
OleDb and Odbc references, which had no business in a folder shared with the cross-platform
LibRed tests. UnsupportedExpressionTestFramework wraps the executor's message sink so a query the
provider has declared it cannot translate is reported skipped rather than failed - a
BeforeAfterTestAttribute cannot do this, as it never learns why a test failed. TestConditions
evaluates the assembly-level ITestCondition, which v3 has no native equivalent for.

Both compile and a Query run shows the expected skip count, but the failure-to-skip rewrite has
not yet been exercised against a real unsupported expression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two independent breaks, both fallout from the xunit v3 / EF 11 move.

The visible one: every suite now fails before a test runs with "Testing with VSTest target is no
longer supported by Microsoft.Testing.Platform". xunit v3 runs on Microsoft.Testing.Platform, and
from the .NET 10 SDK on, routing an MTP project through dotnet test's old VSTest path is a hard
error rather than a fallback. The opt-in goes in global.json ("test": {"runner": ...}); this SDK
build does not read the dotnet.config form, and says so in its own help text. That message names
".NET 10 SDK and later" as a fixed string in the targets file - it is not reporting the SDK in use,
and the same error reproduces on the 11.0.100-preview.6 the workflows install.

That leaves the VSTest-only flags with nowhere to go. --logger trx becomes --report-xunit-trx, same
filename convention and same TRX schema, so the green-tests XPath still parses it unchanged. The two
non-shard suites gain an explicit --results-directory: VSTest defaulted the trx to the project's
TestResults, MTP defaults it to the build output's, which would have quietly emptied the upload globs
and the comparison that reads them. --filter, --results-directory and -p:FixedTestOrder pass through
untouched - xunit v3 accepts the compound VSTest filter syntax the shards rely on.

--blame-hang-timeout is dropped rather than translated. It is a per-test budget; MTP's built-in
--timeout is a per-run one, so carrying the number across would kill a shard three minutes in with
everything passing. Honest per-test behaviour needs the HangDump extension package, which is a bigger
change than this one. A hung test is now bounded only by the job timeout; a crashed one is still
caught by the TestRunnerCrashDetection markers.

Crash detection in the shard retry loops keyed off Sequence_* blame files, which are a VSTest
artifact and will never appear again - the loop would have silently stopped retrying. The trx is
written in-process at the end of a run, so its absence is the crash signal, and that now retries
instead of exiting 3. Ordinary failures still write a trx and fall through to the comparison.

The second break is restore, and it only hit the three suites that inherit EF's test bases:
Microsoft.DotNet.XUnitV3Extensions comes in transitively with Relational.Specification.Tests and is
published to dotnet-eng alone, never to nuget.org, so NU1101. Added that feed.

While in there: dotnet9 carried the right package ids but only 9.x of them, nothing this repo pins
since the EF 11 move, so dotnet11 takes its place. The two myget.org feeds are publish targets - the
NuGet job pushes to them by URL, not by source key - and nothing restores from them, but they were
still probed on every restore and were the source of the connection resets in the log. Five sources
down to three, each with a reason to be there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fallout from the runner switch, found by the first red run rather than by reading.

EFCore.Jet.Data.Tests is the repo's only MSTest project, and global.json's opt-in is all-or-nothing:
"All projects must use that test runner". MSTest reaches MTP through its own runner rather than the
xunit one, which needs EnableMSTestRunner and an executable entry point.

It had a second break behind that one. --report-xunit-trx is an xunit extension option, and MSTest
under MTP ships no trx reporter at all, so even once the project was on the right runner the step
would have died on an unknown option. Microsoft.Testing.Extensions.TrxReport supplies it, pinned to
the 2.2.3 the project already resolves for the platform itself; the flag there is --report-trx.
Verified locally: 97 tests, trx written to the expected directory.

EFCore.Jet.Tests is dropped from the run rather than fixed. The assembly contains no tests - its only
two test files have been <Compile Remove>d since 45bae42, the initial EF 9 update in July 2024 - so
the step had spent two years reporting green over an empty run. VSTest tolerated that; MTP exits 8.
The step and its now-dead upload glob go, with a comment recording why and what would bring it back.
The path filter in Changes stays, since rebuilding on a change there is still right. Much of what is
in those two files appears to be covered by EFCore.Jet.FunctionalTests now, so whether the project
earns its place at all is worth a look before restoring it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SGN sat with the double-precision VBA math group and went through UnaryDouble, whose Func<double,
double> widened Math.Sign's int straight back to a double. VBA's Sgn returns Variant/Integer and
.NET's Math.Sign returns int, so the boxed type was wrong the whole time - it just never mattered
while the value was only ever compared. EF 11 rephrases the Math translation tests to project the
scalar instead of filtering on it, so it now reaches GetInt32, which throws on a boxed Double.

Moved to Convert1, which boxes the int. Null propagation is unchanged - both helpers return null for
a null argument. Covers the whole family: Sign, Sign_float, Sign_decimal and Sign_int all execute,
the latter two passing outright since they carry no baseline. The other functions in that group
(SIN/COS/TAN/ATN/EXP/LOG/SQR) genuinely return doubles and stay where they are.

Sign and Sign_float still fail on AssertSql: their baselines are the EF 10 filter shape and need the
rewrite pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Access has two concatenation operators and neither is what EF assumes. '&' always concatenates but
coerces a NULL operand to a zero-length string; '+' propagates NULL but re-dispatches on operand
TYPE. Verified against ACE (IntA = 0, StringA = 'Foo'):

    NULL & 'x'      -> 'x'        NULL + 'x'      -> NULL
    IntA & '5'      -> '05'       IntA + '5'      -> '5'      numeric ADDITION
    IntA & 'x'      -> '0x'       IntA + 'x'      -> ''       empty, silently, no error
    IntA & StringA  -> '0Foo'     IntA + StringA  -> ''

So '+' is not the fix, despite its NULL behaviour being the one EF wants - that propagation is a side
effect of VBA arithmetic propagating Null through Variants, and the same arithmetic nature is what
makes it choose between two different operations based on operand type. Worse here than in general:
the Convert visitor emits no CAST, so a Convert(number -> string) arrives as a bare numeric and '+'
would return a wrong value rather than an error. That shortcut is safe only because '&' is
type-insensitive - a coupling between the two methods that was nowhere written down, and now is.

Keep '&' and emulate the propagation around it, the same way SKIP/OFFSET, @@rowcount and
parameterised TOP are emulated: IIF(<operand> IS NOT NULL [AND ...], a & b, NULL). MayBeNull skips
the guard for non-null constants and non-nullable columns, so a concat that cannot produce NULL
generates byte-identical SQL to before, and identical operands (x + x) are checked once rather than
twice. Guard shape follows EF's own - IS NOT NULL checks ANDed, value in the THEN.

Verified on LibRed and on real ACE. Across NullSemantics, StringTranslations and NorthwindFunctions:
438 tests, 15 failures, every one of them AssertBaseline and not one AssertResults. The six
Is_not_null_optimizes_binary_op_with_{partial,mixed,nested}_checks tests are the ones this fixes;
the nine Concat_*/Join_non_aggregate baselines are churn awaiting the rewrite pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mechanical follow-on from the EF 11 move, across the Jet and LibRed functional suites:

- [Theory] -> [Fact] where the base method lost its async parameter, so the skip attributes match the
  shape of what they are skipping again.
- Overrides deleted where the inherited baseline now matches what the provider generates, leaving
  nothing for the override to say.
- AssertSql text updated where the EF 11 pipeline emits a different but equivalent shape.

Authored outside this session; committed unrun, so the suites are the check rather than review. Does
NOT include the rewrite pass for the SGN and string-concat fixes in cb0a497 and ab0566c - the 15
known AssertSql failures there are still outstanding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dropping --blame-hang-timeout in eea4702 was the cheap option and it cost six hours of runner time
on the first hang: two BuildAndTest shards sat wedged past three hours with nothing to stop them
short of GitHub's 360-minute default. The crash-retry loop cannot help there either - it keys off a
missing trx after the process exits, and a process that never exits never reaches the check. Hangs
were always the blame flag's job.

Microsoft.Testing.Extensions.HangDump supplies the MTP equivalent. The open question at the time was
whether its timer is per-test like VSTest's blame timeout or per-run like MTP's own global timeout,
which would have made the old values wrong by orders of magnitude. Measured rather than assumed: the
Engine suite runs 895 tests in ~72s and passes clean under --hangdump-timeout 30s, so the timer is
per-test and the original numbers carry over unchanged - 3m for the Jet suites and Engine, 5m for the
LibRed ACE cross-checks, 10m for the LibRed functional suite.

Unlike blame, the option only exists where the package is referenced, so all six suites CI runs get
it. It composes with MSTest as well as xunit v3.

timeout-minutes on every job is the backstop under that, sized well above observed runtimes so only a
hang trips it: 120m for BuildAndTest (three sharded ACE suites, each retryable three times), 90m for
LibRedFunctional, 60m for LibRedAccess, 30m for LibRed and NuGet, 10-20m for the rest. A hang should
now be caught per-test, named, and dumped long before any of these fire - they exist so that a hang
the dump extension somehow misses costs minutes rather than a working day.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The switch to Microsoft.Testing.Platform quietly reversed a default. xunit v3's --show-stdout and
--show-stderr both default to 'All', so captured ITestOutputHelper output is printed for every test
that runs, not just failures - VSTest only surfaced it on failure. EF logs the compiled shaper
expression tree per query under the Query category, so a functional run now emits tens of thousands
of lines of lifted expression trees and GitHub truncates the step.

This is the same cost the comment above the Data.Tests step already describes for --verbosity
detailed, arriving by a different route: nothing consumes that output, the green-tests extraction
reads the trx, and crash detection keys off a missing trx.

Set both to 'Failed' on the xunit suites, which keeps the diagnostics where they are actually read.
Not applied to EFCore.Jet.Data.Tests - these are xunit extension options and the MSTest suite has no
equivalent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AceRenameTests.Access_reads_a_libred_renamed_table_and_column_and_still_applies_the_default kills
LibRed.Core.Tests on the runner with 0xC0000005 inside System.Data.OleDb's ICommandText.Execute. Two
runs stopped at exactly +329 in 45.7s and 45.4s, so it is deterministic there, not a race. It passes
locally - alone and across the full 471 - under the same MTP runner, with the same ACE 16 provider
preference and collection parallelism already disabled, so the difference is the environment rather
than the runner.

Worth being precise about what changed, because three things landed together and the suite could not
run at all in between: the test was added 2026-07-23 and was green on .NET 10 + xunit v2 + VSTest;
2026-07-28 brought .NET 11, EF 11 and xunit v3; the VSTest path then hard-errored until the MTP opt-in
in eea4702. So today is the first execution since, and MTP, xunit v3 and the .NET 11 runtime are all
untested variables. EF 11 is not among them - LibRed.Core.Tests has no EF dependency. A native AV in
OleDb's COM marshalling is at least as plausible from the runtime as from the test host.

Not guessing further without evidence. Microsoft.Testing.Extensions.CrashDump turns the AV into a dump
naming the faulting frame, on every ACE-touching suite, and the dumps upload as artifacts - the Jet
job folds them into its existing test-results artifact, LibRedAccess gets its own step.

The three LibRedAccess steps also gain the retry the Jet shards have always had. Crash is distinguished
from failure by exit code: MTP returns 0 for success, 2 for failed tests and 8 for no tests ran, so
anything else means the host died rather than reported, and only that is retried. A genuine test
failure still fails the step on the first attempt.

Worth noting where it dies. The three assertions before it pass - ACE resolves both renamed names and
honours the DEFAULT - and the crash is on the stale column name, which should raise a clean
OleDbException. An AV there means ACE followed something it should not have, which is either its own
fragile error path or a dangling reference left by the rename. The dump should say which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EF 11 renamed a family of spec tests, and the green lists still name the old methods. The check
compares names that passed, so an entry naming a method that no longer exists can never match and
reads as a regression forever - and it fails the step before the list can ever be updated, so the
staleness is self-perpetuating.

73 entries in each list, the same 73 in both, which is what a removed method should look like:
ConvertToProviderTypes 12, EverythingIsBytes 12, EverythingIsStrings 12, CustomConverters 10,
BuiltInDataTypes 8, JsonTypes 7, ComplexNavigations / TPC / TPH / TPT / Transaction 2 each,
AdHocComplexTypeQuery and PrimitiveCollections 1 each.

Determined by matching every entry against the 38,336 names the assembly currently discovers, not by
running anything - an entry was dropped only when nothing with that exact name exists, so a test that
merely fails is untouched. The renames are real: Can_insert_and_read_back_all_non_nullable_data_types
is gone and the class now has Can_insert_and_read_back_all_mapped_data_types and its variants.

8 of the 73 are not removals but display-name changes - the JsonTypes emoji escaping (\ud83d\udc4d
where the assembly now renders the character), the TimeOnly/TimeSpan collection theory data, and
Column_collection_of_strings_contains_null becoming Contains_null. The old strings can never match
either, so they go too; the auto-update will re-add them under their new names once they pass.

Discovery was taken from an x64 build while the lists are x86. Nothing in the removed set looks
architecture-conditional, but that was not verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repo has to match EF's own SQL Server functional tests, and those run on VSTest. MTP was never a
deliberate choice here - xunit v3 pulls in xunit.v3.core.mtp-v2, whose props default
IsTestingPlatformApplication to true, and the .NET 10+ SDK then hard-errors on the VSTest target. The
opt-in in eea4702 was the path of least resistance out of that error, not a decision.

xunit v3 itself stays: EF 11's Relational.Specification.Tests depends on Microsoft.DotNet.XUnitV3Extensions,
so inheriting its test bases requires v3. Only the runner changes; xunit.runner.visualstudio is the
VSTest adapter for v3 and was already referenced everywhere.

Two properties in test/Directory.Build.props do it, and both are needed. IsTestingPlatformApplication
stops the SDK erroring. TestingPlatformDotnetTestSupport stops MTP importing its VSTest.targets, which
REPLACES the VSTest target with one that calls InvokeTestingPlatform only when the first property is
true - so setting only the first leaves a target that builds and exits, reporting success having run
nothing. That silent no-op cost an hour to spot and is worth knowing about.

EFCore.Jet.FunctionalTests and EFCore.LibRed.FunctionalTests also had to gain Microsoft.NET.Test.Sdk.
Neither referenced it: under MTP they were self-executing test applications and did not need it, so
under VSTest there was nothing to run them. Same silent-success symptom, different cause.

Everything MTP-specific goes with it: the global.json runner opt-in, the TrxReport / HangDump /
CrashDump extension packages and their pins, EnableMSTestRunner and OutputType=Exe on the MSTest suite,
and the workflow flags. The workflows return to --logger trx and --blame-hang-timeout, the crash
detection returns to looking for VSTest blame Sequence_* files, and the job caps and dump-upload steps
that only existed to cover MTP gaps go away.

Kept from the MTP detour, because they were never about the runner: the dotnet-eng NuGet feed that
fixes the XUnitV3Extensions restore, and dropping the EFCore.Jet.Tests step whose project contains no
tests at all.

Verified per suite type: LibRed.Engine 895 passed, EFCore.Jet.FunctionalTests 237 run (173/20/44,
matching what MTP reported for the same filter), EFCore.LibRed.FunctionalTests 69 passed, and the
MSTest suite 97 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The NULL guard added in ab0566c wrapped far more than it needed to. MayBeNull treated anything that
was not a bare column or constant as nullable, so an operand EF had already made non-null got wrapped
anyway. In the precompiled-query baselines that produced

    SET `b`.`Name` = IIF(IIF(`b`.`Name` IS NULL, '', `b`.`Name`) IS NOT NULL AND @suffix IS NOT NULL,
                         IIF(`b`.`Name` IS NULL, '', `b`.`Name`) & @suffix, NULL)

- a NULL check on a COALESCE, which cannot be NULL, with the operand written out three times.

Only a bare nullable ColumnExpression needs the guard now. Anything composed - a COALESCE, a CASE, a
function, a parameter - is left alone, because EF has already expressed whatever null handling it
wants there, and where it has not, the provider has no better information than EF does. The example
above goes back to exactly the SQL it generated before the guard existed.

The tests the guard exists for are unaffected, because the operand that has to propagate in them is a
bare column:

    SELECT IIF(`e`.`BoolA`,
               IIF(`e`.`NullableStringA` IS NOT NULL,
                   `e`.`NullableStringA` & IIF(`e`.`NullableStringB` IS NULL, '', `e`.`NullableStringB`),
                   NULL),
               NULL)

which mirrors SQL Server's [NullableStringA] + ISNULL([NullableStringB], N'') - only A propagates.
NullSemantics still runs 332 with the same 6 baseline-only failures it had under the broad guard, so
the narrowing cost no coverage.

Baselines swept for both providers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EF 11 adds Group37310, whose FK to GroupMember37310 is the optional-composite shape the fixture
already handles four times over: Id is required, GroupOwnerId is not, so a row can carry a partially
null FK. SQL Server's MATCH SIMPLE treats any null column as satisfying the constraint; ACE and LibRed
both apply MATCH FULL and reject it - verified today on real ACE ("a related record is required in
table 'GroupMember37310'") and on LibRed ("no matching row in 'GroupMember37310'"), same constraint,
same statement.

So it joins the four already dropped in SeedAsync. The engine semantics are untouched: MATCH FULL was
established deliberately in 4c31ac0 against ACE ground truth, and the tests that actually target it -
CompositeFkMatchFullTests, CompositeFkNullAccessTests - still enforce it. The constraint is only
scenery for these tests, which are about updating a many-to-many alongside a reference with a
composite key, not about how the FK behaves; leaving it in place blocked the write before the test
could reach what it exists to check.

One difference from the other four: Group37310 is absent from the owned model, where EF's own SQL
Server test stubs this scenario out with "No owned types". An unconditional drop therefore threw in
SeedAsync and took the entire owned fixture down through InitializeAsync - every test in the class,
not just this one - so this drop is guarded on the table being present in the model.

Can_update_many_to_many_and_reference_with_composite_key now passes 10/10 on both providers, up from
8 failures each (Identity, TptIdentity, ClientCascade, ClientNoAction, both async modes).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The x86 legs truncate where x64 does not. On ACE 2010 the same driver, same code and same tests get
through all 23,509 of shard 3 on x64 and die at 3,828 on x86 - which is the shape of a 2GB user address
space rather than a driver fault, ACE being the one native component in that process.

Confirmed there is headroom to gain rather than assuming it: the x86 testhost the test platform ships
(microsoft.testplatform.testhost, build/net8.0/x86/testhost.x86.exe) reports machine 0x014C with the
LARGE_ADDRESS_AWARE bit CLEAR, so it is capped at 2GB today. Setting it lifts the ceiling to 4GB.

Marks both dotnet.exe and the apphosts under test/**/bin, because which process ends up holding ACE
depends on whether VSTest launches the output-directory apphost or runs 'dotnet exec testhost.dll',
and the whole toolchain on these legs is the per-architecture SDK installed into .dotnet_x86. Cheaper
to mark both than to depend on that detail.

The flag is one COFF Characteristics bit (0x0020), patched directly instead of via editbin so the step
needs no vcvars environment; non-x86 images are skipped by machine type. It invalidates Authenticode
signatures, which does not matter on an ephemeral runner. The step reports patched/already/skipped
counts, so if a future SDK ships these already marked the log says so instead of leaving us to assume
the change did something.

Expected to help 2010 x86 only. ACE 2016 dies at 1,025-1,427 of 23,509 on BOTH architectures, so its
failure is the driver rather than address space, and this cannot fix it - that redistributable
(16.0.5044.1000) is also out of support since 2025-10-14 and will not be fixed upstream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6cd0dde marked the x86 hosts large-address-aware in a single step after Build Solution, and it failed:

  Exception calling "WriteAllBytes" ... The process cannot access the file
  '...\.dotnet_x86\dotnet.exe' because it is being used by another process.

MSBuild keeps worker nodes and the VB/C# compiler server alive after a build for reuse, and those hold
dotnet.exe open, so nothing can rewrite it afterwards.

Split in two along what each target needs. dotnet.exe is patched immediately after the SDK install,
before anything has run it. The apphosts under test/**/bin are patched after the build, since they do
not exist until then, preceded by 'dotnet build-server shutdown' so the same node reuse cannot hold
those open either.

No change to what gets marked or why: both remain candidates because which process ends up holding ACE
depends on whether VSTest launches the output-directory apphost or 'dotnet exec testhost.dll'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Added DOTNET_CI env var to CI workflows. Marked flaky test classes with [SkipOnCI] to skip on CI. Updated expected SQL in many-to-many load tests to match actual queries. Combined [ConditionalClass] and [SkipOnCI] for migration tests. Fixed attribute typo in MigrationsInfrastructureLibRedTest.cs.
Shard 2 was doing almost nothing. Measured on the 2010 x86 ODBC leg:

  shard 1  Query minus Associations/Translations   14,029 tests   12m13s
  shard 2  Associations + Translations                773 tests      36s
  shard 3  everything non-Query                    23,509 tests   ~15m

Shard 2 held 2% of the suite and finished in half a minute while shard 1 ran for twelve. Note the axis
is wall clock, not test count: shard 3 carries the most tests by far and is both the fastest per test
(~38ms vs ~52ms) and the more stable of the two big shards, so it is left exactly as it was.

The Query work now splits down a meaningful line rather than an arbitrary one - Northwind and
GearsOfWar together, everything else in Query on the other side:

  shard 1  Query, excluding Northwind and GearsOfWar   6,381
  shard 2  Query Northwind* + GearsOfWar*             8,422
  shard 3  non-Query, unchanged                      23,533

Verified against the assembly's 38,336 discovered tests that the three filters are a partition: the
counts sum exactly, no test matches two shards, and none is left uncovered. The Query/non-Query
separation is preserved - shard 3 remains purely non-Query rather than a mix.

Projected at the measured per-test rates this puts shards 1 and 2 at roughly 5.5 and 7.3 minutes,
both comfortably inside shard 3's 15, so shard 3 stays the critical path and nothing gets slower.
Per-test cost is assumed constant when tests move between shards, which will not hold exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
577acc8 left an empty slot in the attribute list:

  [ConditionalClass(typeof(TestEnvironment), nameof(TestEnvironment.IsNotCI)), , SkipOnCI("Flaky on CI")]

which is a compile error, so the whole EFCore.LibRed.FunctionalTests assembly failed to build rather
than any single test failing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Test assertions in ManyToManyLoadJetTest and ManyToManyLoadLibRedTest now conditionally check the expected SQL based on the EntityState. If the state is Detached, one SQL variant is asserted; otherwise, a different SQL shape is used. The expected SQL strings are formatted with indentation and line breaks for clarity, and the selected columns in the joined subqueries differ between the two cases.
Dependencies
- Bump DotNetVersion/EFCoreVersion/MSLibVersion to 11.0.0-preview.7.26381.103.
- Drop references that are redundant or fiction: Microsoft.Extensions.Caching.Memory
  (supplied by EF Core, pinned 8.0.0 but always resolved 11.x), Newtonsoft.Json (no
  source uses it; still present transitively), and System.Diagnostics.DiagnosticSource,
  System.Collections.Immutable and System.ComponentModel.TypeConverter, which are in-box
  on net10/net11 and were already being pruned before restore.
- NetTopologySuite declared 2.6.0, which is what Relational.Specification.Tests requires
  and what always resolved. Microsoft.Build.Tasks.Core centralised at 18.7.1; the three
  test projects carried inline 18.7.1/18.7.1/18.4.0.
- EFCore.Jet.Odbc no longer pins System.Data.Odbc to 4.5.2 inline, so it follows
  MSLibVersion like every other project. This changes the shipped package's declared
  dependency floor.
- Add LibRed.Benchmarks to the solution; being outside it, nothing restored or built it,
  and it had drifted a month behind.

Guard row-independent projections across a LEFT JOIN (Jet/ACE)
Jet/ACE evaluates a derived table's projected expression after the join, against the
all-NULL row, rather than treating the absent row as NULL wholesale. A bare literal has
no NULL input, so it survives the join and comes back non-NULL for a row that matched
nothing. EF 11 (dotnet/efcore#30915, PR #38479) fixes whole-object materialization off
the nullable side of a group join by injecting "1 AS marker" and gating on it, so on ACE
the gate concludes "matched" and the shaper throws "Nullable object must have a value".

JetOuterJoinProjectionGuardExpressionVisitor rewrites the marker to
CASE WHEN anchor IS NULL THEN NULL ELSE 1 END. It runs in JetParameterBasedSqlProcessor
after base.Process: the anchor is non-nullable within the subquery, so applied earlier
SqlNullabilityProcessor proves the test false and folds the CASE back to its ELSE branch.
It matches only EF's injected fragment, not user constants, which are never read for
null-ness. AdHocMiscellaneousQuery goes from 43 to 15 failures; NorthwindJoinQuery is
unchanged either way.

Migration lock
Take the lock with a conditional INSERT ... SELECT ... WHERE NOT EXISTS reporting
@@rowcount, the Jet analogue of SQLite's INSERT OR IGNORE + SELECT changes(), so losing
the race is an ordinary result rather than an exception recognised by message text. Also
fix the async retry sleeping on the constant instead of the backing-off local, switch its
ConfigureAwait to false, and add DefaultLockTableName for parity with SQLite.

LibRed duplicate keys
RowInserter throws ConstraintViolationException, which LibRedCommand translates at the
ADO boundary into LibRedException with number 2627, so callers can recognise a duplicate
key without matching on message text.

Tests
Port ~140 missing overrides per provider from upstream SQL Server with their baselines,
and order every member of the 24 affected classes to match upstream. SQL Server version
guards are dropped and IsJsonTypeSupported branches collapsed, as neither applies here.
Baselines are still SQL Server dialect and will be swept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ChrisJollyAU and others added 16 commits August 13, 2026 22:38
…tion

global.json pinned the preview 6 SDK while the packages moved to preview 7, which broke
every job in CI. setup-dotnet installs the exact version from global.json and does not
apply rollForward when choosing what to download, so CI built and ran on preview 6.
Microsoft.Extensions.Primitives is in-box, so package pruning drops the package reference
and the app binds to the shared framework's copy - preview 6's, which lacks the
ChangeToken.OnChange overload that preview 7's Configuration calls. Every fixture failed
in its static constructor with MissingMethodException. Locally rollForward: latestFeature
picked up an rc.1 SDK, so none of this showed. The SDK build number matches the package
build exactly; they ship together.

CI: run the two ACE-free LibRed jobs on ubuntu-24.04-arm and windows-11-arm as well.
LibRed is fully managed with no Access dependency, so ARM is the leg that actually
exercises the cross-platform claim off x64. LibRedAccess is unchanged - it cross-checks
against the real engine over OLE DB and needs Windows x64 with ACE.

ConstraintViolationException was only thrown on the insert path; the update path threw a
plain InvalidOperationException for the same violation, so an update that hit a duplicate
key reached ADO callers untranslated while an insert became a DbException. Both paths now
throw it and LibRedCommand translates both.

Deriving from InvalidOperationException keeps catch blocks working but does not keep
Assert.Throws<InvalidOperationException> passing - xUnit matches the type exactly, and
ThrowsAny is the one that accepts derived types. The four affected assertions in
UniqueIndexEnforcementTests now name the type directly, and the doc comment says so
rather than claiming otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jet's ROUND is the VBA function, so it widens a Currency column to a Double:
SUM(ROUND(UnitPrice, 2)) comes back from ACE as a Double even though EF asked
for a decimal. Up to .NET 10, Convert.ToDecimal rounded that Double at 15
significant digits -- the runtime's VarDecFromR8 did so deliberately, to keep
garbage digits out of the decimal it was making. dotnet/runtime#130566 (.NET 11
preview 7) replaced it with a correctly-rounded full-precision conversion, so
58.6 became 58.600000000000001421085471520 and
Sum_over_round_works_correctly_in_projection started failing. Nothing in EF, the
SQL, the model or the provider had changed.

That 15-digit rounding came from OLE Automation, which is also where Jet's
Currency type and its VBA ROUND come from, so the old behaviour was tuned for
exactly this case and we were relying on it without knowing.

Port the pre-change algorithm into JetDecimalConverter (15 significant digits for
double, 7 for float) and call it from JetDataReader.GetDecimal ahead of the
existing Convert.ToDecimal fallback, leaving genuine decimal/Currency values
untouched. A port rather than a G15 round-trip: it reproduces the old overflow
and flush-to-zero boundaries exactly and allocates nothing on a hot read path,
and G15/G7 formatting is what we already had to abandon for the double and float
literal mappings over rounding and out-of-range values.

Also document the OLE Automation heritage in CLAUDE.md, including the rule this
cost us: when a long-stable conversion misbehaves with no code change on our
side, suspect the runtime's OA-era compatibility behaviour before the provider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correct the range error to name the bound it actually tests. It reported
JetConfiguration.TimeSpanOffset (1899-12-30) while the check is against
0100-01-01, so anyone hitting it was told the floor was 1899 -- particularly
confusing given the OLE epoch's part in date handling elsewhere.
JetDateOnlyTypeMapping already reported 0100-01-01; this was the outlier.

Drop the unreachable branch in the same method. Both callers substitute the OLE
epoch for a Ticks-0 value before calling it, so the value can never be default by
then; the guard and the commented-out assignment beside it are leftovers from
before JetDateTimeRangeConverter took over correcting default for ordering.

Remove the unused MaxDateTimeDoublePrecision constant, and the now-unneeded
EntityFrameworkCore.Jet.Data using with it.

Document why ProcessStoreType passes storeTypeNameBase in place of storeType:
Jet/ACE has no scaled datetime, so "datetime(3)" must collapse to the bare
"datetime". It reads like a slip and is not one.

No behaviour change. BuiltInDataTypesJetTest 55/55.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Probing ACE (AceVbaConversionProbeTest, which pins each result) found three
places where .NET's Convert.* stood in for OLE Automation's and had drifted:

CStr formats a Double at 15 significant digits and a Single at 7 -- the OA/VB
convention. ACE returns "0.3" for 0.1+0.2 and "0.333333333333333" for 1/3, while
Convert.ToString gives .NET Core 3.0+'s shortest round-trippable form,
"0.30000000000000004". The same 15-digit convention behind the decimal
regression, surfacing in string form.

Booleans convert as VARIANT_BOOL: CInt/CLng/CDbl/CSng/CCur(True) are all -1 in
ACE, where Convert.ToInt16(true) is 1, and CByte(True) overflows because a byte
cannot hold -1. The evaluator already had a Numeric() helper applying Jet's
-1/0 convention, and the arithmetic path used it -- the C-functions simply
bypassed it and called Convert.To* directly. Route them through it.

CBool accepts numeric strings ("-1") and non-integral numbers; Convert.ToBoolean
rejects the former outright, so LibRed refused input ACE accepts.

Two things the probe corrected rather than confirmed. CDec does not exist in the
Jet Expression Service at all -- ACE rejects it for a column or a literal alike
-- so LibRed's CDec has no parity contract to honour, and CCur is ACE's route to
a decimal. And CStr(True) is "-1", not "True" as the VBA runtime proper renders
it: the expression service is not VBA, so its behaviour is a question to probe
rather than one to reason out from VBA documentation. An existing test asserted
"True" on that basis and was wrong.

LibRed.Engine.Tests 895/895.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An OA date is a double: integer part days from 1899-12-30, fraction the time of
day. Below the epoch the day count goes negative while the time fraction stays
positive, so 1899-12-29 06:00 is -1.25 and 18:00 is -1.75 -- later in the day is
the smaller serial. Microsoft documents the consequence: midnight 1899-12-30 "is
not the minimum value, it is the middle when negative values are considered".

ACE compares and orders on that raw serial, so it places later pre-epoch times
first: `06:00 < 18:00` is False and ORDER BY returns 1,3,2,4,5,6 over rows
spanning the epoch (AcePreEpochDateProbeTest pins both). Its date functions are
unaffected -- DateAdd/DateDiff work in date space -- which is why the existing
DateAdd/DateDiff tests never surfaced any of this.

The evaluator compared CLR DateTime values, i.e. chronologically, while
IndexKeyEncoder writes that same raw serial as the index key. So the two paths
disagreed: a pre-epoch range predicate returned rows 2,3,4 on a scan but only
row 4 through an index seek, the rows at -1.25 and -1.75 falling below a -1.0
lower bound in serial space despite being chronologically later. Which rows a
query returned depended on the planner's choice of access path.

The key encoding cannot change -- ACE writes those keys too -- so matching ACE
in the evaluator is the only way both paths agree. Compare via ToOADate in
Compare, which CompareForSort delegates to, so ordering and comparison move
together. From the epoch onward the two orders are identical; only pre-1899
dates are affected.

Not hypothetical: the GearsOfWar model stores DateTimes in the year 102 (raised
from the original year 2, which is unusable as years below 100 do not
round-trip). GearsOfWar 3300 passed / 0 failed after the change, and
LibRed.Engine.Tests 898/898.

PreEpochDateOrderingTests guards index-vs-scan agreement for ORDER BY and for a
straddling range, comparison consistent with ordering, and pins the sequence to
1,3,2,4,5,6 -- the same sequence ACE produced for the same rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Revised SQL assertions in test files to use Jet/ACE-compliant syntax: replaced T-SQL constructs (e.g., ISNULL, COALESCE, CASE, CHARINDEX, VALUES, OPENJSON, NVARCHAR, N'', NOCOUNT) with Jet/ACE equivalents (IIF, INSTR, CLNG, CVar, TRUE/FALSE), switched identifier quoting from [] to backticks, adjusted join/subquery syntax, and updated parameter/literal formatting. Ensured null handling and conditional logic use IIF. Removed unsupported features and updated test expectations to match Jet/ACE SQL output per project guidelines.
The converter restores the pre-.NET-11 double->decimal behaviour that
dotnet/runtime#130566 replaced, and anyone reading doubles out of ACE has the
same problem, so it is API rather than an internal detail. That also lets the
test reference it directly.

EF Core hit this in the Cosmos provider (JSON numbers are doubles) and fixed it
by round-tripping through a "G15" string. The two are NOT equivalent:
Convert.ToDecimal(-0.9892735183189034) returned -0.989273518318904 on .NET 10,
which this port reproduces, while the round-trip yields ...903 — the correctly
rounded 15-digit answer, but not the one that shipped. The old algorithm scaled
by a power of ten in double arithmetic before rounding, and that error is part
of the behaviour being restored.

The port was checked against the real .NET 10 runtime over 3.5 million values,
including the overflow and flush-to-zero boundaries, with no disagreement.
JetDecimalConverterTest keeps the regression values, the float 7-digit path, the
boundaries, and the pinned divergence from the G15 approach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Working through the tests that passed under EF 10 and no longer do. The green
list is an EF 10 artefact — no EF 11 run has been clean enough to update it — so
these are the EF 10 -> EF 11 delta rather than recent regressions.

Simple_decimal_literals_are_parsed_for_HasDefaultValue built its expectation
with (decimal)-1.1111, a runtime double->decimal cast, so preview 7 moved the
EXPECTATION to -1.1110999999999999765520897199 while scaffolding kept parsing
the literal exactly. Now a decimal literal. Upstream's SQL Server test has the
same latent bug and will hit it when they take the preview 7 SDK; they are
pinned to preview 6.

JsonTypes collection tests passed SQL Server's store types (nchar(32),
varchar(max)); Jet text is always Unicode and 255 is the varchar ceiling, so the
element facets asserted a type the mapping had already resolved. LibRed already
used the Jet values.

CompiledModel needed ManyTypes.Decimal's DecimalTypeDefaultWarning suppressed,
exactly as the SQL Server test does — same validator, same warning.

Select_DTO_constructor_distinct_..._after_client_eval no longer throws on Jet, so
the Assert.ThrowsAsync<TrueException> wrapper (which upstream keeps, because it
still fails on SQL Server) had to go, and its ORDER BY gained the collection key.
ToQueryString_for_include_reference_and_collection lost a now-redundant ordering
term.

Column_collection_of_nullable_strings_contains_null was overwritten during the
preview 7 sync with SQL Server's OPENJSON body. Jet has never been able to query
into a JSON collection: the EF 10 override asserted the translation failure, and
that is restored — calling its own base rather than the non-nullable one, which
the EF 10 version got wrong.

Also removed StringTranslationsJetTest.Equals and six ModelBuilding entries from
the green lists. Those tests are discovered but never execute — name collisions
with object.Equals and with protected generic helpers — so they can never pass
again and would gate the list forever. Fixed upstream by dotnet/efcore#38766 for
rc1, at which point the ModelBuilding ones return under the same names while
Equals becomes Instance_Equals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The generator now emits a ProviderName on DbContextModelAttribute and uses
file-scoped namespaces, so every baseline file differs. Codegen formatting only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
723 files carried a BOM and 1,170 did not, because .editorconfig set
end_of_line = crlf but said nothing about charset — endings were governed and
held, encoding was not and drifted. Visual Studio writes new C# files without
a BOM, so that is the convention: strip the 723 and add charset = utf-8
alongside the existing end_of_line, in both sections that set it.

Encoding only. Verified no file differs by anything but its BOM, the solution
builds with no warnings, and LibRed.Engine.Tests is 898/898.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Redundant null checks in LEFT JOIN conditions are removed by overriding VisitLeftJoin in JetQuerySqlGenerator. Join predicates now use simple equality (ON a = b) instead of complex null checks for composite keys and nullable columns. Updated test baselines reflect the simplified SQL output, ensuring more idiomatic and Jet/Access-compliant queries.
Both write outside a transaction and reseed as their last statement, so a failed
assertion skips the cleanup and leaves its rows behind.
Deadlock_on_deletes_with_dependents_is_handled_correctly has never passed on
ACE, so it left two owners every run, and Inserts_when_database_type_is_different
- which runs after it and counts owners - then saw 4 instead of 2. That is the
intermittent failure: deterministic contagion, appearing only when the two land
in the same shard.

Reseeding in a finally makes a failure stop at the test that caused it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MID raises "Invalid use of Null" when its start or length is NULL rather than
propagating it, so
Null_semantics_is_correctly_applied_for_function_comparisons_that_take_arguments_from_optional_navigation_complex
failed once EF 11 stopped emitting the conversion whose null check happened to
protect it. The split is by argument position, not by function: a NULL in a
string or value position propagates, a NULL in a numeric one - length, start,
count, code - raises, because VBA cannot coerce Null into a numeric parameter.
Verified in LibRed.Core.Tests.AceNullArgumentProbeTest, which also records that
LEFT, RIGHT, INSTR, SPACE, STRING, CHR and DATEADD behave the same; only MID has
been hit so far, because numeric arguments are almost always literals.

Emitted at generation and not as a CASE in the query tree, because
dotnet/efcore#34127 removes a CASE that merely replicates SQL's native null
propagation - which is exactly what this looks like to a dialect where these
functions do propagate. Same reason the outer-join projection guard runs late.
IIF short-circuits, so no inner placeholder is needed.

Nullability originating in the value argument is not guarded: ACE returns NULL
for the whole call when the value is NULL, before coercing the numerics, so
MID(note, 1, LEN(note)) needs nothing and only MID(note, 1, LEN(other.Name))
does. Without that the guard also fired on every Substring over a nullable
column, changing SQL that was already correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the boundary behind the MID guard: string and value positions propagate
NULL, numeric positions - length, start, count, code, date increment - raise.
Also pins that IIF short-circuits, so a guard needs no inner placeholder, and
that a NULL value argument short-circuits the call before its numeric arguments
are coerced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Converting a bool to a number has to yield .NET's 1/0, but the column holds
VARIANT_BOOL's -1/0. The generator did that flip when it saw a Convert node,
which stopped working once EF elided the conversion: bool maps to smallint, so
Convert.ToInt16(bool) looked like a no-op and reached SQL as a bare column,
making `WHERE Bool = 1` match nothing. Convert.ToInt32(bool) was unaffected only
because int and smallint differ.

JetSqlExpressionFactory.Convert is the single funnel - Convert.ToXxx through
JetConvertTranslator, and casts built by EF - so one flip covers both, and a
plain cast whose conversion is also elided, which neither of the other places
could reach. What it emits is a multiplication rather than a cast, so nothing
downstream classifies it as redundant.

Only numeric targets are flipped. Excluding bool and string was not enough: a
bool concatenated into a string arrives with some other target type and came out
as `HasSoulPatch * -1 & ''`.

That makes the generator's bool branch dead, and with the flip gone both of its
arms were identical, so the special case goes entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ChrisJollyAU ChrisJollyAU self-assigned this Aug 17, 2026
@ChrisJollyAU
ChrisJollyAU requested a review from a team as a code owner August 17, 2026 13:09
ChrisJollyAU and others added 2 commits August 19, 2026 00:23
First clean run since the upgrade began, so the lists could finally be
published: OLE DB 34,912 -> 35,581 and ODBC 31,245 -> 32,055, with nothing
removed from either. Committed by hand because the AutoCommit workflow runs the
copy of its file on the default branch, so its fix cannot take effect for this
PR.

The lists had been frozen far longer than the EF 11 delta: 135 MigrationsJetTest
entries appear in both providers, along with the complex-type model builder
tests, all of which have been passing without being recorded. Until now the gate
was only protecting what it had managed to capture before the freeze.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
actions/checkout refuses to check out the head repository from a workflow_run
job, which holds the base repo's token and secrets - the "pwn request" shape.
The green-test commit step never ran because of it.

Check out the base repository at the PR's branch instead of naming the head
repo, which is the same code for a same-repository branch and needs no opt-in,
and skip the job entirely for forks: GITHUB_TOKEN could not push to a fork
anyway, so there was nothing to gain and a fork's code would have been placed in
a trusted context. test_results.yml already dropped its checkout for the same
reason.

Takes effect once on the default branch; workflow_run always runs the file from
there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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