Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3d20fee
LibRed: continue the AutoNumber counter past the int32 wrap
ChrisJollyAU Aug 21, 2026
2a14814
Add JetParseTranslator for .NET Parse method support
ChrisJollyAU Aug 21, 2026
e3dacd8
LibRed: test-suite sweep, reader/writer page scope, and per-test cleanup
ChrisJollyAU Aug 22, 2026
50ee96d
LibRed: the General (v1) text collation, and the complex-column syste…
ChrisJollyAU Aug 23, 2026
8e133cd
LibRed: the collation field is a 32-bit LCID, and what the v0 letter …
ChrisJollyAU Aug 23, 2026
f7b9ab1
LibRed: implement the locale sort orders, including contraction
ChrisJollyAU Aug 23, 2026
1dbab5f
LibRed: extend General Legacy across the blocks, and identify its pro…
ChrisJollyAU Aug 23, 2026
ddb499f
LibRed: fix reading an index where many rows share a key
ChrisJollyAU Aug 23, 2026
8f27344
LibRed: General Legacy covers the whole Basic Multilingual Plane
ChrisJollyAU Aug 23, 2026
81fa756
LibRed: General covers the whole Basic Multilingual Plane
ChrisJollyAU Aug 23, 2026
30dd693
LibRed: the 510-byte index entry limit, and collation above the BMP
ChrisJollyAU Aug 23, 2026
067a50f
LibRed: the inline word-sort position is a 16-bit field, not a marker…
ChrisJollyAU Aug 23, 2026
988e62c
LibRed: reproduce ACE's truncation checksum instead of refusing long …
ChrisJollyAU Aug 23, 2026
4a3555a
LibRed: implement the French sort order - a reversed diacritic section
ChrisJollyAU Aug 23, 2026
da9ff13
LibRed: name the collation constants from [MS-UCODEREF]
ChrisJollyAU Aug 23, 2026
e14432e
LibRed: Bosnian, Croatian and Serbian - the first version-1 tailorings
ChrisJollyAU Aug 23, 2026
0e4130f
LibRed: a weight with no primary folds into the one before it (versio…
ChrisJollyAU Aug 23, 2026
21bb630
LibRed: implement Thai - a contraction, not a reordering
ChrisJollyAU Aug 23, 2026
87268c0
Split the ACE engine tests out of the cross-platform suite
ChrisJollyAU Aug 23, 2026
3dd2275
LibRed: prove creation honours every collation it claims to encode
ChrisJollyAU Aug 23, 2026
14ece02
LibRed: the multiple-record append query, INSERT INTO ... SELECT
ChrisJollyAU Aug 23, 2026
24c7566
LibRed: the make-table query, SELECT ... INTO
ChrisJollyAU Aug 23, 2026
d4745f1
Fix the migrations-lock hang in JetHistoryRepository
ChrisJollyAU Aug 23, 2026
dd1b6bb
Run statementless SQL as a no-op instead of a parse error
ChrisJollyAU Aug 23, 2026
0098809
Update tests
ChrisJollyAU Aug 23, 2026
62af6ae
Take the relocation pointer from the slot's first 4 bytes, not its width
ChrisJollyAU Aug 23, 2026
e82a21e
Use modified DateOnly to make sure that the year is over year 100
ChrisJollyAU Aug 23, 2026
d2264a0
Write an empty byte[] as '' - a bare 0x is T-SQL and ACE rejects it
ChrisJollyAU Aug 24, 2026
4b8f535
Read a binary column as UTF-16 text in the functions that see it as text
ChrisJollyAU Aug 24, 2026
37df3d8
Translate byte[].Any() as LENB(x) > 0
ChrisJollyAU Aug 24, 2026
210e776
Match SqlServerByteArrayMethodTranslator's shape
ChrisJollyAU Aug 24, 2026
84530ee
Assert the refusal for byte-array Length/Index/First, as GearsOfWar does
ChrisJollyAU Aug 24, 2026
a5aed06
LibRed: NULLIF, which ACE does not have
ChrisJollyAU Aug 24, 2026
0529e1c
Emit NULLIF as IIF, which is what Jet actually has
ChrisJollyAU Aug 24, 2026
30d3c84
Millisecond DATEPART/DATEDIFF, spelled "ms" like every other interval
ChrisJollyAU Aug 24, 2026
550f4f6
LibRed: carry milliseconds through the temporal path
ChrisJollyAU Aug 24, 2026
21cb6d7
LibRed: FULL [OUTER] JOIN, which ACE has no syntax for
ChrisJollyAU Aug 24, 2026
f37c354
Bring the ad-hoc query tests up to upstream's override coverage
ChrisJollyAU Aug 25, 2026
27fca49
Replace two stale base expectations with what upstream actually asserts
ChrisJollyAU Aug 25, 2026
48f7de1
LibRed: make a connection observe two ADO.NET contracts it was ignoring
ChrisJollyAU Aug 25, 2026
9b99441
Move the Ado temporal tests onto the millisecond boundary
ChrisJollyAU Aug 25, 2026
b918d1b
LibRed: DATETIME2 end to end - write, index, create, upgrade
ChrisJollyAU Aug 26, 2026
c423c16
LibRed: BIGINT - index keys, the variable-length fix, and the 0x05 up…
ChrisJollyAU Aug 26, 2026
cd9e340
LibRed: decode the page-0 user commit slots - they are a write counter
ChrisJollyAU Aug 26, 2026
3e3ade2
Update SQL tests for Jet date/time literal compliance
ChrisJollyAU Aug 26, 2026
c7bd9e8
Skip the new-format type tests on an ACE that predates the type
ChrisJollyAU Aug 26, 2026
834cd1c
Run LibRed.Engine.AccessTests on push, as it already does on pull_req…
ChrisJollyAU Aug 26, 2026
c717da6
Add settings.json to restrict shell command usage
ChrisJollyAU Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
42 changes: 42 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"permissions": {
"allow": [
"Bash(dotnet test *)",
"Bash(dotnet build *)",
"Bash(dotnet restore *)",
"Bash(dotnet nuget list source)",
"Bash(dotnet --info)",
"Bash(dotnet --version)",
"Bash(dotnet --list-sdks)",
"Bash(dotnet --list-runtimes)",
"PowerShell(dotnet test *)",
"PowerShell(dotnet build *)",
"PowerShell(dotnet restore *)",
"Bash(git status*)",
"Bash(git diff*)",
"Bash(git log*)",
"Bash(git show*)",
"Bash(git branch*)",
"Bash(git commit*)",
"Bash(git push*)",
"PowerShell"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell",
"hooks": [
{
"type": "command",
"command": "grep -qE '(^|[;&|(\")])[[:space:]]*(python[0-9.]*|pythonw|py)([[:space:]]|\")|sed[[:space:]]+-[a-zA-Z]*i|sed[[:space:]]+--in-place|>[[:space:]]*[^[:space:];&|]*\\.(cs|md|csproj|props|targets|sln|json|ps1|g4)|tee[[:space:]][^;&|]*\\.(cs|md|csproj|props|targets|sln|json|ps1|g4)' && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Shell edits to source files (and Python) are disallowed: they bypass file checkpointing, so /rewind cannot undo them. Use Edit or Write instead. Writing logs to the scratchpad is fine.\"}}' || true"
},
{
"type": "command",
"command": "in=$(cat); if printf '%s' \"$in\" | grep -qE '(^|[;&(\")]|\\\\n)[[:space:]]*(grep|egrep|fgrep|rg|cat|head|tail|ls|dir|find|fd|fdfind|Get-Content|gc|Get-ChildItem|gci|Select-String|sls)([[:space:]]|\")' && ! printf '%s' \"$in\" | grep -qE '(scratchpad|\\.log|\\$log|/tmp/)'; then echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Reading files through the shell costs a permission prompt and bypasses the dedicated tools. Use Grep for content, Glob for filenames, Read for file contents. Shell text tools stay allowed on scratchpad logs (paths containing scratchpad, .log or $log).\"}}'; fi; true"
}
]
}
]
}
}
7 changes: 7 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ jobs:
- 'test/LibRed.Core.Tests/**'
- 'test/LibRed.EFCore.Tests/**'
- 'test/LibRed.Engine.Tests/**'
- 'test/LibRed.Engine.AccessTests/**'
- 'test/EFCore.LibRed.FunctionalTests/**'
# LibRed.Ado references EFCore.Jet.Data and LibRed.EFCore references EFCore.Jet, so a
# change on the Jet side has to re-run LibRed. The reverse is not true: nothing under
Expand Down Expand Up @@ -678,6 +679,12 @@ jobs:
if: env.skipTests != 'true'
shell: pwsh
run: dotnet test .\test\LibRed.Core.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-hang-timeout 5m
# The engine tests that cross-check against ACE. They belong here rather than in the cross-platform
# LibRed job above, which runs on five platforms precisely to prove LibRed needs no ACE at all.
- name: 'Run Tests: LibRed.Engine.AccessTests'
if: always() && env.skipTests != 'true'
shell: pwsh
run: dotnet test .\test\LibRed.Engine.AccessTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-hang-timeout 5m
- name: 'Run Tests: LibRed.Ado.Tests'
if: always() && env.skipTests != 'true'
shell: pwsh
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,12 @@ jobs:
if: env.skipTests != 'true'
shell: pwsh
run: dotnet test .\test\LibRed.Core.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-hang-timeout 5m
# The engine tests that cross-check against ACE. They belong here rather than in the cross-platform
# LibRed job above, which runs on five platforms precisely to prove LibRed needs no ACE at all.
- name: 'Run Tests: LibRed.Engine.AccessTests'
if: always() && env.skipTests != 'true'
shell: pwsh
run: dotnet test .\test\LibRed.Engine.AccessTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-hang-timeout 5m
- name: 'Run Tests: LibRed.Ado.Tests'
if: always() && env.skipTests != 'true'
shell: pwsh
Expand Down
19 changes: 17 additions & 2 deletions EFCore.Jet.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.9.12009.208 insiders
VisualStudioVersion = 18.9.12009.208
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F68095EE-6CD1-43A2-B498-6CA72CE2A0CB}"
ProjectSection(SolutionItems) = preProject
Expand Down Expand Up @@ -82,6 +82,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.LibRed.FunctionalTes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibRed.Benchmarks", "test\LibRed.Benchmarks\LibRed.Benchmarks.csproj", "{6F69127C-9481-48B2-A974-F9A7F38BB9EC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibRed.Engine.AccessTests", "test\LibRed.Engine.AccessTests\LibRed.Engine.AccessTests.csproj", "{5287C755-947C-448E-BBE4-48D47051D35F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -332,6 +334,18 @@ Global
{6F69127C-9481-48B2-A974-F9A7F38BB9EC}.Release|x64.Build.0 = Release|Any CPU
{6F69127C-9481-48B2-A974-F9A7F38BB9EC}.Release|x86.ActiveCfg = Release|Any CPU
{6F69127C-9481-48B2-A974-F9A7F38BB9EC}.Release|x86.Build.0 = Release|Any CPU
{5287C755-947C-448E-BBE4-48D47051D35F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5287C755-947C-448E-BBE4-48D47051D35F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5287C755-947C-448E-BBE4-48D47051D35F}.Debug|x64.ActiveCfg = Debug|x64
{5287C755-947C-448E-BBE4-48D47051D35F}.Debug|x64.Build.0 = Debug|x64
{5287C755-947C-448E-BBE4-48D47051D35F}.Debug|x86.ActiveCfg = Debug|x86
{5287C755-947C-448E-BBE4-48D47051D35F}.Debug|x86.Build.0 = Debug|x86
{5287C755-947C-448E-BBE4-48D47051D35F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5287C755-947C-448E-BBE4-48D47051D35F}.Release|Any CPU.Build.0 = Release|Any CPU
{5287C755-947C-448E-BBE4-48D47051D35F}.Release|x64.ActiveCfg = Release|x64
{5287C755-947C-448E-BBE4-48D47051D35F}.Release|x64.Build.0 = Release|x64
{5287C755-947C-448E-BBE4-48D47051D35F}.Release|x86.ActiveCfg = Release|x86
{5287C755-947C-448E-BBE4-48D47051D35F}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -358,6 +372,7 @@ Global
{CADF8601-1CF1-421B-AACF-EE2C16AC610B} = {6A8DE399-1804-4113-A408-F23B7F5C9CAC}
{0F741500-BC61-44FA-9642-3B3326F7C90E} = {6A8DE399-1804-4113-A408-F23B7F5C9CAC}
{6F69127C-9481-48B2-A974-F9A7F38BB9EC} = {6A8DE399-1804-4113-A408-F23B7F5C9CAC}
{5287C755-947C-448E-BBE4-48D47051D35F} = {6A8DE399-1804-4113-A408-F23B7F5C9CAC}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9359773D-6399-447E-9814-6CB41C2FB664}
Expand Down
100 changes: 84 additions & 16 deletions src/EFCore.Jet/Migrations/Internal/JetHistoryRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,29 @@ namespace EntityFrameworkCore.Jet.Migrations.Internal
/// </remarks>
public class JetHistoryRepository(HistoryRepositoryDependencies dependencies) : HistoryRepository(dependencies)
{
private static readonly TimeSpan _retryDelay = TimeSpan.FromSeconds(1);
// Migration-lock retry policy. The lock guards a migration and is released explicitly the moment that
// migration finishes, so contention resolves in milliseconds — these delays are sized for a local file,
// not for a round trip to a remote server.
//
// The previous policy started at one second and doubled while under a minute, which had three separate
// faults that compounded: the delay was never reset, so a thread that lost a single race retried only
// once every 64 seconds for the rest of the run; there was no jitter, so every contender woke in the
// same millisecond, collided, and all but one backed off together; and the cap was 60 seconds, four
// orders of magnitude above the hold time. With N contenders in lockstep exactly one wins per round,
// making a run take N x 64s — about sixteen minutes for fifteen threads, which is indistinguishable
// from a deadlock and is why the parallel migration tests read as "hung" rather than "slow".
private static readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(50);
private static readonly TimeSpan _maxRetryDelay = TimeSpan.FromSeconds(1);

/// <summary>How long to keep trying for the migration lock before giving up.</summary>
/// <remarks>
/// Without a deadline the retry loop cannot fail, only wait, so a lock that is never released blocks
/// the caller forever with nothing logged — the failure mode is a silent hang rather than an error
/// anyone can act on. Giving up turns it into a reportable fault, as SQL Server's
/// <c>sp_getapplock</c> timeout does.
/// </remarks>
private static readonly TimeSpan _lockTimeout = TimeSpan.FromMinutes(1);

public override LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior.Explicit;

/// <summary>
Expand Down Expand Up @@ -146,9 +168,9 @@ public override IMigrationsDatabaseLock AcquireDatabaseLock()
}

var retryDelay = _retryDelay;
var deadline = DateTime.UtcNow + _lockTimeout;
while (true)
{
var dbLock = CreateMigrationDatabaseLock();
int? insertCount = 0;
//No CREATE TABLE IF EXISTS in Jet. We try a normal CREATE TABLE and catch the exception if it already exists
try
Expand All @@ -162,14 +184,18 @@ public override IMigrationsDatabaseLock AcquireDatabaseLock()
}
if ((int)insertCount! == 1)
{
return dbLock;
// Built only once the lock is actually ours; the old loop constructed one per attempt and
// dropped it on every miss.
return CreateMigrationDatabaseLock();
}

Thread.Sleep(retryDelay);
if (retryDelay < TimeSpan.FromMinutes(1))
if (DateTime.UtcNow >= deadline)
{
retryDelay = retryDelay.Add(retryDelay);
throw new TimeoutException(LockTimeoutMessage());
}

Thread.Sleep(JitteredDelay(retryDelay));
retryDelay = EscalateDelay(retryDelay);
}
}

