diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..278e92c2 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,6 @@ +# Copilot Instructions + +## Project Guidelines +- This project (EntityFrameworkCore.Jet) targets the Microsoft Access Jet/ACE database engine, not SQL Server. Generated SQL, type mappings, and literals must be Jet/ACE-compliant (e.g. decimal/currency instead of bigint, #...# date literals, TIMEVALUE()). Note: Access 2016+ (ACE) does have a native BIGINT (Large Number) type, which is a SIGNED 64-bit integer and cannot hold unsigned values like ulong.MaxValue; unsigned ulong/uint should map to decimal(20,0) to avoid overflow. Provider distinctions are OLE DB / ODBC via the ACE/Jet driver rather than SqlClient. Do not assume SQL Server/T-SQL semantics. +- Jet/Access SQL dialect used in this project does not support COALESCE or NZ. When rewriting queries for Jet, prefer using IIF( IS NULL, , ) or 'CASE WHEN IS NULL THEN ELSE END' instead. +- Deferred enhancement for EntityFrameworkCore.Jet: add ACE engine-version detection so that on Access 2016+ the provider can map to native BIGINT (signed long; unsigned ulong/uint still go to decimal(20,0)) and native DATETIME2, while falling back to decimal(20,0)/legacy datetime on older ACE versions. Not a priority right now. \ No newline at end of file diff --git a/.github/workflows/auto_commit.yml b/.github/workflows/auto_commit.yml index a54bada3..cfaf66c3 100644 --- a/.github/workflows/auto_commit.yml +++ b/.github/workflows/auto_commit.yml @@ -11,7 +11,7 @@ jobs: if: github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.head_commit.author.email != 'github-actions@github.com' || github.event.workflow_run.head_commit.message != '[GitHub Actions] Update green tests.') steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: ${{ github.event.workflow_run.head_repository.full_name }} ref: ${{ github.event.workflow_run.head_branch }} @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - name: 'Download Green Tests' id: DownloadGreenTests - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | var allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index b86962ee..4894c9aa 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -28,7 +28,7 @@ jobs: run: | echo 'EventName: ${{ github.event_name }}' - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: ${{ env.checkoutFetchDepth }} - name: 'Get Head Commit Info' @@ -76,7 +76,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set additional variables shell: pwsh run: | @@ -229,30 +229,31 @@ jobs: run: | $env:EFCoreJet_DefaultConnection = '${{ env.defaultConnection }}' & '${{ env.dotnetExecutable }}' test .\test\EFCore.Jet.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --logger trx --verbosity detailed --blame-hang-timeout 3m - - name: 'Run Tests: EFCore.Jet.FunctionalTests' + - name: 'Run Tests: EFCore.Jet.FunctionalTests (Shard 1 - Query Core)' if: always() && env.skipTests != 'true' shell: pwsh run: | + $shardDir = '.\test\EFCore.Jet.FunctionalTests\TestResults\shard1' + for ($i = 0; $i -lt 3; $i++) { - if (Test-Path '.\test\EFCore.Jet.FunctionalTests\TestResults' -PathType Container) { - Get-ChildItem '.\test\EFCore.Jet.FunctionalTests\TestResults' | Remove-Item -Recurse -Force + if (Test-Path $shardDir -PathType Container) { + Get-ChildItem $shardDir | Remove-Item -Recurse -Force } $env:EFCoreJet_DefaultConnection = '${{ env.defaultConnection }}' - & '${{ env.dotnetExecutable }}' test .\test\EFCore.Jet.FunctionalTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --logger trx --verbosity detailed --blame-hang-timeout 3m + & '${{ env.dotnetExecutable }}' test .\test\EFCore.Jet.FunctionalTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --logger trx --verbosity detailed --blame-hang-timeout 3m --results-directory $shardDir --filter "FullyQualifiedName~.FunctionalTests.Query.&FullyQualifiedName!~.FunctionalTests.Query.Associations.&FullyQualifiedName!~.FunctionalTests.Query.Translations." # # Check for test runner crashes: # - $testResultsDir = '.\test\EFCore.Jet.FunctionalTests\TestResults' - $currentTestRunTrx = Get-ChildItem $testResultsDir -Filter '*.trx' | Sort-Object LastWriteTime | Select-Object -Last 1 + $currentTestRunTrx = Get-ChildItem $shardDir -Filter '*.trx' | Sort-Object LastWriteTime | Select-Object -Last 1 if ($null -eq $currentTestRunTrx) { echo 'Test runner log file is missing.' exit 3 } - $currentTestRunDir = Join-Path $testResultsDir $currentTestRunTrx.BaseName + $currentTestRunDir = Join-Path $shardDir $currentTestRunTrx.BaseName if (Test-Path $currentTestRunDir) { if ($null -ne (Get-ChildItem $currentTestRunDir -Filter 'Sequence_*' -Recurse)) { # Split string because searching the log for that phrase should only show actual crashes and not this line. @@ -267,7 +268,97 @@ jobs: $establishedGreenTestsFilePath = ".\test\EFCore.Jet.FunctionalTests\GreenTests\ace_${{ matrix.aceVersion }}_$('${{ matrix.dataAccessProviderType }}'.Replace(' ', '').ToLowerInvariant())_${{ matrix.aceArchitecture }}.txt" $failIfKeepsCrashing = Test-Path $establishedGreenTestsFilePath - + + if ($i -ge 3 -and $failIfKeepsCrashing) { + echo 'Test runner keeps crashing.' + exit 2 + } + exit 0 + - name: 'Run Tests: EFCore.Jet.FunctionalTests (Shard 2 - Query Associations and Translations)' + if: always() && env.skipTests != 'true' + shell: pwsh + run: | + $shardDir = '.\test\EFCore.Jet.FunctionalTests\TestResults\shard2' + + for ($i = 0; $i -lt 3; $i++) { + if (Test-Path $shardDir -PathType Container) { + Get-ChildItem $shardDir | Remove-Item -Recurse -Force + } + + $env:EFCoreJet_DefaultConnection = '${{ env.defaultConnection }}' + & '${{ env.dotnetExecutable }}' test .\test\EFCore.Jet.FunctionalTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --logger trx --verbosity detailed --blame-hang-timeout 3m --results-directory $shardDir --filter "FullyQualifiedName~.FunctionalTests.Query.Associations.|FullyQualifiedName~.FunctionalTests.Query.Translations." + + # + # Check for test runner crashes: + # + + $currentTestRunTrx = Get-ChildItem $shardDir -Filter '*.trx' | Sort-Object LastWriteTime | Select-Object -Last 1 + if ($null -eq $currentTestRunTrx) { + echo 'Test runner log file is missing.' + exit 3 + } + + $currentTestRunDir = Join-Path $shardDir $currentTestRunTrx.BaseName + if (Test-Path $currentTestRunDir) { + if ($null -ne (Get-ChildItem $currentTestRunDir -Filter 'Sequence_*' -Recurse)) { + # Split string because searching the log for that phrase should only show actual crashes and not this line. + echo ('Test runner cras' + 'hed.') + continue + } + } + + echo 'Test runner ran until the end.' + break + } + + $establishedGreenTestsFilePath = ".\test\EFCore.Jet.FunctionalTests\GreenTests\ace_${{ matrix.aceVersion }}_$('${{ matrix.dataAccessProviderType }}'.Replace(' ', '').ToLowerInvariant())_${{ matrix.aceArchitecture }}.txt" + $failIfKeepsCrashing = Test-Path $establishedGreenTestsFilePath + + if ($i -ge 3 -and $failIfKeepsCrashing) { + echo 'Test runner keeps crashing.' + exit 2 + } + exit 0 + - name: 'Run Tests: EFCore.Jet.FunctionalTests (Shard 3 - Non-Query)' + if: always() && env.skipTests != 'true' + shell: pwsh + run: | + $shardDir = '.\test\EFCore.Jet.FunctionalTests\TestResults\shard3' + + for ($i = 0; $i -lt 3; $i++) { + if (Test-Path $shardDir -PathType Container) { + Get-ChildItem $shardDir | Remove-Item -Recurse -Force + } + + $env:EFCoreJet_DefaultConnection = '${{ env.defaultConnection }}' + & '${{ env.dotnetExecutable }}' test .\test\EFCore.Jet.FunctionalTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --logger trx --verbosity detailed --blame-hang-timeout 3m --results-directory $shardDir --filter "FullyQualifiedName!~.FunctionalTests.Query." + + # + # Check for test runner crashes: + # + + $currentTestRunTrx = Get-ChildItem $shardDir -Filter '*.trx' | Sort-Object LastWriteTime | Select-Object -Last 1 + if ($null -eq $currentTestRunTrx) { + echo 'Test runner log file is missing.' + exit 3 + } + + $currentTestRunDir = Join-Path $shardDir $currentTestRunTrx.BaseName + if (Test-Path $currentTestRunDir) { + if ($null -ne (Get-ChildItem $currentTestRunDir -Filter 'Sequence_*' -Recurse)) { + # Split string because searching the log for that phrase should only show actual crashes and not this line. + echo ('Test runner cras' + 'hed.') + continue + } + } + + echo 'Test runner ran until the end.' + break + } + + $establishedGreenTestsFilePath = ".\test\EFCore.Jet.FunctionalTests\GreenTests\ace_${{ matrix.aceVersion }}_$('${{ matrix.dataAccessProviderType }}'.Replace(' ', '').ToLowerInvariant())_${{ matrix.aceArchitecture }}.txt" + $failIfKeepsCrashing = Test-Path $establishedGreenTestsFilePath + if ($i -ge 3 -and $failIfKeepsCrashing) { echo 'Test runner keeps crashing.' exit 2 @@ -280,51 +371,56 @@ jobs: Get-ChildItem -Filter '*.trx' -Recurse | Sort-Object LastWriteTime | ForEach { Rename-Item $_.FullName "ace_${{ matrix.aceVersion }}_$('${{ matrix.dataAccessProviderType }}'.Replace(' ', '').ToLowerInvariant())_${{ matrix.aceArchitecture }}_$($_.Name)" -Verbose } - name: 'Upload Test Results' if: always() && env.skipTests != 'true' && env.uploadTestResults == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: test-results_${{ env.matrixId }} path: | test\EFCore.Jet.Data.Tests\TestResults\*.trx - test\EFCore.Jet.FunctionalTests\TestResults\*.trx + test\EFCore.Jet.FunctionalTests\TestResults\**\*.trx test\EFCore.Jet.Tests\TestResults\*.trx - name: 'Check Tests: EFCore.Jet.FunctionalTests' if: env.skipTests != 'true' shell: pwsh run: | - # Create text file with all tests that passed. + # Collect and merge test results from all shards. $testResultsDir = '.\test\EFCore.Jet.FunctionalTests\TestResults' - $currentTestRunTrx = Get-ChildItem $testResultsDir -Filter '*.trx' | Sort-Object LastWriteTime | Select-Object -Last 1 + $allTrxFiles = @(Get-ChildItem $testResultsDir -Filter '*.trx' -Recurse) - if ($null -eq $currentTestRunTrx) { - echo 'Test runner log file is missing.' + if ($allTrxFiles.Count -eq 0) { + echo 'No test result files found.' exit 3 } - $allTestsFilePath = Join-Path $currentTestRunTrx.DirectoryName ($currentTestRunTrx.BaseName + '_All.txt') - (Select-Xml -Path $currentTestRunTrx.FullName -XPath "//ns:UnitTestResult" -Namespace @{"ns"="http://microsoft.com/schemas/VisualStudio/TeamTest/2010"}).Node | Sort-Object -Property testName -CaseSensitive | ForEach-Object { "$($_.outcome -eq 'Passed' ? 'P' : $_.outcome -eq 'NotExecuted' ? 'N' : $_.outcome -eq 'Failed' ? 'F' : 'U') $($_.testName)" } | Add-Content $allTestsFilePath + $allNodes = @() + foreach ($trxFile in $allTrxFiles) { + $allNodes += (Select-Xml -Path $trxFile.FullName -XPath "//ns:UnitTestResult" -Namespace @{"ns"="http://microsoft.com/schemas/VisualStudio/TeamTest/2010"}).Node + } + + $allTestsFilePath = Join-Path $testResultsDir 'combined_All.txt' + $allNodes | Sort-Object -Property testName -CaseSensitive | ForEach-Object { "$($_.outcome -eq 'Passed' ? 'P' : $_.outcome -eq 'NotExecuted' ? 'N' : $_.outcome -eq 'Failed' ? 'F' : 'U') $($_.testName)" } | Set-Content $allTestsFilePath - $greenTestsFilePath = Join-Path $currentTestRunTrx.DirectoryName ($currentTestRunTrx.BaseName + '_Passed.txt') - Get-Content $allTestsFilePath | Where-Object { $_.StartsWith('P ') } | ForEach-Object { $_.Substring(2) } | Add-Content $greenTestsFilePath + $greenTestsFilePath = Join-Path $testResultsDir 'combined_Passed.txt' + Get-Content $allTestsFilePath | Where-Object { $_.StartsWith('P ') } | ForEach-Object { $_.Substring(2) } | Set-Content $greenTestsFilePath # Compare test file against previously committed file. $establishedGreenTestsFilePath = ".\test\EFCore.Jet.FunctionalTests\GreenTests\ace_${{ matrix.aceVersion }}_$('${{ matrix.dataAccessProviderType }}'.Replace(' ', '').ToLowerInvariant())_${{ matrix.aceArchitecture }}.txt" if (Test-Path $establishedGreenTestsFilePath) { $diffResult = Compare-Object (Get-Content $establishedGreenTestsFilePath) (Get-Content $greenTestsFilePath) - + $notGreenAnymore = $diffResult | Where-Object { $_.SideIndicator -eq '<=' } | Select-Object -ExpandProperty InputObject if ($null -ne $notGreenAnymore) { echo "`nThe following $(@($notGreenAnymore).Length) tests passed in previous runs, but didn't pass in this run:`n" $notGreenAnymore exit 1 } - + echo 'All tests that passed in previous runs still passed in this run.' $newlyGreenTests = $diffResult | Where-Object { $_.SideIndicator -eq '=>' } | Select-Object -ExpandProperty InputObject if ($newlyGreenTests.Length -gt 0) { Copy-Item $greenTestsFilePath $establishedGreenTestsFilePath -Force -Verbose - + echo "`nThe following new tests passed that did not pass before:`n" $newlyGreenTests @@ -335,7 +431,7 @@ jobs: echo 'Check succeeded.' - name: 'Upload Green Tests' if: env.commitGreenTestsFile != '' && env.autoCommitGreenTests == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: green-tests_${{ env.matrixId }} path: ${{ env.commitGreenTestsFile }} @@ -349,7 +445,7 @@ jobs: steps: - name: 'Check Test Results Artifacts' id: CheckTestResultsArtifacts - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | var allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ @@ -369,14 +465,14 @@ jobs: - name: 'Merge Test Results' id: MergeTestResults if: steps.CheckTestResultsArtifacts.outputs.artifactsAvailable == 'true' - uses: actions/upload-artifact/merge@v4 + uses: actions/upload-artifact/merge@v6 with: name: test-results pattern: test-results_* delete-merged: true - name: 'Check Green Tests Artifacts' id: CheckGreenTestsArtifacts - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | var allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ @@ -396,7 +492,7 @@ jobs: - name: 'Merge Green Tests' id: MergeGreenTests if: steps.CheckGreenTestsArtifacts.outputs.artifactsAvailable == 'true' - uses: actions/upload-artifact/merge@v4 + uses: actions/upload-artifact/merge@v6 with: name: green-tests pattern: green-tests_* @@ -418,7 +514,7 @@ jobs: # echo $env:NEEDS_CONTEXT - name: 'Download Green Tests' if: needs.MergeArtifacts.outputs.greenTestsAvailable == 'true' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: green-tests path: green-tests diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 01c90999..ecad2fd2 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -30,7 +30,7 @@ jobs: run: | echo 'EventName: ${{ github.event_name }}' - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: ${{ env.checkoutFetchDepth }} - name: 'Get Head Commit Info' @@ -78,7 +78,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set additional variables shell: pwsh run: | @@ -231,30 +231,31 @@ jobs: run: | $env:EFCoreJet_DefaultConnection = '${{ env.defaultConnection }}' & '${{ env.dotnetExecutable }}' test .\test\EFCore.Jet.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --logger trx --verbosity detailed --blame-hang-timeout 3m - - name: 'Run Tests: EFCore.Jet.FunctionalTests' + - name: 'Run Tests: EFCore.Jet.FunctionalTests (Shard 1 - Query Core)' if: always() && env.skipTests != 'true' shell: pwsh run: | + $shardDir = '.\test\EFCore.Jet.FunctionalTests\TestResults\shard1' + for ($i = 0; $i -lt 3; $i++) { - if (Test-Path '.\test\EFCore.Jet.FunctionalTests\TestResults' -PathType Container) { - Get-ChildItem '.\test\EFCore.Jet.FunctionalTests\TestResults' | Remove-Item -Recurse -Force + if (Test-Path $shardDir -PathType Container) { + Get-ChildItem $shardDir | Remove-Item -Recurse -Force } $env:EFCoreJet_DefaultConnection = '${{ env.defaultConnection }}' - & '${{ env.dotnetExecutable }}' test .\test\EFCore.Jet.FunctionalTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --logger trx --verbosity detailed --blame-hang-timeout 3m + & '${{ env.dotnetExecutable }}' test .\test\EFCore.Jet.FunctionalTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --logger trx --verbosity detailed --blame-hang-timeout 3m --results-directory $shardDir --filter "FullyQualifiedName~.FunctionalTests.Query.&FullyQualifiedName!~.FunctionalTests.Query.Associations.&FullyQualifiedName!~.FunctionalTests.Query.Translations." # # Check for test runner crashes: # - $testResultsDir = '.\test\EFCore.Jet.FunctionalTests\TestResults' - $currentTestRunTrx = Get-ChildItem $testResultsDir -Filter '*.trx' | Sort-Object LastWriteTime | Select-Object -Last 1 + $currentTestRunTrx = Get-ChildItem $shardDir -Filter '*.trx' | Sort-Object LastWriteTime | Select-Object -Last 1 if ($null -eq $currentTestRunTrx) { echo 'Test runner log file is missing.' exit 3 } - $currentTestRunDir = Join-Path $testResultsDir $currentTestRunTrx.BaseName + $currentTestRunDir = Join-Path $shardDir $currentTestRunTrx.BaseName if (Test-Path $currentTestRunDir) { if ($null -ne (Get-ChildItem $currentTestRunDir -Filter 'Sequence_*' -Recurse)) { # Split string because searching the log for that phrase should only show actual crashes and not this line. @@ -269,7 +270,97 @@ jobs: $establishedGreenTestsFilePath = ".\test\EFCore.Jet.FunctionalTests\GreenTests\ace_${{ matrix.aceVersion }}_$('${{ matrix.dataAccessProviderType }}'.Replace(' ', '').ToLowerInvariant())_${{ matrix.aceArchitecture }}.txt" $failIfKeepsCrashing = Test-Path $establishedGreenTestsFilePath - + + if ($i -ge 3 -and $failIfKeepsCrashing) { + echo 'Test runner keeps crashing.' + exit 2 + } + exit 0 + - name: 'Run Tests: EFCore.Jet.FunctionalTests (Shard 2 - Query Associations and Translations)' + if: always() && env.skipTests != 'true' + shell: pwsh + run: | + $shardDir = '.\test\EFCore.Jet.FunctionalTests\TestResults\shard2' + + for ($i = 0; $i -lt 3; $i++) { + if (Test-Path $shardDir -PathType Container) { + Get-ChildItem $shardDir | Remove-Item -Recurse -Force + } + + $env:EFCoreJet_DefaultConnection = '${{ env.defaultConnection }}' + & '${{ env.dotnetExecutable }}' test .\test\EFCore.Jet.FunctionalTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --logger trx --verbosity detailed --blame-hang-timeout 3m --results-directory $shardDir --filter "FullyQualifiedName~.FunctionalTests.Query.Associations.|FullyQualifiedName~.FunctionalTests.Query.Translations." + + # + # Check for test runner crashes: + # + + $currentTestRunTrx = Get-ChildItem $shardDir -Filter '*.trx' | Sort-Object LastWriteTime | Select-Object -Last 1 + if ($null -eq $currentTestRunTrx) { + echo 'Test runner log file is missing.' + exit 3 + } + + $currentTestRunDir = Join-Path $shardDir $currentTestRunTrx.BaseName + if (Test-Path $currentTestRunDir) { + if ($null -ne (Get-ChildItem $currentTestRunDir -Filter 'Sequence_*' -Recurse)) { + # Split string because searching the log for that phrase should only show actual crashes and not this line. + echo ('Test runner cras' + 'hed.') + continue + } + } + + echo 'Test runner ran until the end.' + break + } + + $establishedGreenTestsFilePath = ".\test\EFCore.Jet.FunctionalTests\GreenTests\ace_${{ matrix.aceVersion }}_$('${{ matrix.dataAccessProviderType }}'.Replace(' ', '').ToLowerInvariant())_${{ matrix.aceArchitecture }}.txt" + $failIfKeepsCrashing = Test-Path $establishedGreenTestsFilePath + + if ($i -ge 3 -and $failIfKeepsCrashing) { + echo 'Test runner keeps crashing.' + exit 2 + } + exit 0 + - name: 'Run Tests: EFCore.Jet.FunctionalTests (Shard 3 - Non-Query)' + if: always() && env.skipTests != 'true' + shell: pwsh + run: | + $shardDir = '.\test\EFCore.Jet.FunctionalTests\TestResults\shard3' + + for ($i = 0; $i -lt 3; $i++) { + if (Test-Path $shardDir -PathType Container) { + Get-ChildItem $shardDir | Remove-Item -Recurse -Force + } + + $env:EFCoreJet_DefaultConnection = '${{ env.defaultConnection }}' + & '${{ env.dotnetExecutable }}' test .\test\EFCore.Jet.FunctionalTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --logger trx --verbosity detailed --blame-hang-timeout 3m --results-directory $shardDir --filter "FullyQualifiedName!~.FunctionalTests.Query." + + # + # Check for test runner crashes: + # + + $currentTestRunTrx = Get-ChildItem $shardDir -Filter '*.trx' | Sort-Object LastWriteTime | Select-Object -Last 1 + if ($null -eq $currentTestRunTrx) { + echo 'Test runner log file is missing.' + exit 3 + } + + $currentTestRunDir = Join-Path $shardDir $currentTestRunTrx.BaseName + if (Test-Path $currentTestRunDir) { + if ($null -ne (Get-ChildItem $currentTestRunDir -Filter 'Sequence_*' -Recurse)) { + # Split string because searching the log for that phrase should only show actual crashes and not this line. + echo ('Test runner cras' + 'hed.') + continue + } + } + + echo 'Test runner ran until the end.' + break + } + + $establishedGreenTestsFilePath = ".\test\EFCore.Jet.FunctionalTests\GreenTests\ace_${{ matrix.aceVersion }}_$('${{ matrix.dataAccessProviderType }}'.Replace(' ', '').ToLowerInvariant())_${{ matrix.aceArchitecture }}.txt" + $failIfKeepsCrashing = Test-Path $establishedGreenTestsFilePath + if ($i -ge 3 -and $failIfKeepsCrashing) { echo 'Test runner keeps crashing.' exit 2 @@ -282,31 +373,36 @@ jobs: Get-ChildItem -Filter '*.trx' -Recurse | Sort-Object LastWriteTime | ForEach { Rename-Item $_.FullName "ace_${{ matrix.aceVersion }}_$('${{ matrix.dataAccessProviderType }}'.Replace(' ', '').ToLowerInvariant())_${{ matrix.aceArchitecture }}_$($_.Name)" -Verbose } - name: 'Upload Test Results' if: always() && env.skipTests != 'true' && env.uploadTestResults == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: test-results_${{ env.matrixId }} path: | test\EFCore.Jet.Data.Tests\TestResults\*.trx - test\EFCore.Jet.FunctionalTests\TestResults\*.trx + test\EFCore.Jet.FunctionalTests\TestResults\**\*.trx test\EFCore.Jet.Tests\TestResults\*.trx - name: 'Check Tests: EFCore.Jet.FunctionalTests' if: env.skipTests != 'true' shell: pwsh run: | - # Create text file with all tests that passed. + # Collect and merge test results from all shards. $testResultsDir = '.\test\EFCore.Jet.FunctionalTests\TestResults' - $currentTestRunTrx = Get-ChildItem $testResultsDir -Filter '*.trx' | Sort-Object LastWriteTime | Select-Object -Last 1 + $allTrxFiles = @(Get-ChildItem $testResultsDir -Filter '*.trx' -Recurse) - if ($null -eq $currentTestRunTrx) { - echo 'Test runner log file is missing.' + if ($allTrxFiles.Count -eq 0) { + echo 'No test result files found.' exit 3 } - $allTestsFilePath = Join-Path $currentTestRunTrx.DirectoryName ($currentTestRunTrx.BaseName + '_All.txt') - (Select-Xml -Path $currentTestRunTrx.FullName -XPath "//ns:UnitTestResult" -Namespace @{"ns"="http://microsoft.com/schemas/VisualStudio/TeamTest/2010"}).Node | Sort-Object -Property testName -CaseSensitive | ForEach-Object { "$($_.outcome -eq 'Passed' ? 'P' : $_.outcome -eq 'NotExecuted' ? 'N' : $_.outcome -eq 'Failed' ? 'F' : 'U') $($_.testName)" } | Add-Content $allTestsFilePath + $allNodes = @() + foreach ($trxFile in $allTrxFiles) { + $allNodes += (Select-Xml -Path $trxFile.FullName -XPath "//ns:UnitTestResult" -Namespace @{"ns"="http://microsoft.com/schemas/VisualStudio/TeamTest/2010"}).Node + } - $greenTestsFilePath = Join-Path $currentTestRunTrx.DirectoryName ($currentTestRunTrx.BaseName + '_Passed.txt') - Get-Content $allTestsFilePath | Where-Object { $_.StartsWith('P ') } | ForEach-Object { $_.Substring(2) } | Add-Content $greenTestsFilePath + $allTestsFilePath = Join-Path $testResultsDir 'combined_All.txt' + $allNodes | Sort-Object -Property testName -CaseSensitive | ForEach-Object { "$($_.outcome -eq 'Passed' ? 'P' : $_.outcome -eq 'NotExecuted' ? 'N' : $_.outcome -eq 'Failed' ? 'F' : 'U') $($_.testName)" } | Set-Content $allTestsFilePath + + $greenTestsFilePath = Join-Path $testResultsDir 'combined_Passed.txt' + Get-Content $allTestsFilePath | Where-Object { $_.StartsWith('P ') } | ForEach-Object { $_.Substring(2) } | Set-Content $greenTestsFilePath # Compare test file against previously committed file. $establishedGreenTestsFilePath = ".\test\EFCore.Jet.FunctionalTests\GreenTests\ace_${{ matrix.aceVersion }}_$('${{ matrix.dataAccessProviderType }}'.Replace(' ', '').ToLowerInvariant())_${{ matrix.aceArchitecture }}.txt" @@ -320,24 +416,24 @@ jobs: $notGreenAnymore exit 1 } - + echo 'All tests that passed in previous runs still passed in this run.' $newlyGreenTests = $diffResult | Where-Object { $_.SideIndicator -eq '=>' } | Select-Object -ExpandProperty InputObject if ($newlyGreenTests.Length -gt 0) { Copy-Item $greenTestsFilePath $establishedGreenTestsFilePath -Force -Verbose - + echo "`nThe following new tests passed that did not pass before:`n" $newlyGreenTests $commitGreenTestsFile = $establishedGreenTestsFilePath echo "commitGreenTestsFile=$commitGreenTestsFile" >> $env:GITHUB_ENV - } + } } echo 'Check succeeded.' - name: 'Upload Green Tests' if: ${{ env.commitGreenTestsFile != '' }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: green-tests_${{ env.matrixId }} path: ${{ env.commitGreenTestsFile }} @@ -351,7 +447,7 @@ jobs: steps: - name: 'Check Test Results Artifacts' id: CheckTestResultsArtifacts - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | var allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ @@ -371,14 +467,14 @@ jobs: - name: 'Merge Test Results' id: MergeTestResults if: steps.CheckTestResultsArtifacts.outputs.artifactsAvailable == 'true' - uses: actions/upload-artifact/merge@v4 + uses: actions/upload-artifact/merge@v6 with: name: test-results pattern: test-results_* delete-merged: true - name: 'Check Green Tests Artifacts' id: CheckGreenTestsArtifacts - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | var allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ @@ -398,7 +494,7 @@ jobs: - name: 'Merge Green Tests' id: MergeGreenTests if: steps.CheckGreenTestsArtifacts.outputs.artifactsAvailable == 'true' - uses: actions/upload-artifact/merge@v4 + uses: actions/upload-artifact/merge@v6 with: name: green-tests pattern: green-tests_* @@ -411,9 +507,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: global-json-file: global.json - name: .NET Information @@ -491,7 +587,7 @@ jobs: echo "pushToMygetOrg=$pushToMygetOrg" >> $env:GITHUB_ENV echo "pushToNugetOrg=$pushToNugetOrg" >> $env:GITHUB_ENV - name: Upload Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: nupkgs path: nupkgs diff --git a/.github/workflows/test_results.yml b/.github/workflows/test_results.yml index bcad7a3e..7a7f13db 100644 --- a/.github/workflows/test_results.yml +++ b/.github/workflows/test_results.yml @@ -17,7 +17,7 @@ jobs: shell: bash run: | # - # The later used dorny/test-reporter@v1 action can throw the following exception when enough tests have been + # The later used dorny/test-reporter action can throw the following exception when enough tests have been # executed: # RangeError: Maximum call stack size exceeded # @@ -36,16 +36,15 @@ jobs: which node node --version - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: ${{ github.event.workflow_run.head_repository.full_name }} ref: ${{ github.event.workflow_run.head_branch }} fetch-depth: 1 - # The dorny/test-reporter@v1 action doesn't support actions/upload-artifact@v4 yet. - # We therefore download the artifact manually and feed it to dorny/test-reporter@v1 as local files. + # Download the artifact manually and feed it to dorny/test-reporter as local files. - name: 'Download Test Results' id: DownloadTestResults - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | var allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ @@ -83,7 +82,7 @@ jobs: dir -Recurse | Select-Object -ExpandProperty FullName - name: 'Publish Test Report' if: steps.ExtractArtifact.conclusion == 'success' - uses: dorny/test-reporter@v1 + uses: dorny/test-reporter@v3 env: # # Can throw the following exception, when enough tests have been executed: diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..5282c5de --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# AGENTS.md + +This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. + +## What This Is + +EntityFrameworkCore.Jet is an EF Core provider for Microsoft Jet/ACE databases (Microsoft Access `.mdb`/`.accdb` files). It runs **Windows only** and bridges EF Core to the Access database engine via either ODBC or OLE DB. + +Current version: `10.0.x` targeting EF Core 10 and `net10.0`. + +## Build + +```powershell +dotnet build EFCore.Jet.sln +``` + +Assemblies are **strong-name signed** using `Key.snk`. `TreatWarningsAsErrors=True` is set globally — fix all warnings. + +### Local EFCore Repository (optional) + +To develop against a local EF Core build instead of NuGet packages, copy `Development.props.sample` to `Development.props` and set `LocalEFCoreRepository` to your EF Core checkout. That local build must be compiled with `AssemblyVersion=10.0.0.0` to avoid binding conflicts. + +## Tests + +Tests require a real Microsoft Access driver installed (ODBC or OLE DB) and an actual `.accdb` file — no mocks. The connection string is configured via: +- `test/EFCore.Jet.FunctionalTests/config.json` (OLE DB example present) +- `test/EFCore.Jet.Tests/config.json` (bare filename; picks up default provider) +- Or env var `EFCoreJet_DefaultConnection` + +**Run all tests** (requires x86 or x64 matching your driver bitness): + +```powershell +dotnet test EFCore.Jet.sln --configuration Debug +``` + +**Run a single test class:** + +```powershell +dotnet test test\EFCore.Jet.FunctionalTests\EFCore.Jet.FunctionalTests.csproj --filter "FullyQualifiedName~NorthwindQueryJetTest" +``` + +**Run a single test method:** + +```powershell +dotnet test test\EFCore.Jet.FunctionalTests\EFCore.Jet.FunctionalTests.csproj --filter "FullyQualifiedName=EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindQueryJetTest.Where_simple" +``` + +Tests run in **fixed order by default** (`FIXED_TEST_ORDER` compile constant). All tests lock culture to `en-US` via a module initializer. + +Tests that require features Jet doesn't support are marked `[Fact(Skip = "Unsupported by JET: ...")]` — see `SkipMessages.txt` for the catalog of known unsupported patterns. + +## Project Structure + +``` +src/ + EFCore.Jet.Data/ ADO.NET driver — JetConnection, JetCommand, JetDataReader, + schema management, DUAL table simulation, connection pooling + EFCore.Jet/ EF Core provider — query pipeline, migrations, scaffolding, + type mappings, value generation, conventions + EFCore.Jet.Odbc/ Provider factory for ODBC data access + EFCore.Jet.OleDb/ Provider factory for OLE DB data access + Shared/ Shared source files compiled into multiple src projects + +test/ + EFCore.Jet.Data.Tests/ Unit tests for the ADO.NET driver layer + EFCore.Jet.FunctionalTests/ EF Core specification tests (adapted from EF Core's own suite) + EFCore.Jet.Tests/ Additional functional tests + EFCore.Jet.IntegrationTests/ Integration scenario tests + JetProviderExceptionTests/ Exception-path tests + Shared/ Shared test infrastructure (xunit framework customizations, + test orderers, conditional test attributes) +``` + +## Architecture: Two-Layer Design + +**Layer 1 — `EFCore.Jet.Data`** wraps the raw ODBC/OLE DB driver: +- `JetConnection` detects whether the connection string is ODBC or OLE DB and delegates to the appropriate inner `DbConnection`. +- `JetCommand` rewrites SQL at runtime: handles `SELECT SKIP`, emulates `@@ROWCOUNT`, rewrites `TOP @param`, parses `IF NOT EXISTS ... THEN ...` syntax, and intercepts stored-procedure creation. +- `JetConfiguration` holds global settings: `TimeSpanOffset` (Jet has no TimeSpan; dates are offset from 1899-12-30), `CustomDualTableName`, `IntegerNullValue`, `UseConnectionPooling`. +- Schema operations (create/drop database, list tables) have three implementations: ADOX, DAO, and Precise, selected based on available COM libraries. + +**Layer 2 — `EFCore.Jet`** is the EF Core provider: +- `JetServiceCollectionExtensions.AddEntityFrameworkJet()` registers all provider services. +- `JetQuerySqlGenerator` extends `QuerySqlGenerator` to produce Jet-compatible SQL — converts `CAST` to Jet VBA functions (`CBOOL`, `CINT`, `CLNG`, etc.), handles boolean/numeric null semantics. +- `JetQueryTranslationPostprocessor` applies Jet-specific query rewrites in order: skip/take transformation → base postprocessing → optional millisecond support → ORDER BY lifting. `JetSkipTakePostprocessor` emulates `SKIP`/`OFFSET` since Jet only supports `SELECT TOP n`. +- `JetMigrationsSqlGenerator` generates DDL for Access (no `ALTER COLUMN`, limited constraint support). +- `JetHistoryRepository` implements migration locking via a `__EFMigrationsLock` table with `LockReleaseBehavior.Explicit`. +- `JetRelationalConnection` creates an "empty" (masterless) connection for database creation/drop operations. + +## Key Jet SQL Constraints + +These shape much of the query pipeline complexity: +- No `OFFSET` — emulated via subquery or `TOP`+skip in the data layer +- `SELECT TOP n` only supports a literal integer, not a parameter (rewritten at command level) +- Subqueries in `SELECT` list are limited; scalar subqueries only work in `FROM` +- No parallel transactions (OLE DB) +- No millisecond precision in `DateTime` +- `CROSS JOIN` and mixed `JOIN`/comma syntax must be ordered correctly +- Booleans stored as `-1`/`0` (numeric), not `TRUE`/`FALSE` +- `GUID` support is indirect +- No `rowversion`, no `DateTimeOffset`, no nullable `BIT` + +## Versioning + +`Version.props` owns `VersionPrefix` and `PreReleaseVersionLabel`. Bump `VersionPrefix` after each release. Valid labels: `alpha`, `beta`, `silver`, `preview`, `rc`, `rtm`, `servicing`. CI sets `OfficialVersion`, `ContinuousIntegrationTimestamp`, and `BuildSha` automatically. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..0718af3d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,105 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Is + +EntityFrameworkCore.Jet is an EF Core provider for Microsoft Jet/ACE databases (Microsoft Access `.mdb`/`.accdb` files). It runs **Windows only** and bridges EF Core to the Access database engine via either ODBC or OLE DB. + +Current version: `10.0.x` targeting EF Core 10 and `net10.0`. + +## Build + +```powershell +dotnet build EFCore.Jet.sln +``` + +Assemblies are **strong-name signed** using `Key.snk`. `TreatWarningsAsErrors=True` is set globally — fix all warnings. + +### Local EFCore Repository (optional) + +To develop against a local EF Core build instead of NuGet packages, copy `Development.props.sample` to `Development.props` and set `LocalEFCoreRepository` to your EF Core checkout. That local build must be compiled with `AssemblyVersion=10.0.0.0` to avoid binding conflicts. + +## Tests + +Tests require a real Microsoft Access driver installed (ODBC or OLE DB) and an actual `.accdb` file — no mocks. The connection string is configured via: +- `test/EFCore.Jet.FunctionalTests/config.json` (OLE DB example present) +- `test/EFCore.Jet.Tests/config.json` (bare filename; picks up default provider) +- Or env var `EFCoreJet_DefaultConnection` + +**Run all tests** (requires x86 or x64 matching your driver bitness): + +```powershell +dotnet test EFCore.Jet.sln --configuration Debug +``` + +**Run a single test class:** + +```powershell +dotnet test test\EFCore.Jet.FunctionalTests\EFCore.Jet.FunctionalTests.csproj --filter "FullyQualifiedName~NorthwindQueryJetTest" +``` + +**Run a single test method:** + +```powershell +dotnet test test\EFCore.Jet.FunctionalTests\EFCore.Jet.FunctionalTests.csproj --filter "FullyQualifiedName=EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindQueryJetTest.Where_simple" +``` + +Tests run in **fixed order by default** (`FIXED_TEST_ORDER` compile constant). All tests lock culture to `en-US` via a module initializer. + +Tests that require features Jet doesn't support are marked `[Fact(Skip = "Unsupported by JET: ...")]` — see `SkipMessages.txt` for the catalog of known unsupported patterns. + +## Project Structure + +``` +src/ + EFCore.Jet.Data/ ADO.NET driver — JetConnection, JetCommand, JetDataReader, + schema management, DUAL table simulation, connection pooling + EFCore.Jet/ EF Core provider — query pipeline, migrations, scaffolding, + type mappings, value generation, conventions + EFCore.Jet.Odbc/ Provider factory for ODBC data access + EFCore.Jet.OleDb/ Provider factory for OLE DB data access + Shared/ Shared source files compiled into multiple src projects + +test/ + EFCore.Jet.Data.Tests/ Unit tests for the ADO.NET driver layer + EFCore.Jet.FunctionalTests/ EF Core specification tests (adapted from EF Core's own suite) + EFCore.Jet.Tests/ Additional functional tests + EFCore.Jet.IntegrationTests/ Integration scenario tests + JetProviderExceptionTests/ Exception-path tests + Shared/ Shared test infrastructure (xunit framework customizations, + test orderers, conditional test attributes) +``` + +## Architecture: Two-Layer Design + +**Layer 1 — `EFCore.Jet.Data`** wraps the raw ODBC/OLE DB driver: +- `JetConnection` detects whether the connection string is ODBC or OLE DB and delegates to the appropriate inner `DbConnection`. +- `JetCommand` rewrites SQL at runtime: handles `SELECT SKIP`, emulates `@@ROWCOUNT`, rewrites `TOP @param`, parses `IF NOT EXISTS ... THEN ...` syntax, and intercepts stored-procedure creation. +- `JetConfiguration` holds global settings: `TimeSpanOffset` (Jet has no TimeSpan; dates are offset from 1899-12-30), `CustomDualTableName`, `IntegerNullValue`, `UseConnectionPooling`. +- Schema operations (create/drop database, list tables) have three implementations: ADOX, DAO, and Precise, selected based on available COM libraries. + +**Layer 2 — `EFCore.Jet`** is the EF Core provider: +- `JetServiceCollectionExtensions.AddEntityFrameworkJet()` registers all provider services. +- `JetQuerySqlGenerator` extends `QuerySqlGenerator` to produce Jet-compatible SQL — converts `CAST` to Jet VBA functions (`CBOOL`, `CINT`, `CLNG`, etc.), handles boolean/numeric null semantics. +- `JetQueryTranslationPostprocessor` applies Jet-specific query rewrites in order: skip/take transformation → base postprocessing → optional millisecond support → ORDER BY lifting. `JetSkipTakePostprocessor` emulates `SKIP`/`OFFSET` since Jet only supports `SELECT TOP n`. +- `JetMigrationsSqlGenerator` generates DDL for Access (no `ALTER COLUMN`, limited constraint support). +- `JetHistoryRepository` implements migration locking via a `__EFMigrationsLock` table with `LockReleaseBehavior.Explicit`. +- `JetRelationalConnection` creates an "empty" (masterless) connection for database creation/drop operations. + +## Key Jet SQL Constraints + +These shape much of the query pipeline complexity: +- No `OFFSET` — emulated via subquery or `TOP`+skip in the data layer +- `SELECT TOP n` only supports a literal integer, not a parameter (rewritten at command level) +- Subqueries in `SELECT` list are limited; scalar subqueries only work in `FROM` +- No parallel transactions (OLE DB) +- No millisecond precision in `DateTime` +- `CROSS JOIN` and mixed `JOIN`/comma syntax must be ordered correctly +- Booleans stored as `-1`/`0` (numeric), not `TRUE`/`FALSE` +- `GUID` support is indirect +- No `rowversion`, no `DateTimeOffset`, no nullable `BIT` + +## Versioning + +`Version.props` owns `VersionPrefix` and `PreReleaseVersionLabel`. Bump `VersionPrefix` after each release. Valid labels: `alpha`, `beta`, `silver`, `preview`, `rc`, `rtm`, `servicing`. CI sets `OfficialVersion`, `ContinuousIntegrationTimestamp`, and `BuildSha` automatically. diff --git a/Dependencies.targets b/Dependencies.targets index ebab4e47..62c772be 100644 --- a/Dependencies.targets +++ b/Dependencies.targets @@ -1,8 +1,8 @@ - [10.0.5,10.0.999] - [10.0.5,10.0.999] - [10.0.5,10.0.999] + [10.0.9,10.0.999] + [10.0.9,10.0.999] + [10.0.9,10.0.999] @@ -14,7 +14,7 @@ - + @@ -28,10 +28,10 @@ - - - - + + + + diff --git a/Version.props b/Version.props index cb7abfa9..cf38442b 100644 --- a/Version.props +++ b/Version.props @@ -15,8 +15,8 @@ Bump-up to the next iteration immediately after a release, so that subsequent daily builds are named correctly. --> - 10.0.0 - rtm + 10.0.1 + servicing 0 - 1 + 0 \ No newline at end of file diff --git a/test/EFCore.Jet.IntegrationTests/EFCore.Jet.IntegrationTests.csproj b/test/EFCore.Jet.IntegrationTests/EFCore.Jet.IntegrationTests.csproj index 7b9b5bd2..76a4ec50 100644 --- a/test/EFCore.Jet.IntegrationTests/EFCore.Jet.IntegrationTests.csproj +++ b/test/EFCore.Jet.IntegrationTests/EFCore.Jet.IntegrationTests.csproj @@ -87,7 +87,7 @@ - + diff --git a/test/EFCore.Jet.Tests/EFCore.Jet.Tests.csproj b/test/EFCore.Jet.Tests/EFCore.Jet.Tests.csproj index 8f06624c..2a2f9362 100644 --- a/test/EFCore.Jet.Tests/EFCore.Jet.Tests.csproj +++ b/test/EFCore.Jet.Tests/EFCore.Jet.Tests.csproj @@ -34,7 +34,7 @@ - +