Expand All @@ -182,30 +208,72 @@ public override async Task<IMigrationsDatabaseLock> AcquireDatabaseLockAsync(
await Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName))
.ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken).ConfigureAwait(false)))
{
await CreateLockTableCommand().ExecuteNonQueryAsync(CreateRelationalCommandParameters(), cancellationToken)
.ConfigureAwait(false);
// Same guard as the synchronous overload, which this had been missing: the exists check above
// is not atomic, so concurrent migrators can all decide the table is absent and all issue the
// CREATE. Losing that race is the normal path, not a failure.
try
{
await CreateLockTableCommand()
.ExecuteNonQueryAsync(CreateRelationalCommandParameters(), cancellationToken)
.ConfigureAwait(false);
}
catch (DbException e)
{
if (!e.Message.Contains("already exists")) throw;
}
}

var retryDelay = _retryDelay;
var deadline = DateTime.UtcNow + _lockTimeout;
while (true)
{
var dbLock = CreateMigrationDatabaseLock();
var insertCount = await CreateInsertLockCommand(DateTimeOffset.UtcNow)
.ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken)
.ConfigureAwait(false);
int? insertCount = 0;
try
{
insertCount = (int?)await CreateInsertLockCommand(DateTimeOffset.UtcNow)
.ExecuteScalarAsync(CreateRelationalCommandParameters(), cancellationToken)
.ConfigureAwait(false);
}
catch (DbException e)
{
// Likewise mirrored from the synchronous overload: the WHERE NOT EXISTS guard on the
// insert is not atomic either, so a duplicate key here means someone else took the lock.
if (!e.Message.Contains("duplicate")) throw;
}
if ((int)insertCount! == 1)
{
return dbLock;
return CreateMigrationDatabaseLock();
}

await Task.Delay(retryDelay, cancellationToken).ConfigureAwait(false);
if (retryDelay < TimeSpan.FromMinutes(1))
if (DateTime.UtcNow >= deadline)
{
retryDelay = retryDelay.Add(retryDelay);
throw new TimeoutException(LockTimeoutMessage());
}

await Task.Delay(JitteredDelay(retryDelay), cancellationToken).ConfigureAwait(false);
retryDelay = EscalateDelay(retryDelay);
}
}

/// <summary>
/// Spreads the wait by +/-25% so contenders stop waking together. This matters as much as the cap:
/// with a fixed delay every loser retries in the same millisecond as every other loser, so the herd
/// stays synchronised and each round yields exactly one winner no matter how short the delay is.
/// </summary>
private static TimeSpan JitteredDelay(TimeSpan delay)
=> TimeSpan.FromTicks((long)(delay.Ticks * (0.75 + (Random.Shared.NextDouble() / 2.0))));

/// <summary>Doubles the backoff up to <see cref="_maxRetryDelay" /> and holds there.</summary>
private static TimeSpan EscalateDelay(TimeSpan delay)
=> delay >= _maxRetryDelay
? _maxRetryDelay
: TimeSpan.FromTicks(Math.Min(delay.Ticks * 2, _maxRetryDelay.Ticks));

private string LockTimeoutMessage()
=> $"Timed out after {_lockTimeout.TotalSeconds:N0}s waiting for the migrations lock. Another "
+ $"migration may still be running, or a previous one may have left a row in "
+ $"'{LockTableName}' without releasing it; delete that row to clear the lock.";

private IRelationalCommand CreateLockTableCommand()
=> Dependencies.RawSqlCommandBuilder.Build($"""
CREATE TABLE `{LockTableName}` (
Expand Down
Loading
Loading