diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..e6afd137d --- /dev/null +++ b/.claude/settings.json @@ -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" + } + ] + } + ] + } +} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cc58bf594..bb656ebda 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -89,6 +89,7 @@ jobs: - *shared - 'src/Shared/**' - 'src/EFCore.Jet/**' + - 'src/EFCore.Jet.Common/**' - 'src/EFCore.Jet.Data/**' - 'src/EFCore.Jet.Odbc/**' - 'src/EFCore.Jet.OleDb/**' @@ -104,12 +105,9 @@ 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 - # src/LibRed is referenced by the Jet provider. - - 'src/EFCore.Jet/**' - - 'src/EFCore.Jet.Data/**' + - 'src/EFCore.Jet.Common/**' # Northwind.accdb lives here and every LibRed suite links to it as its fixture. - 'test/JetProviderExceptionTests/**' - name: 'Decide What To Run' @@ -678,6 +676,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 diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 7a8747e0a..1cb9be403 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -97,6 +97,7 @@ jobs: - *shared - 'src/Shared/**' - 'src/EFCore.Jet/**' + - 'src/EFCore.Jet.Common/**' - 'src/EFCore.Jet.Data/**' - 'src/EFCore.Jet.Odbc/**' - 'src/EFCore.Jet.OleDb/**' @@ -112,12 +113,9 @@ 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 - # src/LibRed is referenced by the Jet provider. - - 'src/EFCore.Jet/**' - - 'src/EFCore.Jet.Data/**' + - 'src/EFCore.Jet.Common/**' # Northwind.accdb lives here and every LibRed suite links to it as its fixture. - 'test/JetProviderExceptionTests/**' - name: 'Decide What To Run' @@ -686,6 +684,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 @@ -798,10 +802,7 @@ jobs: pattern: green-tests_* delete-merged: true NuGet: - needs: - - BuildAndTest - - MergeArtifacts - if: always() && (needs.BuildAndTest.result == 'success' || needs.BuildAndTest.result == 'skipped') && (github.event_name == 'push' || github.event_name == 'release') && needs.MergeArtifacts.result == 'success' && github.repository == 'CirrusRedOrg/EntityFrameworkCore.Jet' + if: (github.event_name == 'push' || github.event_name == 'release') && github.repository == 'CirrusRedOrg/EntityFrameworkCore.Jet' runs-on: ubuntu-latest steps: - name: Checkout diff --git a/AGENTS.md b/AGENTS.md index cb5b05dbd..edbf1ca3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,12 +1,28 @@ # AGENTS.md -This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. +This file provides guidance to Codex (and other coding agents) when working with code in this repository. +It is kept in sync with `CLAUDE.md`; if you change guidance in one, check whether the other needs the same edit. ## 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. +EntityFrameworkCore.Jet is an EF Core provider for Microsoft Jet/ACE databases (Microsoft Access `.mdb`/`.accdb` files). The **Jet** provider runs **Windows only** and bridges EF Core to the Access database engine via either ODBC or OLE DB. Alongside it, **LibRed** (also in this repo, on `master`) is a from-scratch managed engine that reads/writes the file format directly and is **cross-platform** — see the LibRed section below. -Current version: `10.0.x` targeting EF Core 10 and `net10.0`. +Current version: `11.0.0-alpha.1` (`Version.props`) targeting EF Core 11 and `net11.0`; `global.json` pins an 11.0.100 preview SDK with `rollForward: latestFeature`. The test projects use **xunit v3**. + +### Which layer am I touching? + +- `src/EFCore.Jet.Data`, `src/EFCore.Jet`, `src/EFCore.Jet.Odbc`, `src/EFCore.Jet.OleDb` — Windows-only, ACE driver + required, tests need a real Access driver. Each of the four applies its own `[SupportedOSPlatform("windows")]` + assembly attribute; nothing in `Directory.Build.props` stamps it any more. +- `src/EFCore.Jet.Common` — the Jet-dialect services **both** providers share: the query pipeline and expression + translators, `JetMigrationsSqlGenerator`, conventions, annotations, value generation, `JetStrings`. Cross-platform, + and it must stay that way — it may **not** reference `EFCore.Jet.Data`, because LibRed depends on it. +- `src/LibRed/*` — no ACE, no COM, runs on Linux/macOS/ARM64; its tests either need no driver at all or exist specifically to cross-check LibRed's output against the real ACE engine. + +Neither provider references the other, and nothing under `src/LibRed` is referenced by the Jet provider. Both sit on +Common: `EFCore.Jet` → `EFCore.Jet.Data` + `EFCore.Jet.Common`, and `LibRed.EFCore` → `LibRed.Ado` + +`EFCore.Jet.Common`. So a change in Common has to re-run **both** suites; a change in `EFCore.Jet` or +`EFCore.Jet.Data` no longer affects LibRed at all. ## Build @@ -18,15 +34,18 @@ Assemblies are **strong-name signed** using `Key.snk`. `TreatWarningsAsErrors=Tr ### 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. +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=11.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: +**Jet** 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) +- `test/EFCore.LibRed.FunctionalTests/config.json` (LibRed connection) - Or env var `EFCoreJet_DefaultConnection` +**LibRed** tests split in two: `LibRed.Engine.Tests` and `EFCore.LibRed.FunctionalTests` need **no driver at all** and CI runs them on Linux/Windows/macOS plus ARM64 legs — that matrix is what proves the cross-platform claim, so don't add an ACE dependency to them. `LibRed.Core.Tests`, `LibRed.Engine.AccessTests`, `LibRed.Ado.Tests` and `LibRed.EFCore.Tests` deliberately cross-check LibRed's output against the real engine over OLE DB, so they need Windows + ACE. + **Run all tests** (requires x86 or x64 matching your driver bitness): ```powershell @@ -45,25 +64,44 @@ dotnet test test\EFCore.Jet.FunctionalTests\EFCore.Jet.FunctionalTests.csproj -- dotnet test test\EFCore.Jet.FunctionalTests\EFCore.Jet.FunctionalTests.csproj --filter "FullyQualifiedName=EntityFrameworkCore.Jet.FunctionalTests.Query.NorthwindQueryJetTest.Where_simple" ``` -**When running a suite, capture the failing test *names* in the same run** — don't -reduce the output to just the `Passed!/Failed!` count line and then re-run the whole -suite to find which failed. Grep a pattern that catches both, e.g. -`grep -iE "Passed!|Failed!|\[FAIL\]|error CS"` (xUnit prints `… [FAIL]` and -`Failed ` lines as it goes), or tee the full output to a file and -inspect it. Only re-run after changing something. +**When running a suite, capture the failing test *names* in the same run** — don't reduce the output to just the `Passed!/Failed!` count line and then re-run the whole suite to find which failed. Grep a pattern that catches both, e.g. `grep -iE "Passed!|Failed!|\[FAIL\]|error CS"` (xUnit prints `… [FAIL]` and `Failed ` lines as it goes), or tee the full output to a file and inspect it. Only re-run after changing something. -Tests run in **fixed order by default** (`FIXED_TEST_ORDER` compile constant). All tests lock culture to `en-US` via a module initializer. +Tests run in **fixed order by default** (`FIXED_TEST_ORDER` compile constant, set unless `-p:FixedTestOrder=false`; see `test/Directory.Build.props`). All tests lock culture to `en-US` via a module initializer (`test/Shared/ModuleInitializer.cs`). Tests that require features Jet doesn't support are skipped with a reason on the test. +**`EFCore.Jet.Tests` contains no tests.** Its only test files have been ``d since the EF 9 update, so the project builds to an empty assembly and CI does not run it. Most of what was in there is covered by `EFCore.Jet.FunctionalTests` now. Don't read a green run of it as coverage. + +### Green-tests baseline + +`EFCore.Jet.FunctionalTests` is gated by a committed pass-list rather than by "everything must pass": +`test/EFCore.Jet.FunctionalTests/GreenTests/ace___.txt` lists the tests that passed +previously for that matrix leg. CI merges the shards' `.trx` files and **fails if any listed test stops passing**; +newly-passing tests are appended and pushed back by the `auto_commit` workflow. So the meaningful question for a +change is "did anything that used to pass stop passing", not the raw failure count. + +CI splits the functional suite into three shards (query core / Northwind+GearsOfWar / non-query) and retries a shard +up to three times if the runner crashes. `EFCore.LibRed.FunctionalTests` is `continue-on-error` for now — it still +runs on every push, but its remaining failures don't block. + +### Docker images + +`.docker/` builds Windows images preloaded with a given ACE version and bitness +(`windows-ace-{12.0,16.0,none}-{x64,x86}.dockerfile`, built by `BuildAllDockerfiles.ps1`) — useful for reproducing a +matrix leg locally without installing that ACE on your own machine. + ## 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/ EF Core provider — the Jet-only half: options, connection, + database creator, scaffolding, transactions, type mappings + EFCore.Jet.Common/ Jet-dialect services shared with LibRed (cross-platform, no + EFCore.Jet.Data reference) — query pipeline and translators, + migrations SQL generation, conventions, annotations, + value generation, JetStrings 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 @@ -71,28 +109,52 @@ src/ see "LibRed" section below and src/LibRed/README.md 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) + EFCore.Jet.Data.Tests/ Unit tests for the ADO.NET driver layer [Windows + ACE] + EFCore.Jet.FunctionalTests/ EF Core specification tests (adapted from EF Core's + own suite); owns GreenTests/ and Northwind fixtures [Windows + ACE] + EFCore.Jet.Tests/ EMPTY — its test files are d [Windows + ACE] + EFCore.Jet.IntegrationTests/ Integration scenario tests [Windows + ACE] + JetProviderExceptionTests/ Exception-path tests; also hosts Northwind.accdb, + which every LibRed suite links to as its fixture [Windows + ACE] + LibRed.Core.Tests/ File-format read/write, cross-checked against ACE [Windows + ACE] + LibRed.Engine.Tests/ Planner/executor, no engine dependency [cross-platform] + LibRed.Engine.AccessTests/ Engine tests that cross-check against ACE [Windows + ACE] + LibRed.Ado.Tests/ ADO.NET surface [Windows + ACE] + LibRed.EFCore.Tests/ LibRed EF Core provider: query round-trip, + database-first scaffolding [Windows + ACE] + EFCore.LibRed.FunctionalTests/ EF Core specification suite over LibRed [cross-platform] + LibRed.Benchmarks/ BenchmarkDotNet harness (not a test project) + Shared/ ModuleInitializer.cs — locks culture to en-US + +tools/ + sortkey-table/ Generates the Windows NLS sort-weight table LibRed's index keys use + JetLockTrace/ Decodes ACE .laccdb/.ldb locking from a ProcMon capture + Resources.tt Resource generation template ``` +Sibling instruction files: `CLAUDE.md` (Claude Code) and `.github/copilot-instructions.md` mirror parts of this file — +when you change guidance here, check whether they need the same edit. `LIBRED_AUDIT_REPORT.md` (2026-07-18) records +a security/robustness audit of the LibRed production code and the fixes made from it. + ## 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`. +- `JetConfiguration` holds global settings: `TimeSpanOffset` (Jet has no TimeSpan; dates are offset from 1899-12-30), `IntegerNullValue`, `UseConnectionPooling`. The DUAL table name is **not** here — it moved to `JetDualTable` in `EFCore.Jet.Common`, because the shared SQL generator emits it and the scaffolding factory detects it. - 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: +**Layer 2 — `EFCore.Jet` + `EFCore.Jet.Common`** is the EF Core provider. The split is by *what varies per +provider*, not by subject area: anything bound to the ODBC/OLE DB driver or to one provider's identity stays in +`EFCore.Jet` (options, connection, database creator, transactions, execution strategy, scaffolding, type mappings, +`AddEntityFrameworkJet`, `UseJet`), and the dialect itself lives in `EFCore.Jet.Common` so LibRed can register the +very same services. Where LibRed needs different behaviour it owns a 1:1 copy in `LibRed.EFCore` rather than the +shared type subclassing or branching — see `LibRedConventionSetBuilder`, `LibRedHistoryRepository`, +`LibRedCodeGenerator`, `LibRedDesignTimeServices`. The types below are named where they live today: - `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). +- `JetQueryTranslationPostprocessor` applies Jet-specific query rewrites in this order: skip/take transformation → base postprocessing → **append the query's last identifier column to `ORDER BY`** (deterministic tie-breaking, only when the query already orders) → optional millisecond support → ORDER BY lifting. `JetSkipTakePostprocessor` emulates `SKIP`/`OFFSET` since Jet only supports `SELECT TOP n`. Note it reaches `SelectExpression._identifier` by reflection, so an EF Core update can break it at runtime rather than at compile time. +- `JetMigrationsSqlGenerator` generates DDL for Access. It **does** emit `ALTER TABLE … ALTER COLUMN` (Jet's form folds the default value into the `ALTER COLUMN` rather than taking a separate operation); constraint support is still limited. - `JetHistoryRepository` implements migration locking via a `__EFMigrationsLock` table with `LockReleaseBehavior.Explicit`. - `JetRelationalConnection` creates an "empty" (masterless) connection for database creation/drop operations. @@ -109,7 +171,38 @@ These shape much of the query pipeline complexity: - `GUID` support is indirect - No `rowversion`, no `DateTimeOffset`, no nullable `BIT` -## LibRed — Native Managed Engine (`libred` branch) +## Heritage: Jet Is Built on OLE Automation + +Most of the constraints above are not arbitrary Jet choices — they are **OLE Automation semantics**, inherited +because Access is a VBA host built on OA/COM types, with DAO and ADOX as COM libraries over the top. Recognising +this makes the behaviour predictable rather than mysterious, and it tells you *where the spec lives*: when Access +and .NET disagree, the OA definition is usually the tiebreaker, and matching Access means matching a mid-90s COM +contract. + +- **Dates** — the 1899-12-30 epoch is the OA `DATE` type: a `double`, integer part days, fraction time of day. + OA has no TimeSpan, hence `JetConfiguration.TimeSpanOffset`. +- **Booleans as `-1`/`0`** — `VARIANT_BOOL`, where `VARIANT_TRUE` is `0xFFFF`. A VARIANT choice, not a storage one. +- **Currency** — OA's `CY`: an `int64` scaled by 10,000, so exactly four decimal places. +- **`GUID` being indirect**, and the general awkwardness of type coercion — everything funnels through VARIANT. +- **VBA functions are OA-era Basic runtime**: `ROUND` widens Currency to Double; `Rnd` is the VB6 24-bit LCG + (see `SessionState.RandSeed`); `VarType`/`TypeName` return VARTYPE codes and VB type names, where `Integer` + is Int16 and `Long Integer` is Int32 — a 16-bit-era naming hangover that regularly confuses. + +> **Two kinds of OA dependency — know which you are touching.** Behaviour that is *contractually pinned* is safe: +> what we implement ourselves (the epoch constants, CY scaling, `VARIANT_BOOL`, the `Rnd` LCG, `VarType`), and +> BCL APIs defined against an OA type — `DateTime.FromOADate`/`ToOADate` specify the 1899-12-30 epoch and the +> days-plus-fraction double, so they cannot drift without breaking their own contract. The risk is *unspecified +> rounding or precision policy inside a general-purpose coercion* — `Convert.ToDecimal(double)` and friends — +> where the OA behaviour was implementation detail rather than contract, and can therefore change +> underneath us. It did: dotnet/runtime#130566 (.NET 11 preview 7) dropped `Convert.ToDecimal`'s 15-significant-digit +> rounding, which came from OA's own `VarDecFromR8` and had been stable since the 1990s. That turned +> `SUM(ROUND(UnitPrice, 2))` into `58.600000000000001421085471520`. The fix was to own it: +> `src/EFCore.Jet.Data/JetDecimalConverter.cs`. **When a long-stable conversion suddenly misbehaves with no code +> change on our side, suspect the runtime's OA-era compatibility behaviour before suspecting the provider.** + +## LibRed — Native Managed Engine + +(No longer a side branch — LibRed is on `master` and builds as part of `EFCore.Jet.sln`.) A from-scratch, **fully managed and cross-platform** reimplementation of the Jet/ACE engine under `src/LibRed/`. It reads and writes the `.mdb`/`.accdb` file format @@ -130,14 +223,36 @@ src/LibRed/ LibRed.Engine/ Logical Plan nodes, QueryPlanner, CatalogSchemaProvider (bridges the catalog to the binder), QueryExecutor, QueryEngine facade LibRed.Ado/ ADO.NET surface: DbConnection/Command/DataReader/Parameter/Transaction/Factory - LibRed.EFCore/ EF Core provider over LibRed.Ado: AddEntityFrameworkLibRed/UseLibRed, - connection, database creator, database-first scaffolding (query round-trips - + scaffolding pass; IQuerySqlGeneratorFactory override still planned) + LibRed.EFCore/ EF Core provider over LibRed.Ado + EFCore.Jet.Common (no EFCore.Jet + reference): AddEntityFrameworkLibRed/UseLibRed, its own options, type + mappings, connection, database creator, transactions, convention set + builder, history repository, code generator and design-time services; + the query pipeline comes from Common. An extended mode with its own + query SQL generator is planned; compat mode keeps using Common's. ``` -**Build configuration:** `src/LibRed/Directory.Build.props` deliberately bypasses -`src/Directory.Build.props` (it imports the repo-root props directly) so these assemblies -are **not** stamped `[SupportedOSPlatform("windows")]`. Strong-naming is preserved. +**Format version gating and auto-upgrade — LibRed already does this.** Don't grep for `JetCapabilities`; +the names are `JetVersion` / `RequiredVersion` / `EnsureFormatAtLeast`: + +- `LibRed.Formats.JetVersion` enumerates the on-disk version byte (`Version3` = 0x00 … `Version17_2019` = 0x06). +- `AccessTypeMapper.RequiredVersion(typeName)` is the capability table: `BIGINT` needs `Version16_2016` (0x05), + `DATETIME2` needs `Version17_2019` (0x06). **Different thresholds** — the natural assumption that ACE 16 added + both at once is wrong, and it's measured in `docs/format/page-00-database.md`. +- `AccessTypeMapper.MapType` **refuses** a type the open file is too old for, so a caller that can't upgrade + (read-only database) fails loudly instead of writing a column Access couldn't read. +- `StatementExecutor.MapColumn` → `JetDatabase.EnsureFormatAtLeast` → `PageChannel.RaiseFormatVersion` + **raises the file's version byte** rather than refusing the DDL, which is what ACE itself does. The raise goes + through `WritePage`, so it joins the statement's transaction and a failed `CREATE`/`ALTER` takes it back down; + both rollback paths re-derive the in-memory `Format` from the version byte then visible. + +The Jet provider (`src/EFCore.Jet*`) has **no** equivalent — it does no ACE version detection, and +`JetTypeMappingSource` keeps `{"bigint", …}` commented out, so `long` maps to `decimal(20,0)` regardless of the +installed engine. That asymmetry is deliberate for now; don't "fix" one side by assuming the other behaves the same. + +**Build configuration:** `src/LibRed/Directory.Build.props` bypasses `src/Directory.Build.props` (it imports the +repo-root props directly) to set its own build options — not packable, no documentation file, its own `NoWarn`, +`ImplicitUsings`/`Nullable` on. The Windows stamp is no longer a reason: each driver-bound project applies +`[SupportedOSPlatform("windows")]` itself. Strong-naming is preserved. **SQL pipeline** (always run end-to-end, even for trivial queries, so new features add node types rather than rewrites): @@ -148,7 +263,9 @@ Jet 4 / ACE format, **split one file per page type** (plus cross-cutting topics) `format/README.md` — it maps every page type and the original §-numbers to their file, and links the `appendix-structures.md` bare field-layout reference. It is the source of truth — keep it updated as the format understanding grows. (`docs/jet-ace-file-format.md` is now just a redirect -stub to that folder.) +stub to that folder.) Alongside it: `docs/functions.md` catalogs the supported VBA/Access function surface, +`docs/design/transactions.md` covers the page-level undo log, and `docs/mdbtools-spec-diff-todo.md` +tracks where our spec and mdbtools' still disagree. > **Rule — spec sync on every `LibRed.Core` change.** Whenever you touch the actual on-disk > read/write code in `src/LibRed/LibRed.Core/` (page/row/index/TDEF/usage-map parsing or @@ -166,6 +283,35 @@ parser: the lexer/parser are pre-generated and committed under `LibRed.Sql/Gramm (managed `Antlr4.Runtime.Standard` runtime only, no build-time codegen), regenerated via `LibRed.Sql/Grammar/generate.ps1` after editing `AccessSql.g4`. +> **LibRed is a single-writer engine.** It tolerates extra open handles (a `.accdb` is a shared-file database and +> EF's own test infra keeps a store connection open), but there is no lock file, no read isolation, and the +> transaction undo log is per-`PageChannel`. Two concurrent writers corrupt the file. See `src/LibRed/README.md` +> for the full statement of what is and isn't safe before designing anything concurrent on top of it. + +## Working in This Repo + +**Do not edit source files through the shell.** No `sed -i`, no redirects or `tee` into `.cs`/`.md`/`.csproj`/ +`.props`/`.json`/`.ps1`/`.g4`, and no Python scripts that rewrite files. Shell edits bypass the agent's file +checkpointing, so the change cannot be rolled back. Use your editor/patch tooling instead. (Claude Code enforces +this with `PreToolUse` hooks in `.claude/settings.json`, which also block reading files via `cat`/`grep`/`ls` in +favour of its own file tools; the underlying convention applies to any agent working here.) + +Writing scratch files and logs outside the repo is fine. `dotnet build`/`test`/`restore` and read-only `git` +commands are the expected shell usage. + +## CI + +`.github/workflows/push.yml` runs on every branch. A `Changes` job path-filters the push's **own** delta into two +flags — `jet` and `libred` — and skips the jobs that don't apply: + +- **BuildAndTest** — the Jet matrix: ACE 2010/2016 × x64/x86 × ODBC/OLE DB on `windows-latest`. The x86 legs patch + `IMAGE_FILE_LARGE_ADDRESS_AWARE` onto `dotnet.exe` and the test hosts, because ACE in a 2GB address space dies + partway through the largest shard. +- **LibRed** — `LibRed.Engine.Tests` on Linux/Windows/macOS + ubuntu-arm/windows-arm, no ACE anywhere. +- **LibRedAccess** — the ACE cross-check suites on `windows-latest` with ACE 2016. +- **LibRedFunctional** — `EFCore.LibRed.FunctionalTests` on the five-platform matrix, `continue-on-error`. +- **NuGet** — packs and pushes to MyGet/NuGet for `master`, `*-servicing`, `*-wip` and release tags. + ## 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 index 262345dce..da771b98d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,9 +4,24 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 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. +EntityFrameworkCore.Jet is an EF Core provider for Microsoft Jet/ACE databases (Microsoft Access `.mdb`/`.accdb` files). The **Jet** provider runs **Windows only** and bridges EF Core to the Access database engine via either ODBC or OLE DB. Alongside it, **LibRed** (also in this repo, on `master`) is a from-scratch managed engine that reads/writes the file format directly and is **cross-platform** — see the LibRed section below. -Current version: `11.0.x` targeting EF Core 11 and `net11.0`. The test projects use **xunit v3**. +Current version: `11.0.0-alpha.1` (`Version.props`) targeting EF Core 11 and `net11.0`; `global.json` pins an 11.0.100 preview SDK with `rollForward: latestFeature`. The test projects use **xunit v3**. + +### Which layer am I touching? + +- `src/EFCore.Jet.Data`, `src/EFCore.Jet`, `src/EFCore.Jet.Odbc`, `src/EFCore.Jet.OleDb` — Windows-only, ACE driver + required, tests need a real Access driver. Each of the four applies its own `[SupportedOSPlatform("windows")]` + assembly attribute; nothing in `Directory.Build.props` stamps it any more. +- `src/EFCore.Jet.Common` — the Jet-dialect services **both** providers share: the query pipeline and expression + translators, `JetMigrationsSqlGenerator`, conventions, annotations, value generation, `JetStrings`. Cross-platform, + and it must stay that way — it may **not** reference `EFCore.Jet.Data`, because LibRed depends on it. +- `src/LibRed/*` — no ACE, no COM, runs on Linux/macOS/ARM64; its tests either need no driver at all or exist specifically to cross-check LibRed's output against the real ACE engine. + +Neither provider references the other, and nothing under `src/LibRed` is referenced by the Jet provider. Both sit on +Common: `EFCore.Jet` → `EFCore.Jet.Data` + `EFCore.Jet.Common`, and `LibRed.EFCore` → `LibRed.Ado` + +`EFCore.Jet.Common`. So a change in Common has to re-run **both** suites; a change in `EFCore.Jet` or +`EFCore.Jet.Data` no longer affects LibRed at all. ## Build @@ -22,11 +37,14 @@ To develop against a local EF Core build instead of NuGet packages, copy `Develo ## 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: +**Jet** 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) +- `test/EFCore.LibRed.FunctionalTests/config.json` (LibRed connection) - Or env var `EFCoreJet_DefaultConnection` +**LibRed** tests split in two: `LibRed.Engine.Tests` and `EFCore.LibRed.FunctionalTests` need **no driver at all** and CI runs them on Linux/Windows/macOS plus ARM64 legs — that matrix is what proves the cross-platform claim, so don't add an ACE dependency to them. `LibRed.Core.Tests`, `LibRed.Engine.AccessTests`, `LibRed.Ado.Tests` and `LibRed.EFCore.Tests` deliberately cross-check LibRed's output against the real engine over OLE DB, so they need Windows + ACE. + **Run all tests** (requires x86 or x64 matching your driver bitness): ```powershell @@ -47,18 +65,42 @@ dotnet test test\EFCore.Jet.FunctionalTests\EFCore.Jet.FunctionalTests.csproj -- **When running a suite, capture the failing test *names* in the same run** — don't reduce the output to just the `Passed!/Failed!` count line and then re-run the whole suite to find which failed. Grep a pattern that catches both, e.g. `grep -iE "Passed!|Failed!|\[FAIL\]|error CS"` (xUnit prints `… [FAIL]` and `Failed ` lines as it goes), or tee the full output to a file and inspect it. Only re-run after changing something. -Tests run in **fixed order by default** (`FIXED_TEST_ORDER` compile constant). All tests lock culture to `en-US` via a module initializer. +Tests run in **fixed order by default** (`FIXED_TEST_ORDER` compile constant, set unless `-p:FixedTestOrder=false`; see `test/Directory.Build.props`). All tests lock culture to `en-US` via a module initializer (`test/Shared/ModuleInitializer.cs`). Tests that require features Jet doesn't support are skipped with a reason on the test. +**`EFCore.Jet.Tests` contains no tests.** Its only test files have been ``d since the EF 9 update, so the project builds to an empty assembly and CI does not run it. Most of what was in there is covered by `EFCore.Jet.FunctionalTests` now. Don't read a green run of it as coverage. + +### Green-tests baseline + +`EFCore.Jet.FunctionalTests` is gated by a committed pass-list rather than by "everything must pass": +`test/EFCore.Jet.FunctionalTests/GreenTests/ace___.txt` lists the tests that passed +previously for that matrix leg. CI merges the shards' `.trx` files and **fails if any listed test stops passing**; +newly-passing tests are appended and pushed back by the `auto_commit` workflow. So the meaningful question for a +change is "did anything that used to pass stop passing", not the raw failure count. + +CI splits the functional suite into three shards (query core / Northwind+GearsOfWar / non-query) and retries a shard +up to three times if the runner crashes. `EFCore.LibRed.FunctionalTests` is `continue-on-error` for now — it still +runs on every push, but its remaining failures don't block. + +### Docker images + +`.docker/` builds Windows images preloaded with a given ACE version and bitness +(`windows-ace-{12.0,16.0,none}-{x64,x86}.dockerfile`, built by `BuildAllDockerfiles.ps1`) — useful for reproducing a +matrix leg locally without installing that ACE on your own machine. + ## 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/ EF Core provider — the Jet-only half: options, connection, + database creator, scaffolding, transactions, type mappings + EFCore.Jet.Common/ Jet-dialect services shared with LibRed (cross-platform, no + EFCore.Jet.Data reference) — query pipeline and translators, + migrations SQL generation, conventions, annotations, + value generation, JetStrings 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 @@ -66,28 +108,52 @@ src/ see "LibRed" section below and src/LibRed/README.md 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) + EFCore.Jet.Data.Tests/ Unit tests for the ADO.NET driver layer [Windows + ACE] + EFCore.Jet.FunctionalTests/ EF Core specification tests (adapted from EF Core's + own suite); owns GreenTests/ and Northwind fixtures [Windows + ACE] + EFCore.Jet.Tests/ EMPTY — its test files are d [Windows + ACE] + EFCore.Jet.IntegrationTests/ Integration scenario tests [Windows + ACE] + JetProviderExceptionTests/ Exception-path tests; also hosts Northwind.accdb, + which every LibRed suite links to as its fixture [Windows + ACE] + LibRed.Core.Tests/ File-format read/write, cross-checked against ACE [Windows + ACE] + LibRed.Engine.Tests/ Planner/executor, no engine dependency [cross-platform] + LibRed.Engine.AccessTests/ Engine tests that cross-check against ACE [Windows + ACE] + LibRed.Ado.Tests/ ADO.NET surface [Windows + ACE] + LibRed.EFCore.Tests/ LibRed EF Core provider: query round-trip, + database-first scaffolding [Windows + ACE] + EFCore.LibRed.FunctionalTests/ EF Core specification suite over LibRed [cross-platform] + LibRed.Benchmarks/ BenchmarkDotNet harness (not a test project) + Shared/ ModuleInitializer.cs — locks culture to en-US + +tools/ + sortkey-table/ Generates the Windows NLS sort-weight table LibRed's index keys use + JetLockTrace/ Decodes ACE .laccdb/.ldb locking from a ProcMon capture + Resources.tt Resource generation template ``` +Sibling instruction files: `AGENTS.md` (Codex) and `.github/copilot-instructions.md` mirror parts of this file — +when you change guidance here, check whether they need the same edit. `LIBRED_AUDIT_REPORT.md` (2026-07-18) records +a security/robustness audit of the LibRed production code and the fixes made from it. + ## 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`. +- `JetConfiguration` holds global settings: `TimeSpanOffset` (Jet has no TimeSpan; dates are offset from 1899-12-30), `IntegerNullValue`, `UseConnectionPooling`. The DUAL table name is **not** here — it moved to `JetDualTable` in `EFCore.Jet.Common`, because the shared SQL generator emits it and the scaffolding factory detects it. - 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: +**Layer 2 — `EFCore.Jet` + `EFCore.Jet.Common`** is the EF Core provider. The split is by *what varies per +provider*, not by subject area: anything bound to the ODBC/OLE DB driver or to one provider's identity stays in +`EFCore.Jet` (options, connection, database creator, transactions, execution strategy, scaffolding, type mappings, +`AddEntityFrameworkJet`, `UseJet`), and the dialect itself lives in `EFCore.Jet.Common` so LibRed can register the +very same services. Where LibRed needs different behaviour it owns a 1:1 copy in `LibRed.EFCore` rather than the +shared type subclassing or branching — see `LibRedConventionSetBuilder`, `LibRedHistoryRepository`, +`LibRedCodeGenerator`, `LibRedDesignTimeServices`. The types below are named where they live today: - `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). +- `JetQueryTranslationPostprocessor` applies Jet-specific query rewrites in this order: skip/take transformation → base postprocessing → **append the query's last identifier column to `ORDER BY`** (deterministic tie-breaking, only when the query already orders) → optional millisecond support → ORDER BY lifting. `JetSkipTakePostprocessor` emulates `SKIP`/`OFFSET` since Jet only supports `SELECT TOP n`. Note it reaches `SelectExpression._identifier` by reflection, so an EF Core update can break it at runtime rather than at compile time. +- `JetMigrationsSqlGenerator` generates DDL for Access. It **does** emit `ALTER TABLE … ALTER COLUMN` (Jet's form folds the default value into the `ALTER COLUMN` rather than taking a separate operation); constraint support is still limited. - `JetHistoryRepository` implements migration locking via a `__EFMigrationsLock` table with `LockReleaseBehavior.Explicit`. - `JetRelationalConnection` creates an "empty" (masterless) connection for database creation/drop operations. @@ -133,7 +199,9 @@ contract. > `src/EFCore.Jet.Data/JetDecimalConverter.cs`. **When a long-stable conversion suddenly misbehaves with no code > change on our side, suspect the runtime's OA-era compatibility behaviour before suspecting the provider.** -## LibRed — Native Managed Engine (`libred` branch) +## LibRed — Native Managed Engine + +(No longer a side branch — LibRed is on `master` and builds as part of `EFCore.Jet.sln`.) A from-scratch, **fully managed and cross-platform** reimplementation of the Jet/ACE engine under `src/LibRed/`. It reads and writes the `.mdb`/`.accdb` file format @@ -154,14 +222,36 @@ src/LibRed/ LibRed.Engine/ Logical Plan nodes, QueryPlanner, CatalogSchemaProvider (bridges the catalog to the binder), QueryExecutor, QueryEngine facade LibRed.Ado/ ADO.NET surface: DbConnection/Command/DataReader/Parameter/Transaction/Factory - LibRed.EFCore/ EF Core provider over LibRed.Ado: AddEntityFrameworkLibRed/UseLibRed, - connection, database creator, database-first scaffolding (query round-trips - + scaffolding pass; IQuerySqlGeneratorFactory override still planned) + LibRed.EFCore/ EF Core provider over LibRed.Ado + EFCore.Jet.Common (no EFCore.Jet + reference): AddEntityFrameworkLibRed/UseLibRed, its own options, type + mappings, connection, database creator, transactions, convention set + builder, history repository, code generator and design-time services; + the query pipeline comes from Common. An extended mode with its own + query SQL generator is planned; compat mode keeps using Common's. ``` -**Build configuration:** `src/LibRed/Directory.Build.props` deliberately bypasses -`src/Directory.Build.props` (it imports the repo-root props directly) so these assemblies -are **not** stamped `[SupportedOSPlatform("windows")]`. Strong-naming is preserved. +**Format version gating and auto-upgrade — LibRed already does this.** Don't grep for `JetCapabilities`; +the names are `JetVersion` / `RequiredVersion` / `EnsureFormatAtLeast`: + +- `LibRed.Formats.JetVersion` enumerates the on-disk version byte (`Version3` = 0x00 … `Version17_2019` = 0x06). +- `AccessTypeMapper.RequiredVersion(typeName)` is the capability table: `BIGINT` needs `Version16_2016` (0x05), + `DATETIME2` needs `Version17_2019` (0x06). **Different thresholds** — the natural assumption that ACE 16 added + both at once is wrong, and it's measured in `docs/format/page-00-database.md`. +- `AccessTypeMapper.MapType` **refuses** a type the open file is too old for, so a caller that can't upgrade + (read-only database) fails loudly instead of writing a column Access couldn't read. +- `StatementExecutor.MapColumn` → `JetDatabase.EnsureFormatAtLeast` → `PageChannel.RaiseFormatVersion` + **raises the file's version byte** rather than refusing the DDL, which is what ACE itself does. The raise goes + through `WritePage`, so it joins the statement's transaction and a failed `CREATE`/`ALTER` takes it back down; + both rollback paths re-derive the in-memory `Format` from the version byte then visible. + +The Jet provider (`src/EFCore.Jet*`) has **no** equivalent — it does no ACE version detection, and +`JetTypeMappingSource` keeps `{"bigint", …}` commented out, so `long` maps to `decimal(20,0)` regardless of the +installed engine. That asymmetry is deliberate for now; don't "fix" one side by assuming the other behaves the same. + +**Build configuration:** `src/LibRed/Directory.Build.props` bypasses `src/Directory.Build.props` (it imports the +repo-root props directly) to set its own build options — not packable, no documentation file, its own `NoWarn`, +`ImplicitUsings`/`Nullable` on. The Windows stamp is no longer a reason: each driver-bound project applies +`[SupportedOSPlatform("windows")]` itself. Strong-naming is preserved. **SQL pipeline** (always run end-to-end, even for trivial queries, so new features add node types rather than rewrites): @@ -172,7 +262,9 @@ Jet 4 / ACE format, **split one file per page type** (plus cross-cutting topics) `format/README.md` — it maps every page type and the original §-numbers to their file, and links the `appendix-structures.md` bare field-layout reference. It is the source of truth — keep it updated as the format understanding grows. (`docs/jet-ace-file-format.md` is now just a redirect -stub to that folder.) +stub to that folder.) Alongside it: `docs/functions.md` catalogs the supported VBA/Access function surface, +`docs/design/transactions.md` covers the page-level undo log, and `docs/mdbtools-spec-diff-todo.md` +tracks where our spec and mdbtools' still disagree. > **Rule — spec sync on every `LibRed.Core` change.** Whenever you touch the actual on-disk > read/write code in `src/LibRed/LibRed.Core/` (page/row/index/TDEF/usage-map parsing or @@ -190,6 +282,37 @@ parser: the lexer/parser are pre-generated and committed under `LibRed.Sql/Gramm (managed `Antlr4.Runtime.Standard` runtime only, no build-time codegen), regenerated via `LibRed.Sql/Grammar/generate.ps1` after editing `AccessSql.g4`. +> **LibRed is a single-writer engine.** It tolerates extra open handles (a `.accdb` is a shared-file database and +> EF's own test infra keeps a store connection open), but there is no lock file, no read isolation, and the +> transaction undo log is per-`PageChannel`. Two concurrent writers corrupt the file. See `src/LibRed/README.md` +> for the full statement of what is and isn't safe before designing anything concurrent on top of it. + +## Working in This Repo + +`.claude/settings.json` installs `PreToolUse` hooks that **deny** two things in `Bash`/`PowerShell`: + +- **Reading files through the shell** (`cat`, `head`, `grep`, `ls`, `find`, `Get-Content`, `Select-String`, …) — + use `Read`, `Grep`, `Glob` instead. Shell text tools stay allowed on paths containing `scratchpad`, `.log` or `/tmp/`. +- **Editing source files through the shell** (`sed -i`, redirects/`tee` into `.cs`/`.md`/`.csproj`/`.props`/`.json`/ + `.ps1`/`.g4`, and any `python` invocation) — these bypass file checkpointing, so `/rewind` cannot undo them. + Use `Edit`/`Write`. + +`dotnet build`/`test`/`restore` and read-only `git` commands are pre-allowed, so don't work around the hooks — the +denial message is telling you which tool to use, not that the action is forbidden. + +## CI + +`.github/workflows/push.yml` runs on every branch. A `Changes` job path-filters the push's **own** delta into two +flags — `jet` and `libred` — and skips the jobs that don't apply: + +- **BuildAndTest** — the Jet matrix: ACE 2010/2016 × x64/x86 × ODBC/OLE DB on `windows-latest`. The x86 legs patch + `IMAGE_FILE_LARGE_ADDRESS_AWARE` onto `dotnet.exe` and the test hosts, because ACE in a 2GB address space dies + partway through the largest shard. +- **LibRed** — `LibRed.Engine.Tests` on Linux/Windows/macOS + ubuntu-arm/windows-arm, no ACE anywhere. +- **LibRedAccess** — the ACE cross-check suites on `windows-latest` with ACE 2016. +- **LibRedFunctional** — `EFCore.LibRed.FunctionalTests` on the five-platform matrix, `continue-on-error`. +- **NuGet** — packs and pushes to MyGet/NuGet for `master`, `*-servicing`, `*-wip` and release tags. + ## 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/EFCore.Jet.sln b/EFCore.Jet.sln index dfbad4030..73f2327c4 100644 --- a/EFCore.Jet.sln +++ b/EFCore.Jet.sln @@ -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 @@ -82,6 +82,12 @@ 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.Jet.Common", "src\EFCore.Jet.Common\EFCore.Jet.Common.csproj", "{E58BB663-7B44-4321-B9C7-631E583C93C4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.LibRed.Extended.FunctionalTests", "test\EFCore.LibRed.Extended.FunctionalTests\EFCore.LibRed.Extended.FunctionalTests.csproj", "{D734965F-6337-4DAA-A209-B065EAE6A6EB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -332,6 +338,42 @@ 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 + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Debug|x64.ActiveCfg = Debug|x64 + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Debug|x64.Build.0 = Debug|x64 + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Debug|x86.ActiveCfg = Debug|x86 + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Debug|x86.Build.0 = Debug|x86 + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Release|Any CPU.Build.0 = Release|Any CPU + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Release|x64.ActiveCfg = Release|x64 + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Release|x64.Build.0 = Release|x64 + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Release|x86.ActiveCfg = Release|x86 + {E58BB663-7B44-4321-B9C7-631E583C93C4}.Release|x86.Build.0 = Release|x86 + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Debug|x64.ActiveCfg = Debug|x64 + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Debug|x64.Build.0 = Debug|x64 + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Debug|x86.ActiveCfg = Debug|x86 + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Debug|x86.Build.0 = Debug|x86 + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Release|Any CPU.Build.0 = Release|Any CPU + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Release|x64.ActiveCfg = Release|x64 + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Release|x64.Build.0 = Release|x64 + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Release|x86.ActiveCfg = Release|x86 + {D734965F-6337-4DAA-A209-B065EAE6A6EB}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -358,6 +400,9 @@ 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} + {E58BB663-7B44-4321-B9C7-631E583C93C4} = {F68095EE-6CD1-43A2-B498-6CA72CE2A0CB} + {D734965F-6337-4DAA-A209-B065EAE6A6EB} = {6A8DE399-1804-4113-A408-F23B7F5C9CAC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9359773D-6399-447E-9814-6CB41C2FB664} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b38440781..c16703271 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -14,19 +14,6 @@ $(MSBuildThisFileDirectory)..\Key.snk true - - - - <_Parameter1>windows - - - - diff --git a/src/EFCore.Jet/Design/Internal/JetAnnotationCodeGenerator.cs b/src/EFCore.Jet.Common/Design/Internal/JetAnnotationCodeGenerator.cs similarity index 100% rename from src/EFCore.Jet/Design/Internal/JetAnnotationCodeGenerator.cs rename to src/EFCore.Jet.Common/Design/Internal/JetAnnotationCodeGenerator.cs diff --git a/src/EFCore.Jet/Design/Internal/JetCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Jet.Common/Design/Internal/JetCSharpRuntimeAnnotationCodeGenerator.cs similarity index 100% rename from src/EFCore.Jet/Design/Internal/JetCSharpRuntimeAnnotationCodeGenerator.cs rename to src/EFCore.Jet.Common/Design/Internal/JetCSharpRuntimeAnnotationCodeGenerator.cs diff --git a/src/EFCore.Jet/Diagnostics/ConflictingValueGenerationStrategiesEventData.cs b/src/EFCore.Jet.Common/Diagnostics/ConflictingValueGenerationStrategiesEventData.cs similarity index 100% rename from src/EFCore.Jet/Diagnostics/ConflictingValueGenerationStrategiesEventData.cs rename to src/EFCore.Jet.Common/Diagnostics/ConflictingValueGenerationStrategiesEventData.cs diff --git a/src/EFCore.Jet/Diagnostics/Internal/JetLoggingDefinitions.cs b/src/EFCore.Jet.Common/Diagnostics/Internal/JetLoggingDefinitions.cs similarity index 100% rename from src/EFCore.Jet/Diagnostics/Internal/JetLoggingDefinitions.cs rename to src/EFCore.Jet.Common/Diagnostics/Internal/JetLoggingDefinitions.cs diff --git a/src/EFCore.Jet/Diagnostics/JetEventId.cs b/src/EFCore.Jet.Common/Diagnostics/JetEventId.cs similarity index 100% rename from src/EFCore.Jet/Diagnostics/JetEventId.cs rename to src/EFCore.Jet.Common/Diagnostics/JetEventId.cs diff --git a/src/EFCore.Jet.Common/EFCore.Jet.Common.csproj b/src/EFCore.Jet.Common/EFCore.Jet.Common.csproj new file mode 100644 index 000000000..0d4502bdb --- /dev/null +++ b/src/EFCore.Jet.Common/EFCore.Jet.Common.csproj @@ -0,0 +1,64 @@ + + + + Shared Jet/ACE provider services for the EntityFrameworkCore.Jet and EntityFrameworkCore.LibRed providers. + $(JetTargetFramework) + EntityFrameworkCore.Jet + EntityFrameworkCore.Jet.Common + AnyCPU;x86;x64 + enable + enable + $(NoWarn);EF9100 + + + + + + + + + + + + + + TextTemplatingFileGenerator + JetStrings.Designer.cs + + + True + True + JetStrings.Designer.tt + + + EntityFrameworkCore.Jet.Internal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/EFCore.Jet/Extensions/JetDbFunctionsExtensions.cs b/src/EFCore.Jet.Common/Extensions/JetDbFunctionsExtensions.cs similarity index 100% rename from src/EFCore.Jet/Extensions/JetDbFunctionsExtensions.cs rename to src/EFCore.Jet.Common/Extensions/JetDbFunctionsExtensions.cs diff --git a/src/EFCore.Jet/Extensions/JetIndexExtensions.cs b/src/EFCore.Jet.Common/Extensions/JetIndexExtensions.cs similarity index 100% rename from src/EFCore.Jet/Extensions/JetIndexExtensions.cs rename to src/EFCore.Jet.Common/Extensions/JetIndexExtensions.cs diff --git a/src/EFCore.Jet/Extensions/JetModelBuilderExtensions.cs b/src/EFCore.Jet.Common/Extensions/JetModelBuilderExtensions.cs similarity index 100% rename from src/EFCore.Jet/Extensions/JetModelBuilderExtensions.cs rename to src/EFCore.Jet.Common/Extensions/JetModelBuilderExtensions.cs diff --git a/src/EFCore.Jet/Extensions/JetModelExtensions.cs b/src/EFCore.Jet.Common/Extensions/JetModelExtensions.cs similarity index 100% rename from src/EFCore.Jet/Extensions/JetModelExtensions.cs rename to src/EFCore.Jet.Common/Extensions/JetModelExtensions.cs diff --git a/src/EFCore.Jet/Extensions/JetPropertyBuilderExtensions.cs b/src/EFCore.Jet.Common/Extensions/JetPropertyBuilderExtensions.cs similarity index 100% rename from src/EFCore.Jet/Extensions/JetPropertyBuilderExtensions.cs rename to src/EFCore.Jet.Common/Extensions/JetPropertyBuilderExtensions.cs diff --git a/src/EFCore.Jet/Extensions/JetPropertyExtensions.cs b/src/EFCore.Jet.Common/Extensions/JetPropertyExtensions.cs similarity index 100% rename from src/EFCore.Jet/Extensions/JetPropertyExtensions.cs rename to src/EFCore.Jet.Common/Extensions/JetPropertyExtensions.cs diff --git a/src/EFCore.Jet.Common/Infrastructure/JetDualTable.cs b/src/EFCore.Jet.Common/Infrastructure/JetDualTable.cs new file mode 100644 index 000000000..f0bb832ff --- /dev/null +++ b/src/EFCore.Jet.Common/Infrastructure/JetDualTable.cs @@ -0,0 +1,52 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace EntityFrameworkCore.Jet.Infrastructure +{ + /// + /// The DUAL table used by the shared Jet/ACE SQL generation, and by any provider running in a + /// Jet-compatible mode. Held here rather than on JetConfiguration so the generated SQL and the + /// scaffolding factory that detects the table can both reach it without the EF layer depending on a + /// particular ADO.NET driver. + /// + public static class JetDualTable + { + // The SQL statement + // + // (SELECT COUNT(*) FROM MSysRelationships) + // + // is a DUAL table simulation in Access databases + // It must be a single line table. + // If user cannot gain access to MSysRelationships table he can create a table with 1 record + // and change DUAL static property. + // I.e. create table dual with one and only one record + // + // CREATE TABLE Dual (id COUNTER CONSTRAINT pkey PRIMARY KEY) + // INSERT INTO Dual (id) VALUES (1) + // ALTER TABLE Dual ADD CONSTRAINT DualTableConstraint CHECK ((SELECT Count(*) FROM Dual) = 1) + // + // then change the DUAL property + // + // JetDualTable.CustomName = "Dual"; + // + // For more information see also https://en.wikipedia.org/wiki/DUAL_table + /// + /// The DUAL table or query + /// + public static string CustomName = ""; + //MSysRelationships + //MSysAccessStorage + //#Dual + //(SELECT COUNT(*) FROM MSysAccessStorage) + + public static string DetectedName = "#Dual"; + + /// + /// The name to generate into SQL: the user's when one has been set, + /// otherwise the the scaffolding factory last found. + /// + public static string Name + => string.IsNullOrEmpty(CustomName) + ? DetectedName + : CustomName; + } +} diff --git a/src/EFCore.Jet/Internal/JetLoggerExtensions.cs b/src/EFCore.Jet.Common/Internal/JetLoggerExtensions.cs similarity index 100% rename from src/EFCore.Jet/Internal/JetLoggerExtensions.cs rename to src/EFCore.Jet.Common/Internal/JetLoggerExtensions.cs diff --git a/src/EFCore.Jet/Internal/JetModelValidator.cs b/src/EFCore.Jet.Common/Internal/JetModelValidator.cs similarity index 100% rename from src/EFCore.Jet/Internal/JetModelValidator.cs rename to src/EFCore.Jet.Common/Internal/JetModelValidator.cs diff --git a/src/EFCore.Jet/Metadata/Conventions/JetIndexConvention.cs b/src/EFCore.Jet.Common/Metadata/Conventions/JetIndexConvention.cs similarity index 100% rename from src/EFCore.Jet/Metadata/Conventions/JetIndexConvention.cs rename to src/EFCore.Jet.Common/Metadata/Conventions/JetIndexConvention.cs diff --git a/src/EFCore.Jet/Metadata/Conventions/JetOnDeleteConvention.cs b/src/EFCore.Jet.Common/Metadata/Conventions/JetOnDeleteConvention.cs similarity index 100% rename from src/EFCore.Jet/Metadata/Conventions/JetOnDeleteConvention.cs rename to src/EFCore.Jet.Common/Metadata/Conventions/JetOnDeleteConvention.cs diff --git a/src/EFCore.Jet/Metadata/Conventions/JetSharedTableConvention.cs b/src/EFCore.Jet.Common/Metadata/Conventions/JetSharedTableConvention.cs similarity index 100% rename from src/EFCore.Jet/Metadata/Conventions/JetSharedTableConvention.cs rename to src/EFCore.Jet.Common/Metadata/Conventions/JetSharedTableConvention.cs diff --git a/src/EFCore.Jet/Metadata/Conventions/JetStoreGenerationConvention.cs b/src/EFCore.Jet.Common/Metadata/Conventions/JetStoreGenerationConvention.cs similarity index 100% rename from src/EFCore.Jet/Metadata/Conventions/JetStoreGenerationConvention.cs rename to src/EFCore.Jet.Common/Metadata/Conventions/JetStoreGenerationConvention.cs diff --git a/src/EFCore.Jet/Metadata/Conventions/JetValueGenerationStrategyConvention.cs b/src/EFCore.Jet.Common/Metadata/Conventions/JetValueGenerationStrategyConvention.cs similarity index 100% rename from src/EFCore.Jet/Metadata/Conventions/JetValueGenerationStrategyConvention.cs rename to src/EFCore.Jet.Common/Metadata/Conventions/JetValueGenerationStrategyConvention.cs diff --git a/src/EFCore.Jet/Metadata/Conventions/JetValueGeneratorConvention.cs b/src/EFCore.Jet.Common/Metadata/Conventions/JetValueGeneratorConvention.cs similarity index 100% rename from src/EFCore.Jet/Metadata/Conventions/JetValueGeneratorConvention.cs rename to src/EFCore.Jet.Common/Metadata/Conventions/JetValueGeneratorConvention.cs diff --git a/src/EFCore.Jet/Metadata/Internal/JetAnnotationNames.cs b/src/EFCore.Jet.Common/Metadata/Internal/JetAnnotationNames.cs similarity index 100% rename from src/EFCore.Jet/Metadata/Internal/JetAnnotationNames.cs rename to src/EFCore.Jet.Common/Metadata/Internal/JetAnnotationNames.cs diff --git a/src/EFCore.Jet/Metadata/Internal/JetAnnotationProvider.cs b/src/EFCore.Jet.Common/Metadata/Internal/JetAnnotationProvider.cs similarity index 100% rename from src/EFCore.Jet/Metadata/Internal/JetAnnotationProvider.cs rename to src/EFCore.Jet.Common/Metadata/Internal/JetAnnotationProvider.cs diff --git a/src/EFCore.Jet/Metadata/JetValueGenerationStrategy.cs b/src/EFCore.Jet.Common/Metadata/JetValueGenerationStrategy.cs similarity index 100% rename from src/EFCore.Jet/Metadata/JetValueGenerationStrategy.cs rename to src/EFCore.Jet.Common/Metadata/JetValueGenerationStrategy.cs diff --git a/src/EFCore.Jet/Migrations/Internal/JetMigrationDatabaseLock.cs b/src/EFCore.Jet.Common/Migrations/Internal/JetMigrationDatabaseLock.cs similarity index 100% rename from src/EFCore.Jet/Migrations/Internal/JetMigrationDatabaseLock.cs rename to src/EFCore.Jet.Common/Migrations/Internal/JetMigrationDatabaseLock.cs diff --git a/src/EFCore.Jet/Migrations/Internal/JetMigrationsAnnotationProvider.cs b/src/EFCore.Jet.Common/Migrations/Internal/JetMigrationsAnnotationProvider.cs similarity index 100% rename from src/EFCore.Jet/Migrations/Internal/JetMigrationsAnnotationProvider.cs rename to src/EFCore.Jet.Common/Migrations/Internal/JetMigrationsAnnotationProvider.cs diff --git a/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs b/src/EFCore.Jet.Common/Migrations/JetMigrationsSqlGenerator.cs similarity index 98% rename from src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs rename to src/EFCore.Jet.Common/Migrations/JetMigrationsSqlGenerator.cs index be74e4139..a52a8ede8 100644 --- a/src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs +++ b/src/EFCore.Jet.Common/Migrations/JetMigrationsSqlGenerator.cs @@ -854,7 +854,7 @@ protected override string GetColumnType( var storeType = operation.ColumnType; if (IsIdentity(operation) && - (storeType == null || Dependencies.TypeMappingSource.FindMapping(storeType) is JetIntTypeMapping or ShortTypeMapping)) + (storeType == null || Dependencies.TypeMappingSource.FindMapping(storeType) is IntTypeMapping or ShortTypeMapping)) { // This column represents the actual identity. storeType = "counter"; @@ -935,9 +935,15 @@ protected override void DefaultValue( : null) ?? Dependencies.TypeMappingSource.GetMappingForValue(defaultValue); - // All time related type mappings derive from JetDateTimeTypeMapping. - defaultValue = typeMapping is JetDateTimeTypeMapping dateTimeTypeMapping - ? dateTimeTypeMapping.GenerateNonNullSqlLiteral(defaultValue, defaultClauseCompatible: true) + var defaultClauseLiteral = typeMapping.GetType().FullName == "EntityFrameworkCore.Jet.Storage.Internal.JetDateTimeTypeMapping" + ? typeMapping.GetType().GetMethod( + "GenerateNonNullSqlLiteral", + BindingFlags.Instance | BindingFlags.Public, + [typeof(object), typeof(bool)]) + : null; + + defaultValue = defaultClauseLiteral != null + ? (string)defaultClauseLiteral.Invoke(typeMapping, [defaultValue, true])! : typeMapping.GenerateSqlLiteral(defaultValue); builder diff --git a/src/EFCore.Jet/Migrations/Operations/JetCreateDatabaseOperation.cs b/src/EFCore.Jet.Common/Migrations/Operations/JetCreateDatabaseOperation.cs similarity index 100% rename from src/EFCore.Jet/Migrations/Operations/JetCreateDatabaseOperation.cs rename to src/EFCore.Jet.Common/Migrations/Operations/JetCreateDatabaseOperation.cs diff --git a/src/EFCore.Jet/Migrations/Operations/JetDropDatabaseOperation.cs b/src/EFCore.Jet.Common/Migrations/Operations/JetDropDatabaseOperation.cs similarity index 100% rename from src/EFCore.Jet/Migrations/Operations/JetDropDatabaseOperation.cs rename to src/EFCore.Jet.Common/Migrations/Operations/JetDropDatabaseOperation.cs diff --git a/src/EFCore.Jet/Properties/JetStrings.Designer.cs b/src/EFCore.Jet.Common/Properties/JetStrings.Designer.cs similarity index 100% rename from src/EFCore.Jet/Properties/JetStrings.Designer.cs rename to src/EFCore.Jet.Common/Properties/JetStrings.Designer.cs diff --git a/src/EFCore.Jet/Properties/JetStrings.Designer.tt b/src/EFCore.Jet.Common/Properties/JetStrings.Designer.tt similarity index 100% rename from src/EFCore.Jet/Properties/JetStrings.Designer.tt rename to src/EFCore.Jet.Common/Properties/JetStrings.Designer.tt diff --git a/src/EFCore.Jet/Properties/JetStrings.resx b/src/EFCore.Jet.Common/Properties/JetStrings.resx similarity index 100% rename from src/EFCore.Jet/Properties/JetStrings.resx rename to src/EFCore.Jet.Common/Properties/JetStrings.resx diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetAggregateMethodCallTranslatorProvider.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetAggregateMethodCallTranslatorProvider.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetAggregateMethodCallTranslatorProvider.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetAggregateMethodCallTranslatorProvider.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetByteArrayMethodTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetByteArrayMethodTranslator.cs similarity index 53% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetByteArrayMethodTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetByteArrayMethodTranslator.cs index 35ce3589d..aa43e466c 100644 --- a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetByteArrayMethodTranslator.cs +++ b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetByteArrayMethodTranslator.cs @@ -69,57 +69,76 @@ public class JetByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFac ? _sqlExpressionFactory.Convert(dataLengthSqlFunction, typeof(int)) : dataLengthSqlFunction; } - if (method is { IsGenericMethod: true, Name: nameof(Enumerable.Contains) } - && arguments[0].Type == typeof(byte[])) + + if (method.IsGenericMethod + && method.DeclaringType == typeof(Enumerable)) { - var source = arguments[0]; - var sourceTypeMapping = source.TypeMapping; - - var value = arguments[1] is SqlConstantExpression constantValue - ? _sqlExpressionFactory.Constant(new[] { (byte)constantValue.Value! }, sourceTypeMapping) - : _sqlExpressionFactory.Function( - "CHR", - [arguments[1]], - nullable: true, - argumentsPropagateNullability: [true], - typeof(string)); - - - - return _sqlExpressionFactory.GreaterThan( - _sqlExpressionFactory.Function( - "INSTR", - [ - _sqlExpressionFactory.Constant(1), + switch (method.Name) + { + case nameof(Enumerable.Contains) when arguments is [var source, var item] && source.Type == typeof(byte[]): + { + var sourceTypeMapping = source.TypeMapping; + + var value = item is SqlConstantExpression constantValue + ? _sqlExpressionFactory.Constant(new[] { (byte)constantValue.Value! }, sourceTypeMapping) + : _sqlExpressionFactory.Function( + "CHR", + [item], + nullable: true, + argumentsPropagateNullability: [true], + typeof(string)); + + return _sqlExpressionFactory.GreaterThan( _sqlExpressionFactory.Function( - "STRCONV", - [source, _sqlExpressionFactory.Constant(64)], + "INSTR", + [ + _sqlExpressionFactory.Constant(1), + _sqlExpressionFactory.Function( + "STRCONV", + [source, _sqlExpressionFactory.Constant(64)], + nullable: true, + argumentsPropagateNullability: [true, false], + typeof(string)), + value, + _sqlExpressionFactory.Constant(0) + ], nullable: true, - argumentsPropagateNullability: [true, false], - typeof(string)), - value, - _sqlExpressionFactory.Constant(0) - ], - nullable: true, - argumentsPropagateNullability: [false, true, true, false], - typeof(int)), - _sqlExpressionFactory.Constant(0)); - } + argumentsPropagateNullability: [false, true, true, false], + typeof(int)), + _sqlExpressionFactory.Constant(0)); + } - if (method is { IsGenericMethod: true, Name: nameof(Enumerable.First) } && method.GetParameters().Length == 1 - && arguments[0].Type == typeof(byte[])) - { - return _sqlExpressionFactory.Function( - "ASCB", - [ _sqlExpressionFactory.Function( - "MIDB", - [arguments[0], _sqlExpressionFactory.Constant(1), _sqlExpressionFactory.Constant(1)], - nullable: true, - argumentsPropagateNullability: [true, true, true], - typeof(byte[])) ], - nullable: true, - argumentsPropagateNullability: [true], - typeof(int)); + // First without a predicate + case nameof(Enumerable.First) when arguments is [var source] && source.Type == typeof(byte[]): + return _sqlExpressionFactory.Function( + "ASCB", + [ + _sqlExpressionFactory.Function( + "MIDB", + [source, _sqlExpressionFactory.Constant(1), _sqlExpressionFactory.Constant(1)], + nullable: true, + argumentsPropagateNullability: [true, true, true], + typeof(byte[])) + ], + nullable: true, + argumentsPropagateNullability: [true], + typeof(int)); + + // Any without a predicate. LENB answers "are there any bytes at all" exactly, and unlike + // ByteArrayLength it needs no caveat: LENB reports the UTF-16 byte count and so rounds an odd + // length UP to even, which is why an EXACT length is unobtainable — but that rounding can + // never move a value across zero. An empty array is 0 and every non-empty array is at least 2, + // so the trailing-0x00 ambiguity behind ByteArrayLength's warning cannot arise for a > 0 test. + case nameof(Enumerable.Any) when arguments is [var source] && source.Type == typeof(byte[]): + return _sqlExpressionFactory.GreaterThan( + _sqlExpressionFactory.Function( + "LENB", + [source], + nullable: true, + argumentsPropagateNullability: [true], + typeof(int)), + _sqlExpressionFactory.Constant(0)); + } } return null; diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetConvertTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetConvertTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetConvertTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetConvertTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateDiffFunctionsTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateDiffFunctionsTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateDiffFunctionsTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateDiffFunctionsTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateOnlyMemberTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateOnlyMemberTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateOnlyMemberTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateOnlyMemberTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateOnlyMethodTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateOnlyMethodTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateOnlyMethodTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateOnlyMethodTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateTimeMemberTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateTimeMemberTranslator.cs similarity index 98% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateTimeMemberTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateTimeMemberTranslator.cs index cad85fe28..0eb7a2a55 100644 --- a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateTimeMemberTranslator.cs +++ b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateTimeMemberTranslator.cs @@ -24,7 +24,7 @@ private static readonly Dictionary DatePartMapping { nameof(DateTime.Minute), "n" }, { nameof(DateTime.Second), "s" }, { nameof(DateTime.DayOfWeek), "w" }, - //{ nameof(DateTime.Millisecond), "millisecond" } + { nameof(DateTime.Millisecond), "ms" } }; private readonly JetSqlExpressionFactory _sqlExpressionFactory = (JetSqlExpressionFactory)sqlExpressionFactory; diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateTimeMethodTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateTimeMethodTranslator.cs similarity index 95% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateTimeMethodTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateTimeMethodTranslator.cs index 77f05b76b..4ae71188a 100644 --- a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetDateTimeMethodTranslator.cs +++ b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetDateTimeMethodTranslator.cs @@ -20,20 +20,20 @@ public class JetDateTimeMethodTranslator(ISqlExpressionFactory sqlExpressionFact {typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddHours), [typeof(double)])!, "h"}, {typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMinutes), [typeof(double)])!, "n"}, {typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddSeconds), [typeof(double)])!, "s"}, - //{typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMilliseconds), [typeof(double)])!, "millisecond"}, + {typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMilliseconds), [typeof(double)])!, "ms"}, {typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddYears), [typeof(int)])!, "yyyy"}, {typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMonths), [typeof(int)])!, "m"}, {typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddDays), [typeof(double)])!, "d"}, {typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddHours), [typeof(double)])!, "h"}, {typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMinutes), [typeof(double)])!, "n"}, {typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddSeconds), [typeof(double)])!, "s"}, - //{typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), [typeof(double)])!, "millisecond"} + {typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), [typeof(double)])!, "ms"} }; private static readonly Dictionary _methodInfoDateDiffMapping = new() { { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeSeconds), Type.EmptyTypes)!, "s" }, - { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeMilliseconds), Type.EmptyTypes)!, "millisecond" } + { typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeMilliseconds), Type.EmptyTypes)!, "ms" } }; public SqlExpression? Translate(SqlExpression? instance, MethodInfo method, IReadOnlyList arguments, IDiagnosticsLogger logger) diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetIsDateFunctionTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetIsDateFunctionTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetIsDateFunctionTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetIsDateFunctionTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetMathTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetMathTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetMathTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetMathTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetMemberTranslatorProvider.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetMemberTranslatorProvider.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetMemberTranslatorProvider.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetMemberTranslatorProvider.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetMethodCallTranslatorProvider.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetMethodCallTranslatorProvider.cs similarity index 96% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetMethodCallTranslatorProvider.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetMethodCallTranslatorProvider.cs index 689996a3a..355601f27 100644 --- a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetMethodCallTranslatorProvider.cs +++ b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetMethodCallTranslatorProvider.cs @@ -30,6 +30,7 @@ public JetMethodCallTranslatorProvider( new JetMathTranslator(sqlExpressionFactory), new JetNewGuidTranslator(sqlExpressionFactory), new JetObjectToStringTranslator(sqlExpressionFactory), + new JetParseTranslator(sqlExpressionFactory), new JetStringMethodTranslator(sqlExpressionFactory), new JetRandomTranslator(sqlExpressionFactory), new JetTimeOnlyMethodTranslator(sqlExpressionFactory) diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetNewGuidTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetNewGuidTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetNewGuidTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetNewGuidTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetObjectToStringTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetObjectToStringTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetObjectToStringTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetObjectToStringTranslator.cs diff --git a/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetParseTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetParseTranslator.cs new file mode 100644 index 000000000..41d0ae43c --- /dev/null +++ b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetParseTranslator.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace EntityFrameworkCore.Jet.Query.ExpressionTranslators.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class JetParseTranslator(ISqlExpressionFactory sqlExpressionFactory) : IMethodCallTranslator +{ + private static readonly Type[] SupportedClrTypes = + [ + typeof(bool), // bit + typeof(byte), // tinyint + typeof(decimal), // decimal + typeof(double), // float + typeof(float), // float + typeof(short), // smallint + typeof(int), // int + typeof(long) // bigint + ]; + + private static readonly MethodInfo[] SupportedMethods + = SupportedClrTypes + .SelectMany(t => t.GetTypeInfo().GetDeclaredMethods(nameof(int.Parse)) + .Where(m => m.GetParameters().Length == 1 + && m.GetParameters().First().ParameterType == typeof(string))) + .ToArray(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + => SupportedMethods.Contains(method) + ? sqlExpressionFactory.Convert( + arguments[0], + method.ReturnType) + : null; +} diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetRandomTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetRandomTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetRandomTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetRandomTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetStatisticsAggregateMethodTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetStatisticsAggregateMethodTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetStatisticsAggregateMethodTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetStatisticsAggregateMethodTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetStringMemberTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetStringMemberTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetStringMemberTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetStringMemberTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetStringMethodTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetStringMethodTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetStringMethodTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetStringMethodTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetTimeOnlyMemberTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetTimeOnlyMemberTranslator.cs similarity index 97% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetTimeOnlyMemberTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetTimeOnlyMemberTranslator.cs index 7aa415ff4..e10c49642 100644 --- a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetTimeOnlyMemberTranslator.cs +++ b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetTimeOnlyMemberTranslator.cs @@ -24,7 +24,7 @@ public class JetTimeOnlyMemberTranslator(ISqlExpressionFactory sqlExpressionFact { nameof(TimeOnly.Hour), "h" }, { nameof(TimeOnly.Minute), "n" }, { nameof(TimeOnly.Second), "s" }, - { nameof(TimeOnly.Millisecond), "millisecond" } + { nameof(TimeOnly.Millisecond), "ms" } }; private readonly ISqlExpressionFactory _sqlExpressionFactory = sqlExpressionFactory; diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetTimeOnlyMethodTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetTimeOnlyMethodTranslator.cs similarity index 100% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetTimeOnlyMethodTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetTimeOnlyMethodTranslator.cs diff --git a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetTimeSpanMemberTranslator.cs b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetTimeSpanMemberTranslator.cs similarity index 97% rename from src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetTimeSpanMemberTranslator.cs rename to src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetTimeSpanMemberTranslator.cs index 89c4e398d..d8d5432a5 100644 --- a/src/EFCore.Jet/Query/ExpressionTranslators/Internal/JetTimeSpanMemberTranslator.cs +++ b/src/EFCore.Jet.Common/Query/ExpressionTranslators/Internal/JetTimeSpanMemberTranslator.cs @@ -24,7 +24,7 @@ public class JetTimeSpanMemberTranslator(ISqlExpressionFactory sqlExpressionFact { nameof(TimeSpan.Hours), "h" }, { nameof(TimeSpan.Minutes), "n" }, { nameof(TimeSpan.Seconds), "s" }, - { nameof(TimeSpan.Milliseconds), "millisecond" } + { nameof(TimeSpan.Milliseconds), "ms" } }; private readonly ISqlExpressionFactory _sqlExpressionFactory = sqlExpressionFactory; diff --git a/src/EFCore.Jet/Query/Internal/JetCompatibilityExpressionVisitor.cs b/src/EFCore.Jet.Common/Query/Internal/JetCompatibilityExpressionVisitor.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetCompatibilityExpressionVisitor.cs rename to src/EFCore.Jet.Common/Query/Internal/JetCompatibilityExpressionVisitor.cs diff --git a/src/EFCore.Jet/Query/Internal/JetCompiledQueryCacheKeyGenerator.cs b/src/EFCore.Jet.Common/Query/Internal/JetCompiledQueryCacheKeyGenerator.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetCompiledQueryCacheKeyGenerator.cs rename to src/EFCore.Jet.Common/Query/Internal/JetCompiledQueryCacheKeyGenerator.cs diff --git a/src/EFCore.Jet/Query/Internal/JetDateTimeExpressionVisitor.cs b/src/EFCore.Jet.Common/Query/Internal/JetDateTimeExpressionVisitor.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetDateTimeExpressionVisitor.cs rename to src/EFCore.Jet.Common/Query/Internal/JetDateTimeExpressionVisitor.cs diff --git a/src/EFCore.Jet/Query/Internal/JetDateTimeRangeConverter.cs b/src/EFCore.Jet.Common/Query/Internal/JetDateTimeRangeConverter.cs similarity index 97% rename from src/EFCore.Jet/Query/Internal/JetDateTimeRangeConverter.cs rename to src/EFCore.Jet.Common/Query/Internal/JetDateTimeRangeConverter.cs index 431f396aa..5d0b667f7 100644 --- a/src/EFCore.Jet/Query/Internal/JetDateTimeRangeConverter.cs +++ b/src/EFCore.Jet.Common/Query/Internal/JetDateTimeRangeConverter.cs @@ -21,7 +21,7 @@ namespace EntityFrameworkCore.Jet.Query.Internal; /// alone: d == default must keep matching the epoch a default was stored as. /// /// -/// This has to run in rather than in the compile-time +/// This has to run in JetParameterBasedSqlProcessor rather than in the compile-time /// JetDateTimeExpressionVisitor, because the value normally arrives as a parameter and query /// compilation is cached across invocations, so no value is available there. Nor can it be deferred to /// a value-independent clamp in the emitted SQL: by then the DateTime type mapping has already replaced diff --git a/src/EFCore.Jet/Query/Internal/JetLiftOrderByPostprocessor.cs b/src/EFCore.Jet.Common/Query/Internal/JetLiftOrderByPostprocessor.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetLiftOrderByPostprocessor.cs rename to src/EFCore.Jet.Common/Query/Internal/JetLiftOrderByPostprocessor.cs diff --git a/src/EFCore.Jet/Query/Internal/JetLocateScalarSubqueryVisitor.cs b/src/EFCore.Jet.Common/Query/Internal/JetLocateScalarSubqueryVisitor.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetLocateScalarSubqueryVisitor.cs rename to src/EFCore.Jet.Common/Query/Internal/JetLocateScalarSubqueryVisitor.cs diff --git a/src/EFCore.Jet/Query/Internal/JetOuterJoinProjectionGuardExpressionVisitor.cs b/src/EFCore.Jet.Common/Query/Internal/JetOuterJoinProjectionGuardExpressionVisitor.cs similarity index 98% rename from src/EFCore.Jet/Query/Internal/JetOuterJoinProjectionGuardExpressionVisitor.cs rename to src/EFCore.Jet.Common/Query/Internal/JetOuterJoinProjectionGuardExpressionVisitor.cs index 68e2d3f43..16208d5eb 100644 --- a/src/EFCore.Jet/Query/Internal/JetOuterJoinProjectionGuardExpressionVisitor.cs +++ b/src/EFCore.Jet.Common/Query/Internal/JetOuterJoinProjectionGuardExpressionVisitor.cs @@ -48,7 +48,7 @@ namespace EntityFrameworkCore.Jet.Query.Internal /// guard's test, anchor IS NULL, is provably false — the anchor is non-nullable within the /// subquery — so SqlNullabilityProcessor folds the whole CASE straight back to its /// ELSE branch, restoring the bare literal. Applied in - /// after base.Process, it runs past that + /// JetParameterBasedSqlProcessor after base.Process, it runs past that /// optimization and survives to SQL generation. Moving it earlier silently reverts it: the tree looks /// correct on the way out and the generator still receives the original literal. /// diff --git a/src/EFCore.Jet/Query/Internal/JetQueryCompilationContext.cs b/src/EFCore.Jet.Common/Query/Internal/JetQueryCompilationContext.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetQueryCompilationContext.cs rename to src/EFCore.Jet.Common/Query/Internal/JetQueryCompilationContext.cs diff --git a/src/EFCore.Jet/Query/Internal/JetQueryCompilationContextFactory.cs b/src/EFCore.Jet.Common/Query/Internal/JetQueryCompilationContextFactory.cs similarity index 94% rename from src/EFCore.Jet/Query/Internal/JetQueryCompilationContextFactory.cs rename to src/EFCore.Jet.Common/Query/Internal/JetQueryCompilationContextFactory.cs index 354feb09b..98f9fcb99 100644 --- a/src/EFCore.Jet/Query/Internal/JetQueryCompilationContextFactory.cs +++ b/src/EFCore.Jet.Common/Query/Internal/JetQueryCompilationContextFactory.cs @@ -16,11 +16,8 @@ namespace EntityFrameworkCore.Jet.Query.Internal; /// public class JetQueryCompilationContextFactory( QueryCompilationContextDependencies dependencies, - RelationalQueryCompilationContextDependencies relationalDependencies, - IJetRelationalConnection jetConnection) : IQueryCompilationContextFactory + RelationalQueryCompilationContextDependencies relationalDependencies) : IQueryCompilationContextFactory { - private readonly IJetRelationalConnection _jetConnection = jetConnection; - /// /// Dependencies for this service. /// diff --git a/src/EFCore.Jet/Query/Internal/JetQueryTranslationPreprocessor.cs b/src/EFCore.Jet.Common/Query/Internal/JetQueryTranslationPreprocessor.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetQueryTranslationPreprocessor.cs rename to src/EFCore.Jet.Common/Query/Internal/JetQueryTranslationPreprocessor.cs diff --git a/src/EFCore.Jet/Query/Internal/JetQueryTranslationPreprocessorFactory.cs b/src/EFCore.Jet.Common/Query/Internal/JetQueryTranslationPreprocessorFactory.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetQueryTranslationPreprocessorFactory.cs rename to src/EFCore.Jet.Common/Query/Internal/JetQueryTranslationPreprocessorFactory.cs diff --git a/src/EFCore.Jet/Query/Internal/JetQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Jet.Common/Query/Internal/JetQueryableMethodTranslatingExpressionVisitor.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetQueryableMethodTranslatingExpressionVisitor.cs rename to src/EFCore.Jet.Common/Query/Internal/JetQueryableMethodTranslatingExpressionVisitor.cs diff --git a/src/EFCore.Jet/Query/Internal/JetQueryableMethodTranslatingExpressionVisitorFactory.cs b/src/EFCore.Jet.Common/Query/Internal/JetQueryableMethodTranslatingExpressionVisitorFactory.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetQueryableMethodTranslatingExpressionVisitorFactory.cs rename to src/EFCore.Jet.Common/Query/Internal/JetQueryableMethodTranslatingExpressionVisitorFactory.cs diff --git a/src/EFCore.Jet/Query/Internal/JetSkipTakePostprocessor.cs b/src/EFCore.Jet.Common/Query/Internal/JetSkipTakePostprocessor.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetSkipTakePostprocessor.cs rename to src/EFCore.Jet.Common/Query/Internal/JetSkipTakePostprocessor.cs diff --git a/src/EFCore.Jet/Query/Internal/JetSqlNullabilityProcessor.cs b/src/EFCore.Jet.Common/Query/Internal/JetSqlNullabilityProcessor.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetSqlNullabilityProcessor.cs rename to src/EFCore.Jet.Common/Query/Internal/JetSqlNullabilityProcessor.cs diff --git a/src/EFCore.Jet/Query/Internal/JetSqlTranslatingExpressionVisitor.cs b/src/EFCore.Jet.Common/Query/Internal/JetSqlTranslatingExpressionVisitor.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetSqlTranslatingExpressionVisitor.cs rename to src/EFCore.Jet.Common/Query/Internal/JetSqlTranslatingExpressionVisitor.cs diff --git a/src/EFCore.Jet/Query/Internal/JetSqlTranslatingExpressionVisitorFactory.cs b/src/EFCore.Jet.Common/Query/Internal/JetSqlTranslatingExpressionVisitorFactory.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/JetSqlTranslatingExpressionVisitorFactory.cs rename to src/EFCore.Jet.Common/Query/Internal/JetSqlTranslatingExpressionVisitorFactory.cs diff --git a/src/EFCore.Jet/Query/Internal/JetZeroLimitConverter.cs b/src/EFCore.Jet.Common/Query/Internal/JetZeroLimitConverter.cs similarity index 96% rename from src/EFCore.Jet/Query/Internal/JetZeroLimitConverter.cs rename to src/EFCore.Jet.Common/Query/Internal/JetZeroLimitConverter.cs index 352ba1b2e..6b1f61fe4 100644 --- a/src/EFCore.Jet/Query/Internal/JetZeroLimitConverter.cs +++ b/src/EFCore.Jet.Common/Query/Internal/JetZeroLimitConverter.cs @@ -60,9 +60,9 @@ protected override Expression VisitExtension(Expression extensionExpression) { var result = selectExpression.Update( selectExpression.Tables, - selectExpression.GroupBy.Count > 0 ? selectExpression.Predicate : _sqlExpressionFactory.Constant(false, JetBoolTypeMapping.Default), + selectExpression.GroupBy.Count > 0 ? selectExpression.Predicate : _sqlExpressionFactory.ApplyDefaultTypeMapping(_sqlExpressionFactory.Constant(false)), selectExpression.GroupBy, - selectExpression.GroupBy.Count > 0 ? _sqlExpressionFactory.Constant(false, JetBoolTypeMapping.Default) : null, + selectExpression.GroupBy.Count > 0 ? _sqlExpressionFactory.ApplyDefaultTypeMapping(_sqlExpressionFactory.Constant(false)) : null, selectExpression.Projection, [], limit: null, diff --git a/src/EFCore.Jet/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.Jet.Common/Query/Internal/SearchConditionConvertingExpressionVisitor.cs similarity index 100% rename from src/EFCore.Jet/Query/Internal/SearchConditionConvertingExpressionVisitor.cs rename to src/EFCore.Jet.Common/Query/Internal/SearchConditionConvertingExpressionVisitor.cs diff --git a/src/EFCore.Jet/Query/JetSqlExpressionFactory.cs b/src/EFCore.Jet.Common/Query/JetSqlExpressionFactory.cs similarity index 100% rename from src/EFCore.Jet/Query/JetSqlExpressionFactory.cs rename to src/EFCore.Jet.Common/Query/JetSqlExpressionFactory.cs diff --git a/src/EFCore.Jet/Query/Sql/Internal/IJetExpressionVisitor.cs b/src/EFCore.Jet.Common/Query/Sql/Internal/IJetExpressionVisitor.cs similarity index 100% rename from src/EFCore.Jet/Query/Sql/Internal/IJetExpressionVisitor.cs rename to src/EFCore.Jet.Common/Query/Sql/Internal/IJetExpressionVisitor.cs diff --git a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs b/src/EFCore.Jet.Common/Query/Sql/Internal/JetQuerySqlGenerator.cs similarity index 95% rename from src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs rename to src/EFCore.Jet.Common/Query/Sql/Internal/JetQuerySqlGenerator.cs index 4fb5f0de3..b12e2ed2d 100644 --- a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGenerator.cs +++ b/src/EFCore.Jet.Common/Query/Sql/Internal/JetQuerySqlGenerator.cs @@ -1,8 +1,4 @@ -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using EntityFrameworkCore.Jet.Data; -using EntityFrameworkCore.Jet.Infrastructure.Internal; -using EntityFrameworkCore.Jet.Storage.Internal; +using EntityFrameworkCore.Jet.Infrastructure; using EntityFrameworkCore.Jet.Utilities; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage.Internal; @@ -39,10 +35,7 @@ public class JetQuerySqlGenerator : QuerySqlGenerator, IJetExpressionVisitor }; private readonly ITypeMappingSource _typeMappingSource; - private readonly IJetOptions _options; - private readonly ISqlGenerationHelper _sqlGenerationHelper; - //private readonly JetSqlExpressionFactory _sqlExpressionFactory; private List _nullNumerics = []; private Stack parent = new(); private CoreTypeMapping? _boolTypeMapping; @@ -52,12 +45,10 @@ public class JetQuerySqlGenerator : QuerySqlGenerator, IJetExpressionVisitor /// public JetQuerySqlGenerator( QuerySqlGeneratorDependencies dependencies, - ITypeMappingSource typeMappingSource, - IJetOptions options) + ITypeMappingSource typeMappingSource) : base(dependencies) { _typeMappingSource = typeMappingSource; - _options = options; _sqlGenerationHelper = dependencies.SqlGenerationHelper; _boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool)); } @@ -534,7 +525,7 @@ private bool IsNonComposedSetOperation(SelectExpression selectExpression) protected override void GeneratePseudoFromClause() { Sql.AppendLine() - .Append("FROM " + "(SELECT COUNT(*) FROM `" + (string.IsNullOrEmpty(JetConfiguration.CustomDualTableName) ? JetConfiguration.DetectedDualTableName : JetConfiguration.CustomDualTableName) + "`)"); + .Append("FROM " + "(SELECT COUNT(*) FROM `" + JetDualTable.Name + "`)"); } private void GenerateList( @@ -560,7 +551,8 @@ protected override Expression VisitOrdering(OrderingExpression orderingExpressio // Jet uses the value -1 as True, so ordering by a boolean expression will first list the True values // before the False values, which is the opposite of what .NET and other DBMS do, which are using 1 as True. - if (orderingExpression.Expression.TypeMapping?.GetType() == typeof(JetBoolTypeMapping)) + if (orderingExpression.Expression.TypeMapping is BoolTypeMapping + && orderingExpression.Expression.TypeMapping.GetType() == _boolTypeMapping?.GetType()) { orderingExpression = new OrderingExpression( new SqlUnaryExpression( @@ -590,14 +582,18 @@ protected override Expression VisitOrdering(OrderingExpression orderingExpressio protected override Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression) { - if (sqlParameterExpression.Type == typeof(DateTime) && sqlParameterExpression.TypeMapping is JetDateTimeTypeMapping or NullTypeMapping) + // Matched on the EF Core base mapping, not on JetDateTimeTypeMapping/JetTimeOnlyTypeMapping: a derived + // provider (LibRed) substitutes its own mapping off the same base, and it needs the same coercion. The + // CLR-type test in front is what actually narrows this - the mapping test only rules out a DateTime + // that some other mapping claimed. + if (sqlParameterExpression.Type == typeof(DateTime) && sqlParameterExpression.TypeMapping is DateTimeTypeMapping or NullTypeMapping) { Sql.Append("CDATE("); base.VisitSqlParameter(sqlParameterExpression); Sql.Append(")"); return sqlParameterExpression; } - if (sqlParameterExpression.Type == typeof(TimeOnly) && sqlParameterExpression.TypeMapping is JetTimeOnlyTypeMapping) + if (sqlParameterExpression.Type == typeof(TimeOnly) && sqlParameterExpression.TypeMapping is TimeOnlyTypeMapping) { Sql.Append("TIMEVALUE("); base.VisitSqlParameter(sqlParameterExpression); @@ -776,7 +772,8 @@ protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstant { if (sqlConstantExpression.TypeMapping == RelationalTypeMapping.NullMapping && sqlConstantExpression.Value is DateTime) { - sqlConstantExpression = (SqlConstantExpression)sqlConstantExpression.ApplyTypeMapping(new JetDateTimeTypeMapping("datetime")); + sqlConstantExpression = (SqlConstantExpression)sqlConstantExpression.ApplyTypeMapping( + (RelationalTypeMapping?)_typeMappingSource.FindMapping(typeof(DateTime))); } parent.TryPeek(out var exp); @@ -788,7 +785,8 @@ protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstant return sqlConstantExpression; } - if (sqlConstantExpression.TypeMapping is BoolTypeMapping and not JetBoolTypeMapping) + if (sqlConstantExpression.TypeMapping is BoolTypeMapping + && sqlConstantExpression.TypeMapping.GetType() != _boolTypeMapping?.GetType()) { Sql.Append((bool)sqlConstantExpression.Value! ? "TRUE" : "FALSE"); return sqlConstantExpression; @@ -1073,17 +1071,8 @@ protected override void GenerateTop(SelectExpression selectExpression) if (selectExpression.Offset != null) { - if (_options.UseOuterSelectSkipEmulationViaDataReader) - { - Sql.Append("SKIP "); - Visit(selectExpression.Offset); - Sql.Append(" "); - } - else - { - throw new InvalidOperationException( - "Jet does not support skipping rows. Switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync() if needed."); - } + throw new InvalidOperationException( + "Jet does not support skipping rows. Switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync() if needed."); } if (selectExpression.Limit != null) @@ -1117,6 +1106,30 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction return sqlFunctionExpression; } + // Jet/ACE has no NULLIF - "Undefined function 'NULLIF' in expression" (verified against ACE). EF + // produces it in SqlExpressionFactory.Case, which collapses `a == b ? null : a` into + // Function("NULLIF", [a, b]), so there is no translator to override upstream: by the time the tree + // reaches a provider it is already a function call. + // + // The SQL standard defines NULLIF(a, b) as shorthand for CASE WHEN a = b THEN NULL ELSE a END, so + // rebuild exactly that and let VisitCase render it - which for Jet means IIF(a = b, NULL, a). + // Emitting the CASE rather than the text reuses the existing operator and NULL-literal rendering, + // and mirrors how binary Coalesce is handled above. + // + // 'a' is written twice, which the standard's own definition also implies; Jet has no way to say it + // once. + if (sqlFunctionExpression.Name.Equals("NULLIF", StringComparison.OrdinalIgnoreCase) + && sqlFunctionExpression.Arguments is [var nullIfLeft, var nullIfRight]) + { + var equal = new SqlBinaryExpression( + ExpressionType.Equal, nullIfLeft, nullIfRight, typeof(bool), null); + var nullResult = new SqlConstantExpression( + null, sqlFunctionExpression.Type, sqlFunctionExpression.TypeMapping); + + Visit(new CaseExpression([new CaseWhenClause(equal, nullResult)], nullIfLeft)); + return sqlFunctionExpression; + } + // The guard has to be applied here rather than in the query tree: EF removes a CASE that merely // replicates SQL's native null propagation (dotnet/efcore#34127), which is what this looks like to // every dialect where these functions do propagate. IIF short-circuits, so the call is not evaluated. diff --git a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGeneratorFactory.cs b/src/EFCore.Jet.Common/Query/Sql/Internal/JetQuerySqlGeneratorFactory.cs similarity index 72% rename from src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGeneratorFactory.cs rename to src/EFCore.Jet.Common/Query/Sql/Internal/JetQuerySqlGeneratorFactory.cs index 1edd17057..5883225c7 100644 --- a/src/EFCore.Jet/Query/Sql/Internal/JetQuerySqlGeneratorFactory.cs +++ b/src/EFCore.Jet.Common/Query/Sql/Internal/JetQuerySqlGeneratorFactory.cs @@ -1,7 +1,3 @@ -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using EntityFrameworkCore.Jet.Infrastructure.Internal; - namespace EntityFrameworkCore.Jet.Query.Sql.Internal { /// @@ -14,14 +10,12 @@ namespace EntityFrameworkCore.Jet.Query.Sql.Internal /// public class JetQuerySqlGeneratorFactory( QuerySqlGeneratorDependencies dependencies, - ITypeMappingSource typeMappingSource, - IJetOptions options) : IQuerySqlGeneratorFactory + ITypeMappingSource typeMappingSource) : IQuerySqlGeneratorFactory { private readonly QuerySqlGeneratorDependencies _dependencies = dependencies; private readonly ITypeMappingSource _typeMappingSource = typeMappingSource; - private readonly IJetOptions _options = options; public virtual QuerySqlGenerator Create() - => new JetQuerySqlGenerator(_dependencies, _typeMappingSource, _options); + => new JetQuerySqlGenerator(_dependencies, _typeMappingSource); } } diff --git a/src/EFCore.Jet/Storage/Internal/JetSqlGenerationHelper.cs b/src/EFCore.Jet.Common/Storage/Internal/JetSqlGenerationHelper.cs similarity index 100% rename from src/EFCore.Jet/Storage/Internal/JetSqlGenerationHelper.cs rename to src/EFCore.Jet.Common/Storage/Internal/JetSqlGenerationHelper.cs diff --git a/src/EFCore.Jet/Update/Internal/IJetUpdateSqlGenerator.cs b/src/EFCore.Jet.Common/Update/Internal/IJetUpdateSqlGenerator.cs similarity index 100% rename from src/EFCore.Jet/Update/Internal/IJetUpdateSqlGenerator.cs rename to src/EFCore.Jet.Common/Update/Internal/IJetUpdateSqlGenerator.cs diff --git a/src/EFCore.Jet/Update/Internal/JetModificationCommandBatch.cs b/src/EFCore.Jet.Common/Update/Internal/JetModificationCommandBatch.cs similarity index 100% rename from src/EFCore.Jet/Update/Internal/JetModificationCommandBatch.cs rename to src/EFCore.Jet.Common/Update/Internal/JetModificationCommandBatch.cs diff --git a/src/EFCore.Jet/Update/Internal/JetModificationCommandBatchFactory.cs b/src/EFCore.Jet.Common/Update/Internal/JetModificationCommandBatchFactory.cs similarity index 100% rename from src/EFCore.Jet/Update/Internal/JetModificationCommandBatchFactory.cs rename to src/EFCore.Jet.Common/Update/Internal/JetModificationCommandBatchFactory.cs diff --git a/src/EFCore.Jet/Update/Internal/JetUpdateSqlGenerator.cs b/src/EFCore.Jet.Common/Update/Internal/JetUpdateSqlGenerator.cs similarity index 100% rename from src/EFCore.Jet/Update/Internal/JetUpdateSqlGenerator.cs rename to src/EFCore.Jet.Common/Update/Internal/JetUpdateSqlGenerator.cs diff --git a/src/EFCore.Jet/ValueGeneration/Internal/JetValueGeneratorSelector.cs b/src/EFCore.Jet.Common/ValueGeneration/Internal/JetValueGeneratorSelector.cs similarity index 100% rename from src/EFCore.Jet/ValueGeneration/Internal/JetValueGeneratorSelector.cs rename to src/EFCore.Jet.Common/ValueGeneration/Internal/JetValueGeneratorSelector.cs diff --git a/src/EFCore.Jet/ValueGeneration/JetSequentialGuidValueGenerator.cs b/src/EFCore.Jet.Common/ValueGeneration/JetSequentialGuidValueGenerator.cs similarity index 100% rename from src/EFCore.Jet/ValueGeneration/JetSequentialGuidValueGenerator.cs rename to src/EFCore.Jet.Common/ValueGeneration/JetSequentialGuidValueGenerator.cs diff --git a/src/EFCore.Jet.Data/EFCore.Jet.Data.csproj b/src/EFCore.Jet.Data/EFCore.Jet.Data.csproj index a0b5a14ca..006155977 100644 --- a/src/EFCore.Jet.Data/EFCore.Jet.Data.csproj +++ b/src/EFCore.Jet.Data/EFCore.Jet.Data.csproj @@ -1,8 +1,8 @@ - + Jet/ACE data provider for .NET used by EntityFrameworkCore.Jet (Microsoft Access MDB/ACCDB files). - $(JetTargetFramework) + net11.0 AnyCPU;x86;x64 EntityFrameworkCore.Jet.Data EntityFrameworkCore.Jet.Data @@ -10,4 +10,9 @@ annotations + + + <_Parameter1>windows + + \ No newline at end of file diff --git a/src/EFCore.Jet.Data/JetConfiguration.cs b/src/EFCore.Jet.Data/JetConfiguration.cs index 4d38f3387..5c4a4cc59 100644 --- a/src/EFCore.Jet.Data/JetConfiguration.cs +++ b/src/EFCore.Jet.Data/JetConfiguration.cs @@ -35,35 +35,6 @@ public static object IntegerNullValue public static DataAccessProviderType DefaultDataAccessProviderType { get; set; } = DataAccessProviderType.Odbc; - // The SQL statement - // - // (SELECT COUNT(*) FROM MSysRelationships) - // - // is a DUAL table simulation in Access databases - // It must be a single line table. - // If user cannot gain access to MSysRelationships table he can create a table with 1 record - // and change DUAL static property. - // I.e. create table dual with one and only one record - // - // CREATE TABLE Dual (id COUNTER CONSTRAINT pkey PRIMARY KEY) - // INSERT INTO Dual (id) VALUES (1) - // ALTER TABLE Dual ADD CONSTRAINT DualTableConstraint CHECK ((SELECT Count(*) FROM Dual) = 1) - // - // then change the DUAL property - // - // JetConfiguration.DUAL = "Dual"; - // - // For more information see also https://en.wikipedia.org/wiki/DUAL_table - /// - /// The DUAL table or query - /// - public static string CustomDualTableName = ""; - //MSysRelationships - //MSysAccessStorage - //#Dual - //(SELECT COUNT(*) FROM MSysAccessStorage) - - public static string DetectedDualTableName = "#Dual"; /// /// Gets or sets a value indicating whether show SQL statements. /// diff --git a/src/EFCore.Jet.Odbc/EFCore.Jet.Odbc.csproj b/src/EFCore.Jet.Odbc/EFCore.Jet.Odbc.csproj index e61e78e6a..89c682362 100644 --- a/src/EFCore.Jet.Odbc/EFCore.Jet.Odbc.csproj +++ b/src/EFCore.Jet.Odbc/EFCore.Jet.Odbc.csproj @@ -37,4 +37,9 @@ + + + <_Parameter1>windows + + diff --git a/src/EFCore.Jet.OleDb/EFCore.Jet.OleDb.csproj b/src/EFCore.Jet.OleDb/EFCore.Jet.OleDb.csproj index fd246e33d..567947c43 100644 --- a/src/EFCore.Jet.OleDb/EFCore.Jet.OleDb.csproj +++ b/src/EFCore.Jet.OleDb/EFCore.Jet.OleDb.csproj @@ -37,4 +37,9 @@ + + + <_Parameter1>windows + + diff --git a/src/EFCore.Jet/EFCore.Jet.csproj b/src/EFCore.Jet/EFCore.Jet.csproj index 73700aa43..de9833857 100644 --- a/src/EFCore.Jet/EFCore.Jet.csproj +++ b/src/EFCore.Jet/EFCore.Jet.csproj @@ -1,8 +1,8 @@ - + Jet/ACE database provider for Entity Framework Core (Microsoft Access MDB/ACCDB files). - $(JetTargetFramework) + net11.0 EntityFrameworkCore.Jet EntityFrameworkCore.Jet AnyCPU;x86;x64 @@ -11,6 +11,12 @@ enable + + + <_Parameter1>windows + + + @@ -44,6 +50,7 @@ + diff --git a/src/EFCore.Jet/Infrastructure/Internal/IJetOptions.cs b/src/EFCore.Jet/Infrastructure/Internal/IJetOptions.cs index 9fffc25a1..ecba6e809 100644 --- a/src/EFCore.Jet/Infrastructure/Internal/IJetOptions.cs +++ b/src/EFCore.Jet/Infrastructure/Internal/IJetOptions.cs @@ -12,7 +12,6 @@ public interface IJetOptions : ISingletonOptions string? ConnectionString { get; } DataAccessProviderType DataAccessProviderType { get; } bool UseOuterSelectSkipEmulationViaDataReader { get; } - bool EnableMillisecondsSupport { get; } bool UseShortTextForSystemString { get; } } } diff --git a/src/EFCore.Jet/Infrastructure/Internal/JetOptionsExtension.cs b/src/EFCore.Jet/Infrastructure/Internal/JetOptionsExtension.cs index c963db26d..1930ee0c4 100644 --- a/src/EFCore.Jet/Infrastructure/Internal/JetOptionsExtension.cs +++ b/src/EFCore.Jet/Infrastructure/Internal/JetOptionsExtension.cs @@ -14,11 +14,8 @@ namespace EntityFrameworkCore.Jet.Infrastructure.Internal public class JetOptionsExtension : RelationalOptionsExtension { private DbContextOptionsExtensionInfo? _info; - - // private bool? _rowNumberPaging; private DbProviderFactory? _dataAccessProviderFactory; private bool _useOuterSelectSkipEmulationViaDataReader; - private bool _enableMillisecondsSupport; private bool _useShortTextForSystemString; /// @@ -45,7 +42,6 @@ protected JetOptionsExtension(JetOptionsExtension copyFrom) // _rowNumberPaging = copyFrom._rowNumberPaging; _dataAccessProviderFactory = copyFrom._dataAccessProviderFactory; _useOuterSelectSkipEmulationViaDataReader = copyFrom._useOuterSelectSkipEmulationViaDataReader; - _enableMillisecondsSupport = copyFrom._enableMillisecondsSupport; _useShortTextForSystemString = copyFrom._useShortTextForSystemString; } @@ -138,29 +134,6 @@ public virtual JetOptionsExtension WithUseOuterSelectSkipEmulationViaDataReader( return clone; } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual bool EnableMillisecondsSupport => _enableMillisecondsSupport; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual JetOptionsExtension WithEnableMillisecondsSupport(bool enabled) - { - var clone = (JetOptionsExtension)Clone(); - - clone._enableMillisecondsSupport = enabled; - - return clone; - } - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -230,10 +203,6 @@ public override string LogFragment builder.Append("UseOuterSelectSkipEmulationViaDataReader "); } - if (Extension._enableMillisecondsSupport) - { - builder.Append("EnableMillisecondsSupport "); - } if (Extension._useShortTextForSystemString) { builder.Append("UseShortTextForSystemString "); @@ -251,7 +220,6 @@ public override int GetServiceProviderHashCode() _serviceProviderHash ??= (base.GetServiceProviderHashCode() * 397) ^ (Extension._dataAccessProviderFactory?.GetHashCode() ?? 0) ^ (Extension._useOuterSelectSkipEmulationViaDataReader.GetHashCode() * 397) ^ - (Extension._enableMillisecondsSupport.GetHashCode() * 397) ^ (Extension._useShortTextForSystemString.GetHashCode() * 397)/* ^ (Extension._rowNumberPaging?.GetHashCode() ?? 0L)*/; @@ -270,8 +238,6 @@ public override void PopulateDebugInfo(IDictionary debugInfo) debugInfo["Jet:" + nameof(JetDbContextOptionsBuilder.UseOuterSelectSkipEmulationViaDataReader)] #pragma warning restore 618 = Extension._useOuterSelectSkipEmulationViaDataReader.GetHashCode().ToString(CultureInfo.InvariantCulture); - debugInfo["Jet:" + nameof(JetDbContextOptionsBuilder.EnableMillisecondsSupport)] - = Extension._enableMillisecondsSupport.GetHashCode().ToString(CultureInfo.InvariantCulture); debugInfo["Jet:" + nameof(JetDbContextOptionsBuilder.UseShortTextForSystemString)] = Extension._useShortTextForSystemString.GetHashCode().ToString(CultureInfo.InvariantCulture); } diff --git a/src/EFCore.Jet/Infrastructure/JetDbContextOptionsBuilder.cs b/src/EFCore.Jet/Infrastructure/JetDbContextOptionsBuilder.cs index 729ddd599..22150ddf9 100644 --- a/src/EFCore.Jet/Infrastructure/JetDbContextOptionsBuilder.cs +++ b/src/EFCore.Jet/Infrastructure/JetDbContextOptionsBuilder.cs @@ -37,14 +37,6 @@ public class JetDbContextOptionsBuilder(DbContextOptionsBuilder optionsBuilder) public virtual JetDbContextOptionsBuilder UseOuterSelectSkipEmulationViaDataReader(bool enabled = true) => WithOption(e => e.WithUseOuterSelectSkipEmulationViaDataReader(enabled)); - /// - /// Configures the context support milliseconds in `DateTime`, `DateTimeOffset` and `TimeSpan` values when - /// accessing Jet databases. Jet has no native support for milliseconds, therefore this feature is opt-in. - /// - public virtual JetDbContextOptionsBuilder EnableMillisecondsSupport(bool enabled = true) - => WithOption(e => e.WithEnableMillisecondsSupport(enabled)); - - /// /// Set this to enabled to map the System.String CLR type to the Jet `Short Text` data type instead of the /// Long Text data type. This will limit the maximum length of strings to 255 characters. diff --git a/src/EFCore.Jet/Internal/JetOptions.cs b/src/EFCore.Jet/Internal/JetOptions.cs index df8605438..2774a9665 100644 --- a/src/EFCore.Jet/Internal/JetOptions.cs +++ b/src/EFCore.Jet/Internal/JetOptions.cs @@ -29,7 +29,6 @@ public virtual void Initialize(IDbContextOptions options) DataAccessProviderType = GetDataAccessProviderTypeFromOptions(jetOptions); UseOuterSelectSkipEmulationViaDataReader = jetOptions.UseOuterSelectSkipEmulationViaDataReader; - EnableMillisecondsSupport = jetOptions.EnableMillisecondsSupport; ConnectionString = jetOptions.Connection?.ConnectionString ?? jetOptions.ConnectionString!; UseShortTextForSystemString = jetOptions.UseShortTextForSystemString; } @@ -73,13 +72,6 @@ public virtual void Validate(IDbContextOptions options) nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); } - if (EnableMillisecondsSupport != jetOptions.EnableMillisecondsSupport) - { - throw new InvalidOperationException( - CoreStrings.SingletonOptionChanged( - nameof(JetOptionsExtension.EnableMillisecondsSupport), - nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); - } if (UseShortTextForSystemString != jetOptions.UseShortTextForSystemString) { throw new InvalidOperationException( @@ -153,14 +145,6 @@ private static DataAccessProviderType GetDataAccessProviderTypeFromOptions(JetOp /// public virtual bool UseOuterSelectSkipEmulationViaDataReader { get; private set; } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public bool EnableMillisecondsSupport { get; private set; } - public bool UseShortTextForSystemString { get; private set; } /// diff --git a/src/EFCore.Jet/Migrations/Internal/JetHistoryRepository.cs b/src/EFCore.Jet/Migrations/Internal/JetHistoryRepository.cs index ca5eb698b..196a01ca9 100644 --- a/src/EFCore.Jet/Migrations/Internal/JetHistoryRepository.cs +++ b/src/EFCore.Jet/Migrations/Internal/JetHistoryRepository.cs @@ -1,7 +1,5 @@ -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - using System.Text; -using EntityFrameworkCore.Jet.Data; +using EntityFrameworkCore.Jet.Infrastructure; using EntityFrameworkCore.Jet.Internal; using EntityFrameworkCore.Jet.Utilities; @@ -29,7 +27,29 @@ namespace EntityFrameworkCore.Jet.Migrations.Internal /// 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); + + /// How long to keep trying for the migration lock before giving up. + /// + /// 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 + /// sp_getapplock timeout does. + /// + private static readonly TimeSpan _lockTimeout = TimeSpan.FromMinutes(1); + public override LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior.Explicit; /// @@ -146,9 +166,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 @@ -162,14 +182,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); } } @@ -182,30 +206,72 @@ public override async Task 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); } } + /// + /// 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. + /// + private static TimeSpan JitteredDelay(TimeSpan delay) + => TimeSpan.FromTicks((long)(delay.Ticks * (0.75 + (Random.Shared.NextDouble() / 2.0)))); + + /// Doubles the backoff up to and holds there. + 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}` ( @@ -239,9 +305,7 @@ private IRelationalCommand CreateLockTableCommand() private IRelationalCommand CreateInsertLockCommand(DateTimeOffset timestamp) { var timestampLiteral = Dependencies.TypeMappingSource.GetMapping(typeof(DateTimeOffset)).GenerateSqlLiteral(timestamp); - var dualTableName = string.IsNullOrEmpty(JetConfiguration.CustomDualTableName) - ? JetConfiguration.DetectedDualTableName - : JetConfiguration.CustomDualTableName; + var dualTableName = JetDualTable.Name; return Dependencies.RawSqlCommandBuilder.Build($""" INSERT INTO `{LockTableName}` (`Id`, `Timestamp`) diff --git a/src/EFCore.Jet/Query/Internal/JetQueryTranslationPostprocessor.cs b/src/EFCore.Jet/Query/Internal/JetQueryTranslationPostprocessor.cs index d8142bd6c..94306f44d 100644 --- a/src/EFCore.Jet/Query/Internal/JetQueryTranslationPostprocessor.cs +++ b/src/EFCore.Jet/Query/Internal/JetQueryTranslationPostprocessor.cs @@ -1,10 +1,5 @@ -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using EntityFrameworkCore.Jet.Infrastructure.Internal; using EntityFrameworkCore.Jet.Internal; -using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace EntityFrameworkCore.Jet.Query.Internal @@ -17,7 +12,6 @@ public class JetQueryTranslationPostprocessor : RelationalQueryTranslationPostpr ?? throw new InvalidOperationException("Could not find SelectExpression._identifier."); private readonly IRelationalTypeMappingSource _relationalTypeMappingSource; - private readonly IJetOptions _options; private readonly SkipWithoutOrderByInSplitQueryVerifier _skipWithoutOrderByInSplitQueryVerifier = new(); private readonly JetLiftOrderByPostprocessor _liftOrderByPostprocessor; private readonly JetSkipTakePostprocessor _skipTakePostprocessor; @@ -26,12 +20,10 @@ public JetQueryTranslationPostprocessor( QueryTranslationPostprocessorDependencies dependencies, RelationalQueryTranslationPostprocessorDependencies relationalDependencies, RelationalQueryCompilationContext queryCompilationContext, - IRelationalTypeMappingSource relationalTypeMappingSource, - IJetOptions options) + IRelationalTypeMappingSource relationalTypeMappingSource) : base(dependencies, relationalDependencies, queryCompilationContext) { _relationalTypeMappingSource = relationalTypeMappingSource; - _options = options; _liftOrderByPostprocessor = new JetLiftOrderByPostprocessor(relationalTypeMappingSource, relationalDependencies.SqlExpressionFactory, queryCompilationContext.SqlAliasManager); _skipTakePostprocessor = new JetSkipTakePostprocessor(relationalTypeMappingSource, relationalDependencies.SqlExpressionFactory, ((RelationalQueryCompilationContext)QueryCompilationContext).QuerySplittingBehavior); @@ -55,10 +47,6 @@ public override Expression Process(Expression query) } //query = _skipTakePostprocessor.Process(query); - if (_options.EnableMillisecondsSupport) - { - query = new JetDateTimeExpressionVisitor(RelationalDependencies.SqlExpressionFactory, _relationalTypeMappingSource).Visit(query); - } //query = _skipWithoutOrderByInSplitQueryVerifier.Visit(query); //query = _skipTakePostprocessor.Process(query); query = _liftOrderByPostprocessor.Process(query); diff --git a/src/EFCore.Jet/Query/Internal/JetQueryTranslationPostprocessorFactory.cs b/src/EFCore.Jet/Query/Internal/JetQueryTranslationPostprocessorFactory.cs index 084e462e9..6a7b5d8d6 100644 --- a/src/EFCore.Jet/Query/Internal/JetQueryTranslationPostprocessorFactory.cs +++ b/src/EFCore.Jet/Query/Internal/JetQueryTranslationPostprocessorFactory.cs @@ -1,7 +1,3 @@ -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using EntityFrameworkCore.Jet.Infrastructure.Internal; - namespace EntityFrameworkCore.Jet.Query.Internal { /// @@ -20,8 +16,7 @@ namespace EntityFrameworkCore.Jet.Query.Internal public class JetQueryTranslationPostprocessorFactory( QueryTranslationPostprocessorDependencies dependencies, RelationalQueryTranslationPostprocessorDependencies relationalDependencies, - IRelationalTypeMappingSource relationalTypeMappingSource, - IJetOptions options) + IRelationalTypeMappingSource relationalTypeMappingSource) : IQueryTranslationPostprocessorFactory { public virtual QueryTranslationPostprocessor Create(QueryCompilationContext queryCompilationContext) @@ -29,7 +24,6 @@ public virtual QueryTranslationPostprocessor Create(QueryCompilationContext quer dependencies, relationalDependencies, (RelationalQueryCompilationContext)queryCompilationContext, - relationalTypeMappingSource, - options); + relationalTypeMappingSource); } } diff --git a/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs b/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs index 439194bf6..8c4770ca3 100644 --- a/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs +++ b/src/EFCore.Jet/Scaffolding/Internal/JetDatabaseModelFactory.cs @@ -4,6 +4,7 @@ using EntityFrameworkCore.Jet.Data; using System.Globalization; using System.Text.RegularExpressions; +using EntityFrameworkCore.Jet.Infrastructure; using EntityFrameworkCore.Jet.Internal; using EntityFrameworkCore.Jet.Metadata.Internal; using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; @@ -123,15 +124,15 @@ public override DatabaseModel Create( var tableNames = databaseModel.Tables.Select(t => t.Name).ToList(); if (tableNames.Contains("MSysAccessStorage") || _msysNames.Contains("MSysAccessStorage")) { - JetConfiguration.DetectedDualTableName = "MSysAccessStorage"; + JetDualTable.DetectedName ="MSysAccessStorage"; } else if (tableNames.Contains("MSysRelationships") || _msysNames.Contains("MSysRelationships")) { - JetConfiguration.DetectedDualTableName = "MSysRelationships"; + JetDualTable.DetectedName ="MSysRelationships"; } else if (tableNames.Contains("#Dual")) { - JetConfiguration.DetectedDualTableName = "#Dual"; + JetDualTable.DetectedName ="#Dual"; } return databaseModel; diff --git a/src/EFCore.Jet/Storage/Internal/JetByteArrayTypeMapping.cs b/src/EFCore.Jet/Storage/Internal/JetByteArrayTypeMapping.cs index 21733afaa..d297b76f7 100644 --- a/src/EFCore.Jet/Storage/Internal/JetByteArrayTypeMapping.cs +++ b/src/EFCore.Jet/Storage/Internal/JetByteArrayTypeMapping.cs @@ -78,10 +78,24 @@ protected override void ConfigureParameter(DbParameter parameter) /// protected override string GenerateNonNullSqlLiteral(object value) { - var builder = new StringBuilder(); + var bytes = (byte[])value; + + // An empty array has no hex form here. SQL Server writes a bare '0x', but that is T-SQL only — + // ACE rejects it outright ("Syntax error in query expression '0x'"), so the SQL was invalid for + // this provider rather than merely unusual, and failed the same way on ACE and on LibRed. + // + // An empty STRING literal is what Access accepts, in a value position and in a DEFAULT. Verified + // against ACE: '' stores and reads back as a zero-length byte[], and stays distinct from NULL + // (IS NULL does not match it). Note 0x00 is NOT the same thing — that is a one-byte zero. + if (bytes.Length == 0) + { + return "''"; + } + + var builder = new StringBuilder(2 + bytes.Length * 2); builder.Append("0x"); - foreach (var @byte in (byte[])value) + foreach (var @byte in bytes) { builder.Append(@byte.ToString("X2", CultureInfo.InvariantCulture)); } diff --git a/src/EFCore.Jet/Storage/Internal/JetLongTypeMapping.cs b/src/EFCore.Jet/Storage/Internal/JetLongTypeMapping.cs index 4ab52decf..dc32bb5fc 100644 --- a/src/EFCore.Jet/Storage/Internal/JetLongTypeMapping.cs +++ b/src/EFCore.Jet/Storage/Internal/JetLongTypeMapping.cs @@ -32,9 +32,16 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p protected override void ConfigureParameter(DbParameter parameter) { base.ConfigureParameter(parameter); - //Needs to be Object. Using BigInt doesn't always work + //Needs to be Numeric. Using BigInt doesn't always work //If the argument value is a long and within Int32 range, having it as BigInt in x86 works - //When running in x64 it fails to convert. Using Object bypasses the conversion + //When running in x64 it fails to convert. Using Numeric bypasses the conversion + //Confirmed since against a real ACE Large Number (BIGINT) column, where it is not "doesn't + //always" but never: an OleDbType.BigInt parameter carries no value at all, zero included, + //while Numeric, Decimal and Variant each round-trip the full Int64 range exactly. DBTYPE_I8 + //looks simply never to have been wired into ACE's VARIANT-based coercion - which has been able + //to hold a decimal since the beginning, hence the type that works being the one for decimals. + //OdbcType.Numeric = 7; + //OleDbType.Numeric = 131; var setodbctype = parameter.GetType().GetMethods().FirstOrDefault(x => x.Name == "set_OdbcType"); var setoledbtype = parameter.GetType().GetMethods().FirstOrDefault(x => x.Name == "set_OleDbType"); diff --git a/src/EFCore.Jet/Storage/Internal/JetTimeSpanTypeMapping.cs b/src/EFCore.Jet/Storage/Internal/JetTimeSpanTypeMapping.cs index ba74a6e1e..c6e748f31 100644 --- a/src/EFCore.Jet/Storage/Internal/JetTimeSpanTypeMapping.cs +++ b/src/EFCore.Jet/Storage/Internal/JetTimeSpanTypeMapping.cs @@ -1,7 +1,5 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using EntityFrameworkCore.Jet.Infrastructure.Internal; - namespace EntityFrameworkCore.Jet.Storage.Internal { public class JetTimeSpanTypeMapping : TimeSpanTypeMapping diff --git a/src/LibRed/Directory.Build.props b/src/LibRed/Directory.Build.props index 8f4f617bc..0c8fb6120 100644 --- a/src/LibRed/Directory.Build.props +++ b/src/LibRed/Directory.Build.props @@ -3,17 +3,15 @@ The LibRed.* projects are a from-scratch, fully managed implementation of the Jet/ACE (Access MDB/ACCDB) engine. Unlike the ODBC/OleDb-based EFCore.Jet projects, they have NO Windows dependency and must stay cross-platform. + [SupportedOSPlatform("windows")] is applied by each driver-bound project itself, + so nothing here needs to opt out of it. - We therefore deliberately bypass src/Directory.Build.props (which stamps every - assembly with [SupportedOSPlatform("windows")]) and import the repo-root props - directly to pick up shared metadata, versioning and analyzer settings. --> - + enable enable - false false $(NoWarn);CS1591 diff --git a/src/LibRed/LibRed.Ado/LibRedCommand.cs b/src/LibRed/LibRed.Ado/LibRedCommand.cs index a17091ea5..095a09776 100644 --- a/src/LibRed/LibRed.Ado/LibRedCommand.cs +++ b/src/LibRed/LibRed.Ado/LibRedCommand.cs @@ -72,9 +72,15 @@ private Engine.CommandResult ExecuteBatch() Engine.CommandResult? last = null; foreach (string statement in SplitStatements(CommandText)) { + // A fragment holding no statement (only comments) is skipped rather than run: it must not become + // the batch's last result, or `INSERT …; -- done` would report the comment's zero rows instead of + // the insert's. A batch that is entirely comments falls through to the empty result below. + if (engine.IsStatementless(statement)) continue; + try { last = engine.Execute(statement, parameters); + Connection?.ReconcileSqlTransactionControl(); } catch (LibRed.ConstraintViolationException e) { @@ -84,6 +90,15 @@ private Engine.CommandResult ExecuteBatch() // unrecognised constraint failure there turns contention into a hard failure. throw new LibRedException(e.Message, LibRedException.DuplicateKey, e); } + catch (LibRed.SchemaObjectExistsException e) + { + // Same contract for DDL name collisions. EF Core's migration lock creates its lock table + // behind an exists-then-create check that several connections can pass at once, and catches + // the losers' "already exists" as DbException. Left untranslated this escapes that guard and + // fails the migration outright — ACE raises OleDbException there, so translating is what + // makes LibRed behave like the engine it stands in for. + throw new LibRedException(e.Message, LibRedException.ObjectAlreadyExists, e); + } } return last ?? new Engine.CommandResult(Engine.Execution.ResultSet.Empty, RecordsAffected: 0); @@ -149,24 +164,34 @@ private void ValidateTransaction() /// the epoch date + time-of-day, a date at midnight. private static readonly DateTime OleEpoch = new(1899, 12, 30); - /// Coerces a parameter value to what the engine should see. Jet/ACE has no native TimeSpan, TimeOnly, + /// + /// Coerces a parameter value to what the engine should see. Jet/ACE has no native TimeSpan, TimeOnly, /// DateOnly or DateTimeOffset — they are all stored as a on the 1899-12-30 epoch — so /// this boundary (the single point EF parameters enter the engine) converts each to that DateTime, exactly as /// the literal path does (a TimeSpan literal renders as a #…#/TIMEVALUE DateTime). The engine then only - /// ever handles DateTime for temporals, and the reader converts back on the way out. Sub-seconds are stripped - /// (Jet has 1-second resolution) so a WHERE d = @p comparison matches the seconds-only stored value. + /// ever handles DateTime for temporals, and the reader converts back on the way out. + /// + /// + /// Values are truncated to whole MILLISECONDS, not whole seconds. ACE has one-second resolution, but that is + /// ACE truncating on write — LibRed stores the full OA double, and a millisecond survives it exactly + /// (measured: 12:34:56.123 round-trips with zero tick loss). Below a millisecond nothing survives whatever + /// this does, because .NET's ToOADate/FromOADate quantise there; truncating to the same boundary the store + /// uses is what keeps WHERE d = @p matching, which is the reason this truncates at all. + /// private static object? Normalize(object? value) => value switch { DBNull => null, - DateTime d => Seconds(d), + DateTime d => Milliseconds(d), // DateTimeOffset is read back at offset zero, so store its UTC instant. - DateTimeOffset dto => Seconds(dto.UtcDateTime), - TimeSpan t => OleEpoch + Seconds(t), - TimeOnly to => OleEpoch + Seconds(to.ToTimeSpan()), + DateTimeOffset dto => Milliseconds(dto.UtcDateTime), + TimeSpan t => OleEpoch + Milliseconds(t), + TimeOnly to => OleEpoch + Milliseconds(to.ToTimeSpan()), DateOnly d => d.ToDateTime(TimeOnly.MinValue), _ => value, }; - private static DateTime Seconds(DateTime d) => d.AddTicks(-(d.Ticks % TimeSpan.TicksPerSecond)); - private static TimeSpan Seconds(TimeSpan t) => TimeSpan.FromTicks(t.Ticks - t.Ticks % TimeSpan.TicksPerSecond); + private static DateTime Milliseconds(DateTime d) => d.AddTicks(-(d.Ticks % TimeSpan.TicksPerMillisecond)); + + private static TimeSpan Milliseconds(TimeSpan t) => + TimeSpan.FromTicks(t.Ticks - t.Ticks % TimeSpan.TicksPerMillisecond); } diff --git a/src/LibRed/LibRed.Ado/LibRedConnection.cs b/src/LibRed/LibRed.Ado/LibRedConnection.cs index abe6b161a..0b715769d 100644 --- a/src/LibRed/LibRed.Ado/LibRedConnection.cs +++ b/src/LibRed/LibRed.Ado/LibRedConnection.cs @@ -55,6 +55,14 @@ internal void RollbackTransaction(LibRedTransaction transaction) CurrentTransaction = null; } + /// Keeps an ADO transaction handle honest when SQL COMMIT/ROLLBACK closes its transaction. + internal void ReconcileSqlTransactionControl() + { + if (_database?.TransactionDepth != 0 || CurrentTransaction is null) return; + CurrentTransaction.CompleteFromSql(); + CurrentTransaction = null; + } + /// Opens a savepoint in the connection's active transaction (called by /// ). internal LibRed.IO.Savepoint CreateSavepoint() => @@ -103,16 +111,28 @@ public static string GetConnectionString(string fileNameOrConnectionString) /// /// synthesises the file from scratch (page 0, /// the free map, and the bootstrap system catalog), then LibRed's ordinary writers populate it. - /// Produces an ACE 2007-format (.accdb) database that LibRed reads and writes fully; the - /// remaining Access-compatibility system tables are still being filled in. + /// Produces an .accdb that LibRed reads and writes fully; the remaining Access-compatibility + /// system tables are still being filled in. /// - public static void CreateDatabase(string connectionString) + /// The database's default text collating order, written to page 0 and inherited + /// by every column created in it. Defaults to General-Legacy (the order the engine writes); pass + /// for the "General" order Access 2010+ offers. + /// The file format to create. Defaults to + /// (ACE 12), which every Access from 2007 on can open — + /// raise it only for a database that needs a later type, since an older Access cannot open a newer file + /// at all. is what BIGINT requires and + /// what DATETIME2 requires; AccessTypeMapper + /// refuses either type below its version rather than write a file Access could not read. + public static void CreateDatabase( + string connectionString, + Catalog.Collation? collation = null, + Formats.JetVersion version = Formats.JetVersion.Version12_2007) { string path = ParseDataSource(connectionString); if (string.IsNullOrEmpty(path)) throw new ArgumentException("The connection string is missing a Data Source.", nameof(connectionString)); - Storage.DatabaseCreator.CreateEmpty(path); + Storage.DatabaseCreator.CreateEmpty(path, (byte)version, collation); CreateDualTable(path); } @@ -181,6 +201,7 @@ public override void Open() _database = JetDatabase.Open(path, readOnly: false); Engine = new QueryEngine(_database); _state = ConnectionState.Open; + OnStateChange(new StateChangeEventArgs(ConnectionState.Closed, ConnectionState.Open)); } public override void Close() @@ -197,7 +218,14 @@ public override void Close() _database?.Dispose(); _database = null; Engine = null; + + // Only a real transition raises the event. Close() is not guarded against being called on an already + // closed connection - and Dispose() calls it - so firing unconditionally would report a second close + // that never happened. EF's connection diagnostics count these. + if (_state == ConnectionState.Closed) return; + _state = ConnectionState.Closed; + OnStateChange(new StateChangeEventArgs(ConnectionState.Open, ConnectionState.Closed)); } public override void ChangeDatabase(string databaseName) => @@ -223,7 +251,15 @@ protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLeve protected override void Dispose(bool disposing) { + // Clearing the connection string is what makes a disposed connection unusable, as ADO.NET requires: + // Open() then fails its existing "missing a Data Source" guard instead of quietly reopening the file. + // SqlConnection and JetConnection both do exactly this, and it is why the resulting exception is an + // InvalidOperationException rather than an ObjectDisposedException. Assigned to the field directly + // because the property setter refuses to change while the connection is still open. + _connectionString = string.Empty; + if (disposing) Close(); + base.Dispose(disposing); } diff --git a/src/LibRed/LibRed.Ado/LibRedException.cs b/src/LibRed/LibRed.Ado/LibRedException.cs index c5cdbcaec..c12d4f3b0 100644 --- a/src/LibRed/LibRed.Ado/LibRedException.cs +++ b/src/LibRed/LibRed.Ado/LibRedException.cs @@ -33,4 +33,13 @@ public LibRedException(string message, int number, Exception? innerException) /// duplicate-key error so callers that already special-case 2627 need no extra branch. /// public const int DuplicateKey = 2627; + + /// + /// DDL named a schema object that already exists. Provider code keys on this rather than on the + /// message: EF Core's migration lock creates its lock table under a racy exists-then-create guard and + /// treats the loser's failure as the normal path. The value matches SQL Server's "there is already an + /// object named ... in the database" error, so callers that already special-case 2714 need no extra + /// branch. + /// + public const int ObjectAlreadyExists = 2714; } diff --git a/src/LibRed/LibRed.Ado/LibRedFactory.cs b/src/LibRed/LibRed.Ado/LibRedFactory.cs index 991ff27cf..5a8cfa7b4 100644 --- a/src/LibRed/LibRed.Ado/LibRedFactory.cs +++ b/src/LibRed/LibRed.Ado/LibRedFactory.cs @@ -24,11 +24,4 @@ private LibRedFactory() { } /// rather than sharing that generic extension surface. /// public override DbConnectionStringBuilder CreateConnectionStringBuilder() => new LibRedConnectionStringBuilder(); - - /// - /// Returns the to use for LibRed data access. LibRed has a - /// single native engine (no ODBC/OLE DB driver choice), so this always returns - /// . Exists for API parity with JetFactory.GetDataAccessProviderFactory. - /// - public static DbProviderFactory GetDataAccessProviderFactory() => Instance; } diff --git a/src/LibRed/LibRed.Ado/LibRedTransaction.cs b/src/LibRed/LibRed.Ado/LibRedTransaction.cs index 9537bb0f5..4c4886909 100644 --- a/src/LibRed/LibRed.Ado/LibRedTransaction.cs +++ b/src/LibRed/LibRed.Ado/LibRedTransaction.cs @@ -63,6 +63,14 @@ private void EnsureActive() throw new InvalidOperationException("This transaction has already been committed or rolled back."); } + /// Marks this handle complete when SQL COMMIT/ROLLBACK closes the physical transaction. + internal void CompleteFromSql() + { + _completed = true; + _connection = null; + _savepoints.Clear(); + } + // A DbException (not a plain InvalidOperationException): referencing a released/never-opened savepoint is a // database-operation error, and callers — EF Core's transaction tests included — expect DbException for it. private Savepoint Lookup(string name) => diff --git a/src/LibRed/LibRed.Core/Catalog/Collation.cs b/src/LibRed/LibRed.Core/Catalog/Collation.cs index 6e2b2879a..befc0d911 100644 --- a/src/LibRed/LibRed.Core/Catalog/Collation.cs +++ b/src/LibRed/LibRed.Core/Catalog/Collation.cs @@ -3,38 +3,71 @@ namespace LibRed.Catalog; /// /// A text collating order, identified by its Windows locale id (LCID) — the value Jet/ACE stores in a /// column descriptor's locale bytes (0x0B/0x0C) and, database-wide, in the page-0 sort order. -/// These mirror DAO's CollatingOrderEnum; each name is the LCID Access records. +/// These mirror DAO's CollatingOrderEnum; each name is the LCID Access records. That is a Jet-3.5-era +/// list and no longer matches what ACE offers, in both directions: Access's "New Database Sort Order" adds +/// Bosnian, Croatian, Serbian, Macedonian, Ukrainian, Estonian, Latvian, Lithuanian, Slovak, Romanian, +/// Georgian Modern, Vietnamese, Indic, French, German Phone Book, Hungarian Technical and the CJK variants, +/// and offers none of the five marked inert below. Those five are still creatable through DAO and are +/// recorded faithfully on page 0 and in column descriptors, but ACE encodes **General** keys for them +/// regardless — verified over 82 samples in DaoLocaleCollationProbeTest. Treat them as metadata. +/// +/// That sample set includes words carrying TWO marks in each script, which is the shape that matters: French +/// tailors no letter at all and would look inert on single characters, because it reverses the diacritic +/// section and a word with one accent encodes identically to General. The Greek triple +/// άα/αά/άαά is the direct analogue of the French coté/côte/côté +/// that exposed it. The probe also carries Spanish, Czech, Polish and Turkish as positive controls, so a +/// null result means the orders are inert rather than the harness being dead. +/// /// /// /// The LCID alone does not pin the on-disk key bytes: "General" (1033) has a legacy order (version 0, /// Access 2000–2007) and a different default order (version 1, Access 2010+). The version lives in a -/// separate descriptor byte — see . Paradox-ISAM variants that DAO lists share -/// LCIDs with these (e.g. dbSortPDXIntl == 1033) and are omitted; they are link-only and irrelevant here. +/// separate descriptor byte — see . The two axes are independent: both Spanish orders +/// are version 0, so the version selects a weight-table generation rather than a locale variant. Paradox-ISAM +/// variants that DAO lists share LCIDs with these (e.g. dbSortPDXIntl == 1033) and are omitted; they are +/// link-only and irrelevant here. /// public enum CollatingOrder { Undefined = -1, Neutral = 1024, - Arabic = 1025, + Arabic = 1025, // inert: recorded, but ACE encodes General keys — see the remarks below ChineseTraditional = 1028, Czech = 1029, NorwegianDanish = 1030, - Greek = 1032, + Greek = 1032, // inert + German = 1031, // with sort id 1 = "German Phone Book" General = 1033, // English, German, French, Portuguese — the default - Spanish = 1034, - Hebrew = 1037, - Hungarian = 1038, + Spanish = 1034, // Spanish Traditional: "ch" and "ll" are letters (DAO's dbSortSpanish) + French = 1036, + Hebrew = 1037, // inert + Hungarian = 1038, // with sort id 1 = "Hungarian Technical" Icelandic = 1039, Japanese = 1041, Korean = 1042, - Dutch = 1043, + Dutch = 1043, // inert + Norwegian = 1044, // Access's "Norwegian/Danish" — note DAO's dbSortNorwDan is Danish 1030 instead Polish = 1045, - Cyrillic = 1049, + Romanian = 1048, + Cyrillic = 1049, // inert + Croatian = 1050, + Slovak = 1051, SwedishFinnish = 1053, Thai = 1054, Turkish = 1055, + Ukrainian = 1058, Slovenian = 1060, + Estonian = 1061, + Latvian = 1062, + Lithuanian = 1063, + Vietnamese = 1066, + Macedonian = 1071, + Georgian = 1079, // with sort id 1 = "Georgian Modern" + Indic = 1081, ChineseSimplified = 2052, + Serbian = 2074, + SpanishModern = 3082, // The 1994 reform: "ch"/"ll" are letter pairs. No DAO name — it postdates the enum + Bosnian = 5146, } /// @@ -42,15 +75,22 @@ public enum CollatingOrder /// that selects between weight tables sharing that LCID. Determines the index-key /// bytes for text/memo columns, and is written into their column descriptors. /// -/// The collating order (LCID). -/// The sort-order version — the byte at column-descriptor 0x0E (the high byte of -/// a nominally 2-byte version field at 0x0D). Verified vs Access: General Legacy (Access 2000–2007) = -/// 0; the "General" order Access 2010 made default = 1. The low byte at 0x0D is 0 -/// in every file seen and is not modelled — but the field is nominally 2 bytes, so keep an eye on it: if a -/// database ever carries a non-zero 0x0D we are truncating a wider value (cf. the AutoNumber increment -/// at TDEF 0x18, which looked like a 1-byte flag but was a full signed int32). -public readonly record struct Collation(CollatingOrder Order, byte Version) +/// The collating order's LANGID — column descriptor 0x0B/0x0C. +/// The sort-order version — the byte at column-descriptor 0x0E. Verified vs +/// Access: the legacy compacted table (Access 2000–2007) = 0; the "General" order Access 2010 made +/// default = 1. Not a General-only axis: Croatian and Romanian each ship in both versions. +/// The LCID's high word — column descriptor 0x0D, page-0 0x70. Zero for a +/// locale's default order; non-zero selects a Windows alternate sort order, which is a different +/// ordering for the same LANGID. Without it Hungarian Technical (0x0001040E) is +/// indistinguishable from Hungarian (0x0000040E), and German Phone Book from German. This byte was +/// documented as "0 in every file seen — keep an eye on it" until fixtures for those two orders showed it +/// carrying 0x01. +public readonly record struct Collation(CollatingOrder Order, byte Version, byte SortId = 0) { + /// The full 32-bit Windows LCID: (SortId << 16) | LANGID. The version is not part of + /// it — Jet stores that in the LCID's unused top byte, but Windows does not define it. + public int Lcid => (SortId << 16) | (int)Order; + /// The sort-order version byte for the Access-2010+ "General" order. public const byte GeneralVersion = 1; @@ -58,11 +98,24 @@ public readonly record struct Collation(CollatingOrder Order, byte Version) /// can encode index keys for. public static Collation GeneralLegacy => new(CollatingOrder.General, 0); - /// The Access-2010+ default "General" order — locale 1033, version 1. LibRed reads and - /// distinguishes it, but does not yet encode its (different) index keys — see . + /// The Access-2010+ default "General" order — locale 1033, version 1. Its keys use the Windows + /// NLS weights directly rather than General-Legacy's compacted table; see JetTextCollationV1. public static Collation General => new(CollatingOrder.General, GeneralVersion); - /// Whether LibRed can encode index keys for this collation. Only General legacy is implemented; - /// see JetTextCollation and the format spec §10.4. - public bool IsIndexKeyEncodable => this == GeneralLegacy; + /// Whether LibRed can encode index keys for this collation: both General orders (v0 via + /// JetTextCollation, v1 via JetTextCollationV1), plus every locale with an entry in + /// JetLocaleTailoring. Anything else is refused rather than encoded with the English table — see + /// the format spec §10.4. + public bool IsIndexKeyEncodable + { + get + { + if (this == GeneralLegacy || this == General) return true; + // Both encoders tailor now. Version 1 was refused for a while on the grounds that its primaries + // are two-byte NLS values "a different shape" — but that was a fact about the encoder, not about + // the orders: Bosnian, Croatian and Serbian measure as General v1 plus twenty entries, the same + // six devices every version-0 locale uses. + return Storage.JetLocaleTailoring.For(this) is not null; + } + } } diff --git a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs index 6e50f8ea2..252e12fcf 100644 --- a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs +++ b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs @@ -37,36 +37,47 @@ public sealed class JetCatalog(PageChannel channel, int catalogPage = 2) private Dictionary? _views; private Dictionary? _actionQueries; private Dictionary>? _queryParameters; + private long _seenSchemaGeneration = channel.SchemaGeneration; /// All tables in the database (user and system). - public IReadOnlyList Tables => _tables ??= LoadTables(); + public IReadOnlyList Tables { get { EnsureFresh(); return _tables ??= LoadTables(); } } /// Views (stored simple-SELECT queries) as name → reconstructed SELECT SQL, rebuilt from /// each view's MSysQueries rows. Complex/system queries that don't reconstruct are omitted. - public IReadOnlyDictionary Views { get { EnsureStoredQueries(); return _views!; } } + public IReadOnlyDictionary Views { get { EnsureFresh(); EnsureStoredQueries(); return _views!; } } /// Stored action queries (a CREATE PROCEDURE body that is not a SELECT) as name → readback. /// A supported query (CREATE/DROP TABLE, INSERT … VALUES) carries executable SQL; an unsupported one /// (INSERT … SELECT, etc.) carries only a reason and throws when LibRed is asked to execute it. - public IReadOnlyDictionary ActionQueries { get { EnsureStoredQueries(); return _actionQueries!; } } + public IReadOnlyDictionary ActionQueries { get { EnsureFresh(); EnsureStoredQueries(); return _actionQueries!; } } /// A stored query's declared parameter names in declaration order (its Attribute=2 rows). /// Used to bind an EXECUTE proc a, b's positional arguments to the procedure's named parameters. /// Empty for a query with no parameters. - public IReadOnlyDictionary> QueryParameters { get { EnsureStoredQueries(); return _queryParameters!; } } + public IReadOnlyDictionary> QueryParameters { get { EnsureFresh(); EnsureStoredQueries(); return _queryParameters!; } } /// Drops the cached catalog so a freshly created table is picked up on next read. - public void Invalidate() + public void Invalidate(bool markChanged = true) { _tables = null; _relationships = null; _views = null; _actionQueries = null; _queryParameters = null; + _seenSchemaGeneration = _channel.SchemaGeneration; + if (markChanged) _channel.MarkSchemaChanged(); + } + + private void EnsureFresh() + { + long generation = _channel.SchemaGeneration; + if (generation == _seenSchemaGeneration) return; + Invalidate(markChanged: false); + _seenSchemaGeneration = generation; } /// All relationships (foreign keys) defined in the database. - public IReadOnlyList Relationships => _relationships ??= LoadRelationships(); + public IReadOnlyList Relationships { get { EnsureFresh(); return _relationships ??= LoadRelationships(); } } /// Relationships for which is the referencing (child) table. public IEnumerable ForeignKeysOf(string table) => @@ -250,6 +261,11 @@ private static StoredActionQuery ReconstructAction(List rows, int att return new StoredActionQuery($"INSERT INTO [{target}] ({columns}) VALUES ({values})", null); } + if (kind == StoredQueryFormat.ActionUpdate) + return new StoredActionQuery(null, "UPDATE stored queries are not executed by LibRed yet."); + if (kind == StoredQueryFormat.ActionDelete) + return new StoredActionQuery(null, "DELETE stored queries are not executed by LibRed yet."); + return new StoredActionQuery(null, "This stored action query kind is not supported by LibRed yet."); } diff --git a/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs b/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs index 346260ed7..4abec58c0 100644 --- a/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs +++ b/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs @@ -479,12 +479,12 @@ public static byte[] BuildColumnDescriptor(ColumnDef c, JetFormatBase format) } else { - // Non-numeric columns store the text-collation LCID in the precision/scale bytes (0x0B/0x0C) and the - // sort-order version as the HIGH byte (0x0E) of the 2-byte field at 0x0D. General legacy is LCID 1033 - // (0x0409), version 0; the Access-2010 "General" order is version 1. The low byte 0x0D is left as-is - // (0 on a fresh column, preserved from the original descriptor otherwise) — see Collation.Version. + // Non-numeric columns use the precision/scale bytes (0x0B/0x0C) onward for the text collation: + // 0x0B/0x0C LANGID, 0x0D sort id, 0x0E sort-order version. Together a 32-bit LCID with the version + // in its unused top byte. General legacy is LANGID 1033 (0x0409), sort id 0, version 0. BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnLocaleOffset, 2), (ushort)c.Collation.Order); - d[format.ColumnCollationVersionOffset + 1] = c.Collation.Version; + d[format.ColumnCollationSortIdOffset] = c.Collation.SortId; + d[format.ColumnCollationVersionOffset] = c.Collation.Version; } // Compose the flag byte (0x0F) from EVERY documented bit; only the undocumented bits survive from the // original (zero in every file observed). Likewise the extended-flag byte (0x10). diff --git a/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs b/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs index 7d8892ffd..77f68f4a7 100644 --- a/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs +++ b/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs @@ -62,8 +62,7 @@ public static void RemovePassword(string path, string password) public static void SetPasswordRc4(string path, string password, int keyBits = 40, StandardHash hash = StandardHash.Sha1) { ArgumentException.ThrowIfNullOrEmpty(password); - if (keyBits is < 40 or > 128 || keyBits % 8 != 0) - throw new ArgumentOutOfRangeException(nameof(keyBits), "RC4 key length must be 40–128 bits, in multiples of 8."); + ValidateRc4Options(keyBits, hash); byte[] file = File.ReadAllBytes(path); if (!DetectFormat(file).IsAccdb) @@ -81,6 +80,10 @@ public static void SetPasswordRc4(string path, string password, int keyBits = 40 /// with the new one — exactly remove + set. public static void ChangePassword(string path, string oldPassword, string newPassword, AccessEncryption scheme) { + ArgumentException.ThrowIfNullOrEmpty(newPassword); + // Validate before RemovePassword decrypts, so a rejected scheme can't leave the database plaintext. + // Detect reads only the header, so this costs one page rather than a second copy of the whole file. + ValidateScheme(scheme, DetectFormatOf(path)); RemovePassword(path, oldPassword); SetPassword(path, newPassword, scheme); } @@ -89,10 +92,19 @@ public static void ChangePassword(string path, string oldPassword, string newPas /// decrypt with the old password, then . public static void ChangePasswordRc4(string path, string oldPassword, string newPassword, int keyBits = 40, StandardHash hash = StandardHash.Sha1) { + ArgumentException.ThrowIfNullOrEmpty(newPassword); + ValidateRc4Options(keyBits, hash); RemovePassword(path, oldPassword); SetPasswordRc4(path, newPassword, keyBits, hash); } + private static void ValidateRc4Options(int keyBits, StandardHash hash) + { + if (keyBits is < 40 or > 128 || keyBits % 8 != 0) + throw new ArgumentOutOfRangeException(nameof(keyBits), "RC4 key length must be 40–128 bits, in multiples of 8."); + _ = ToHashName(hash); + } + /// Sets the legacy Jet 4 (.mdb) database password — the "Set Database Password" feature, which is /// password obfuscation only (the data pages stay plaintext; this is not RC4 page encryption). The password /// (≤20 chars) is stored UTF-16LE at 0x42, XOR-masked with the 32-bit truncation of the creation-date @@ -281,6 +293,14 @@ private static void ValidateScheme(AccessEncryption scheme, JetFormatBase format } } + /// Detects a file's format without loading it: reads only the + /// header, so this is a header read rather than a full copy of a database that may be hundreds of MB. + private static JetFormatBase DetectFormatOf(string path) + { + using FileStream stream = File.OpenRead(path); + return JetFormatBase.Detect(stream); + } + private static JetFormatBase DetectFormat(byte[] file) { using var ms = new MemoryStream(file, writable: false); diff --git a/src/LibRed/LibRed.Core/Crypto/OfficeStandardEncryption.cs b/src/LibRed/LibRed.Core/Crypto/OfficeStandardEncryption.cs index e879bbeff..a64310ce6 100644 --- a/src/LibRed/LibRed.Core/Crypto/OfficeStandardEncryption.cs +++ b/src/LibRed/LibRed.Core/Crypto/OfficeStandardEncryption.cs @@ -90,6 +90,16 @@ private OfficeStandardEncryption(bool rc4, HashAlgorithmName hashName, byte[] ba if (databaseKey == 0) return null; + const int descriptorOffset = 0x29B; + if (page0.Length < descriptorOffset) + throw new InvalidDataException("Page 0 is too short to contain an ACE EncryptionInfo frame."); + int descriptorLength = BinaryPrimitives.ReadUInt16LittleEndian(page0.Slice(0x299, 2)); + if (descriptorLength == 0) + return null; + if (descriptorLength > page0.Length - descriptorOffset) + throw new InvalidDataException("The declared Office-Standard EncryptionInfo extends beyond page 0."); + page0 = page0.Slice(descriptorOffset, descriptorLength); + int ei = LocateBinaryEncryptionInfo(page0); if (ei < 0) return null; @@ -346,7 +356,7 @@ private static int LocateBinaryEncryptionInfo(ReadOnlySpan page0) { // A binary EncryptionInfo begins: uint16 major, uint16 minor(=2 for standard/CryptoAPI), uint32 flags // (fCryptoAPI=0x04 set), uint32 headerSize. Validate against a known cipher AlgID to avoid false hits. - for (int i = 0x100; i + 32 < page0.Length && i < 0x400; i++) + for (int i = 0; i + 32 < page0.Length; i++) { ushort major = BinaryPrimitives.ReadUInt16LittleEndian(page0.Slice(i, 2)); ushort minor = BinaryPrimitives.ReadUInt16LittleEndian(page0.Slice(i + 2, 2)); diff --git a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs index d8d9e44ae..6b36cc45a 100644 --- a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs +++ b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs @@ -68,11 +68,17 @@ public abstract class JetFormatBase /// creation-date-derived value, so an empty password does not read as zeroes). public const int PasswordOffset = 0x42; - /// Offset of the 4-byte default text collating sort order: LCID (2 bytes LE, e.g. - /// 0x0409 = 1033 en-US) followed by the sort-order version at 0x71 (0 = General - /// Legacy, 1 = General). The version here matches each column's descriptor byte 0x0E. + /// Offset of the 4-byte default text collating sort order — a 32-bit Windows LCID whose + /// otherwise-unused top byte carries the sort-order version. Byte for byte it mirrors a column + /// descriptor's 0x0B..0x0E: LANGID at 0x6E (2 bytes LE, 0x0409 = 1033 en-US), + /// sort id at 0x70, version at 0x71. public const int CollationSortOrderOffset = 0x6E; + /// Offset of the collation's 1-byte sort id — the LCID's high word, which is what distinguishes + /// an alternate sort order from its base locale (German Phone Book 0x00010407 vs German + /// 0x00000407; Hungarian Technical 0x0001040E vs Hungarian 0x0000040E). + public const int CollationSortIdOffset = 0x70; + /// Offset of the 1-byte collation sort-order version within the sort-order field (0/1). public const int CollationVersionOffset = 0x71; @@ -170,10 +176,15 @@ public abstract class JetFormatBase public virtual int ColumnVariableIndexOffset => 0x07; // position among variable columns (0 for fixed) public virtual int ColumnPrecisionOffset => 0x0B; // Decimal/Numeric columns only public virtual int ColumnScaleOffset => 0x0C; // Decimal/Numeric columns only - // Non-numeric columns instead use 0x0B/0x0C for the text-collation LCID (a little-endian UInt16 read at - // 0x0B), and 0x0D for its sort-order version (0 = General legacy, 1 = the Access 2010 order). + // Non-numeric columns instead use 0x0B..0x0E for the text collation, and the four bytes together are a + // 32-bit Windows LCID with the sort-order version in its otherwise-unused top byte: + // 0x0B/0x0C LANGID, little-endian (0x0409 = General/en-US) + // 0x0D sort id — the high word of the LCID, which is what separates an alternate sort order from + // its base locale (German Phone Book = 0x00010407, Hungarian Technical = 0x0001040E) + // 0x0E sort-order version (0 = the legacy compacted table, 1 = the Access 2010 NLS order) public virtual int ColumnLocaleOffset => 0x0B; - public virtual int ColumnCollationVersionOffset => 0x0D; + public virtual int ColumnCollationSortIdOffset => 0x0D; + public virtual int ColumnCollationVersionOffset => 0x0E; public virtual int ColumnFlagsOffset => 0x0F; /// Extended column flags (0x10): bit 0x01 = compressed-Unicode capable, 0xC0 = calculated column. public virtual int ColumnExtendedFlagsOffset => 0x10; diff --git a/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs b/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs index 5618b9786..45be1e743 100644 --- a/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs +++ b/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs @@ -39,6 +39,12 @@ internal static class StoredQueryFormat /// AttrAction Flag value for an append (INSERT) query (target table in Name1). public const short ActionAppend = 3; + /// AttrAction Flag value for a DELETE query. + public const short ActionDelete = 5; + + /// AttrAction Flag value for an UPDATE query. + public const short ActionUpdate = 4; + /// AttrColumn Flag bit marking an appended literal value. public const short AppendValueFlag = unchecked((short)0x8000); } diff --git a/src/LibRed/LibRed.Core/IO/ILockManager.cs b/src/LibRed/LibRed.Core/IO/ILockManager.cs index 7c411bf7b..c3295a757 100644 --- a/src/LibRed/LibRed.Core/IO/ILockManager.cs +++ b/src/LibRed/LibRed.Core/IO/ILockManager.cs @@ -15,9 +15,10 @@ namespace LibRed.IO; /// docs/design/transactions.md. /// /// -/// Locks are operation-scoped in this phase: acquired and released around a single page read or -/// write, which prevents a reader from seeing a half-written page. Holding locks to transaction commit (strict -/// two-phase locking, for full isolation) is a later refinement layered on the same seam. +/// Locks are operation-scoped: acquired and released around a single page read or write, which +/// prevents a reader from seeing a half-written page. Transaction overlays use optimistic committed-page +/// validation at publish time, so overlapping writers fail deterministically instead of losing an update. +/// Strict two-phase/cross-process locking remains a later refinement layered on the same seam. /// /// The API is Enter/Exit (not a disposable handle) so the hot path — a page read takes /// and releases a shared lock — allocates nothing; pairs each Enter with an Exit in a diff --git a/src/LibRed/LibRed.Core/IO/PageCache.cs b/src/LibRed/LibRed.Core/IO/PageCache.cs index 20dd920a4..d5b738e64 100644 --- a/src/LibRed/LibRed.Core/IO/PageCache.cs +++ b/src/LibRed/LibRed.Core/IO/PageCache.cs @@ -4,8 +4,9 @@ namespace LibRed.IO; /// A bounded, write-through buffer pool of raw page bytes for one physical database file, shared by every /// open on that file (see ). A single cache per file is what /// keeps coexisting handles coherent: because they all read and write through the same pool, one connection's -/// committed — or, as today, uncommitted — writes are immediately visible to the others, exactly as the old -/// straight-to-disk reads were, only now served from memory instead of a Seek()+Read() per page. +/// committed writes are immediately visible to the others, while transaction-local overlay pages remain private, +/// exactly as the old straight-to-disk committed reads were, only now served from memory instead of a +/// Seek()+Read() per page. /// /// The pool owns its own copies: reads copy out to the caller and writes copy in from the /// caller, so a caller mutating its buffer can never corrupt a cached page. Eviction is LRU, capped at @@ -32,6 +33,10 @@ private sealed class Entry(int page, byte[] bytes) private readonly int _pageSize; private readonly object _gate = new(); + // Shared for readers, exclusive for publication. Recursive so a writing statement can hold the exclusive + // scope while each page write re-enters it; an attempted read→write upgrade is rejected explicitly below. + private readonly ReaderWriterLockSlim _publishGate = new(LockRecursionPolicy.SupportsRecursion); + private long _schemaGeneration; private readonly Dictionary> _map = []; private readonly LinkedList _lru = new(); // first = most-recently-used @@ -140,6 +145,58 @@ public void Store(int page, ReadOnlySpan source) } } + /// Removes a page which was appended during a failed commit and subsequently truncated. + public void Remove(int page) + { + lock (_gate) + { + if (!_map.Remove(page, out LinkedListNode? node)) return; + _lru.Remove(node); + } + } + + /// Serializes committed-page validation and publication for every channel on this file, and + /// excludes readers for the duration. A transaction validates all pages it derived from and publishes its + /// overlay while holding this gate; ordinary write-through operations use the same gate, preventing a write + /// between validation and publish. Writing statements hold it for their whole duration, so a reader never + /// sees one statement's pages half-written. The gate is recursive: a statement-scoped acquisition nests the + /// per-page publications inside it. + public void PublishLocked(Action action) => PublishLocked(() => { action(); return null; }); + + /// + public T PublishLocked(Func action) + { + // A reader that turns out to write is a statement-classification bug (see QueryEngine): the shared + // gate cannot be upgraded, so say so rather than surfacing a bare LockRecursionException. + if (_publishGate.IsReadLockHeld) + throw new InvalidOperationException( + "A page write was attempted inside a shared (read-consistent) scope; the statement should have taken the exclusive scope."); + + _publishGate.EnterWriteLock(); + try { return action(); } + finally { _publishGate.ExitWriteLock(); } + } + + /// Runs one logical read while excluding a multi-page commit publication, so the caller sees + /// either the complete pre-commit or complete post-commit page set. Shared: concurrent readers on this file + /// run together, and only a publication (see ) excludes them. + public T ReadConsistent(Func action) + { + _publishGate.EnterReadLock(); + try { return action(); } + finally { _publishGate.ExitReadLock(); } + } + + public long SchemaGeneration + { + get { lock (_gate) return _schemaGeneration; } + } + + public void MarkSchemaChanged() + { + lock (_gate) _schemaGeneration++; + } + /// Returns a previously cached higher-layer parse of (see /// ), or false if the page is not resident or has no parse cached. public bool TryGetParsed(int page, out object? parsed) diff --git a/src/LibRed/LibRed.Core/IO/PageChannel.cs b/src/LibRed/LibRed.Core/IO/PageChannel.cs index 23b9d248e..7b318796a 100644 --- a/src/LibRed/LibRed.Core/IO/PageChannel.cs +++ b/src/LibRed/LibRed.Core/IO/PageChannel.cs @@ -40,6 +40,11 @@ public sealed class PageChannel : IDisposable // lock. `_txPageCount` is the logical page count during a transaction (committed pages plus any the overlay // allocated), since deferred allocations do not grow the file until commit. private readonly Dictionary _overlay = []; + // Committed plaintext image from which each transactional page was first derived. At commit, every image + // must still match; otherwise another channel committed the same page and publishing this stale overlay + // would silently lose that writer's change. + private readonly Dictionary _commitBaselines = []; + private bool _schemaDirty; private int _txPageCount; private PageChannel(FileStream stream, JetFormatBase format, bool readOnly, string path, IPageCodec? codec, ILockManager? locks) @@ -58,7 +63,20 @@ private PageChannel(FileStream stream, JetFormatBase format, bool readOnly, stri /// Whether a transaction is currently open on this channel. public bool InTransaction => _active is not null; - public JetFormatBase Format { get; } + /// + /// The resolved on-disk format. Settable only by and its rollback + /// counterpart: a database's format version can move up in place when DDL introduces a type that needs a + /// newer one, which is what Access itself does. + /// + public JetFormatBase Format { get; private set; } + + internal long SchemaGeneration => _cache.SchemaGeneration; + + internal void MarkSchemaChanged() + { + if (_active is not null) _schemaDirty = true; + else _cache.MarkSchemaChanged(); + } public int PageSize => Format.PageSize; @@ -231,6 +249,8 @@ public void WritePage(int pageNumber, ReadOnlySpan source) // restore it, then buffer a private copy of the new bytes and advance the logical page count. if (_active is not null) { + if (!_commitBaselines.ContainsKey(pageNumber)) + _commitBaselines[pageNumber] = ReadCommittedPageOrNull(pageNumber); if (_active.NeedsBeforeImage(pageNumber)) _active.RecordBeforeImage(pageNumber, _overlay.TryGetValue(pageNumber, out byte[]? prior) ? prior : null); _overlay[pageNumber] = source[..PageSize].ToArray(); @@ -245,6 +265,20 @@ public void WritePage(int pageNumber, ReadOnlySpan source) /// disk for an encrypted file while caching plaintext, growing the file if the page lies past its end. Used /// for non-transactional writes and to publish each overlay page on commit. private void WriteThrough(int pageNumber, ReadOnlySpan source) + { + byte[] copy = source[..PageSize].ToArray(); + _cache.PublishLocked(() => WriteThroughUnderPublishLock(pageNumber, copy)); + } + + /// Runs a logical read against one committed page-set generation. Shared: other readers on this + /// file run concurrently; only a publication excludes them. + internal T ReadConsistent(Func action) => _cache.ReadConsistent(action); + + /// Runs a logical write with every other reader and writer on this file excluded, so its pages + /// publish as one unit. + internal T WriteExclusive(Func action) => _cache.PublishLocked(action); + + private void WriteThroughUnderPublishLock(int pageNumber, ReadOnlySpan source) { _locks?.EnterExclusive(pageNumber); try @@ -302,6 +336,8 @@ public Transaction BeginTransaction() if (_active is not null) throw new InvalidOperationException("A transaction is already in progress."); _overlay.Clear(); + _commitBaselines.Clear(); + _schemaDirty = false; _txPageCount = PageCount; // committed count at start (PageCount is still file-based while _active is null) return _active = new Transaction(_txPageCount); } @@ -318,10 +354,71 @@ public void CommitTransaction(bool flush = true) int[] pages = _overlay.Keys.ToArray(); Array.Sort(pages); - _active = null; // clear first so WriteThrough takes the committed path and PageCount reverts to the file - foreach (int page in pages) - WriteThrough(page, _overlay[page]); - _overlay.Clear(); + _cache.PublishLocked(() => + { + foreach (int page in pages) + { + byte[]? current = ReadCommittedPageOrNull(page); + byte[]? baseline = _commitBaselines[page]; + if (!SamePage(baseline, current)) + throw new InvalidOperationException( + $"Transaction write conflict on page {page}: another connection committed a change to this page."); + } + + // Keep the transaction open until every page has published. If a later page fails, restore the + // already-published prefix from its validated committed baselines so the caller can still roll back. + var published = new List(pages.Length); + try + { + foreach (int page in pages) + { + WriteThroughUnderPublishLock(page, _overlay[page]); + published.Add(page); + } + } + catch (Exception publishFailure) + { + // The restore writes to the same file that just failed to accept a write, so they can fail + // too — and if one does, the original cause must not be swallowed by the cleanup's exception. + // Collect both: the caller needs the publish failure to know why the commit failed, and the + // restore failure to know the file was left mid-publish rather than rolled back. + List? restoreFailures = null; + for (int i = published.Count - 1; i >= 0; i--) + { + int page = published[i]; + try + { + if (_commitBaselines[page] is { } baseline) + { + WriteThroughUnderPublishLock(page, baseline); + } + else + { + // A null baseline is a transaction-allocated tail page. Validation proved no other + // writer had claimed it, and the publish gate excludes one while we truncate it again. + _stream.SetLength((long)page * PageSize); + _cache.Remove(page); + } + } + catch (Exception restoreFailure) + { + (restoreFailures ??= []).Add( + new IOException($"Could not restore page {page} after a failed commit publication.", restoreFailure)); + } + } + + if (restoreFailures is null) throw; + throw new AggregateException( + "A commit publication failed and the already-published pages could not all be restored; " + + "the file is left mid-publish.", [publishFailure, .. restoreFailures]); + } + + _active = null; + _overlay.Clear(); + _commitBaselines.Clear(); + if (_schemaDirty) _cache.MarkSchemaChanged(); + _schemaDirty = false; + }); if (flush) _stream.Flush(flushToDisk: true); } @@ -334,7 +431,43 @@ public void RollbackTransaction() { if (_active is null) return; _overlay.Clear(); + _commitBaselines.Clear(); + _schemaDirty = false; _active = null; + ResyncFormatVersion(); // a discarded format raise must not stay raised in memory + } + + /// + /// Raises the file's format version byte (page 0, 0x14) to , in place, and + /// swaps to match. Returns false — writing nothing — when the file already meets it, + /// so callers can call this unconditionally. + /// + /// + /// The write goes through rather than to the stream, so it joins the calling + /// statement's transaction overlay: the upgrade commits with the DDL that needed it, or is discarded with + /// it. Page 0 is never page-encrypted, so the write is byte-transparent even on an encrypted file. + /// Only the version byte moves. The ACE format classes above 0x02 override nothing but + /// — same page size, same offsets — so the swap changes what the + /// database reports about itself and nothing about how it is parsed. + /// + internal bool RaiseFormatVersion(byte version) + { + byte[] page0 = ReadPage(0).Span.ToArray(); + if (page0[JetFormatBase.VersionOffset] >= version) return false; + + page0[JetFormatBase.VersionOffset] = version; + WritePage(0, page0); + Format = JetFormatBase.FromVersionByte(version); + return true; + } + + /// Re-derives from the version byte now visible on page 0. Cheap, and only + /// on the rollback paths, so it costs nothing in the ordinary case. + private void ResyncFormatVersion() + { + byte onDisk = ReadPage(0).Span[JetFormatBase.VersionOffset]; + if ((byte)Format.Version != onDisk) + Format = JetFormatBase.FromVersionByte(onDisk); } /// Opens a savepoint in the current transaction; pass the handle to @@ -377,8 +510,35 @@ private void RestoreOverlay(List> before, int pageCou else _overlay[page] = image; } _txPageCount = pageCount; + foreach (int page in _commitBaselines.Keys.Where(p => !_overlay.ContainsKey(p)).ToArray()) + _commitBaselines.Remove(page); + ResyncFormatVersion(); // page 0 may have been one of the restored images + } + + private byte[]? ReadCommittedPageOrNull(int pageNumber) + { + int committedPageCount = (int)(_stream.Length / PageSize); + if (pageNumber < 0 || pageNumber >= committedPageCount) return null; + + var buffer = new byte[PageSize]; + if (_cache.TryRead(pageNumber, buffer)) return buffer; + + _locks?.EnterShared(pageNumber); + try + { + if (_cache.TryRead(pageNumber, buffer)) return buffer; + _stream.Seek((long)pageNumber * PageSize, SeekOrigin.Begin); + _stream.ReadExactly(buffer); + _codec?.DecryptPage(pageNumber, buffer); + _cache.Store(pageNumber, buffer); + return buffer; + } + finally { _locks?.ExitShared(pageNumber); } } + private static bool SamePage(byte[]? left, byte[]? right) => + left is null ? right is null : right is not null && left.AsSpan().SequenceEqual(right); + /// Retrieves a higher-layer parse of a page previously stored via /// (e.g. an index page's decoded entries), or false if none is cached. The parse is dropped automatically /// when the page is written (any channel) or evicted, so a hit is always consistent with the current bytes. diff --git a/src/LibRed/LibRed.Core/JetDatabase.cs b/src/LibRed/LibRed.Core/JetDatabase.cs index f13968fea..27ec83390 100644 --- a/src/LibRed/LibRed.Core/JetDatabase.cs +++ b/src/LibRed/LibRed.Core/JetDatabase.cs @@ -41,15 +41,21 @@ private JetDatabase(PageChannel channel) /// The database's default collation LCID (e.g. 1033 = en-US), decoded from page 0. public int DefaultCollationLcid => DefinitionPage.DefaultCollationLcid; - /// The database default sort-order version (0 = General Legacy, 1 = General), from page 0. + /// The database default sort-order version (0 = the legacy compacted table, 1 = the Access-2010 + /// NLS order), from page 0. public byte DefaultCollationVersion => DefinitionPage.DefaultCollationVersion; - /// The database's default text collating order — the source of truth for the LCID and - /// sort-order version written into new columns, in place of a hardcoded constant. Defaults to General - /// legacy (locale 1033, version 0), which is what every file LibRed currently handles uses; decoding - /// the actual value from the page-0 sort order (obfuscated region) is a follow-up, after which this - /// property would be populated from . - public Collation Collation { get; internal set; } = Collation.GeneralLegacy; + /// The database default collation's sort id (page-0 0x70) — the LCID's high word, non-zero + /// only for a Windows alternate sort order such as German Phone Book or Hungarian Technical. + public byte DefaultCollationSortId => DefinitionPage.DefaultCollationSortId; + + /// The database's default text collating order — the LCID and sort-order version written into + /// new columns. Read from the page-0 sort order, so a table created in a General (v1) database gets v1 + /// columns, as Access would create them. (This used to be hardcoded to General legacy while the page-0 + /// decode was pending; the decode landed in / + /// but this was left behind, which silently gave every new column + /// v0 weights even in a v1 database.) + public Collation Collation => DefinitionPage.Collation; /// Reads and decodes the table definition (TDEF) page at . public TableDefinitionPage ReadTableDefinition(int pageNumber) @@ -68,7 +74,7 @@ public DataPage ReadDataPage(int pageNumber) } /// Opens a database file (read-only by default). For a password-encrypted ACCDB, supply - /// — encrypted databases open read-only. + /// ; writable opens encrypt modified pages again before publishing them. public static JetDatabase Open(string path, bool readOnly = true, string? password = null) { // Coordinate page access between every handle open on this file (EF holds several connections on one @@ -91,9 +97,48 @@ public static JetDatabase Open(string path, bool readOnly = true, string? passwo /// The resolved on-disk format/version of the database. public JetFormatBase Format => _channel.Format; + /// + /// Raises the database's format version to if it is below it, and reports + /// whether it moved. Writing nothing when the file already qualifies, so callers can call it + /// unconditionally. + /// + /// + /// This is what lets a `BIGINT` or `DATETIME2` column be added to an older file: the type cannot be + /// represented below a given format, and Access's own engine responds by upgrading the file rather than + /// refusing the DDL — verified by having ACE add a Date/Time Extended column to an ACE 12 database and + /// diffing the result, which moved the version byte to 0x06 and nothing else + /// (docs/format/page-00-database.md). + /// The upgrade is one-way and there is no downgrade: a raised file cannot be opened by an Access + /// older than the new format. That is inherent to the type, not a choice here — the alternative is a file + /// whose columns Access cannot read at all. + /// It joins the caller's transaction, so it commits with the statement that needed it and is undone + /// with a statement that fails. + /// + public bool EnsureFormatAtLeast(JetVersion minimum) + { + if (Format.Version >= minimum) return false; + if (!_channel.RaiseFormatVersion((byte)minimum)) return false; + + RereadDefinitionPage(); + return true; + } + + /// Re-decodes page 0 into — after the format version moves, and + /// after a rollback that may have put it back. + private void RereadDefinitionPage() => DefinitionPage.Read(_channel.ReadPage(0), _channel.Format); + /// Whether a transaction is currently open. public bool InTransaction => _channel.InTransaction; + /// Runs a logical read without allowing a concurrent multi-page commit to publish halfway through. + /// Shared — reads on this file run concurrently with each other. + public T ReadConsistent(Func action) => _channel.ReadConsistent(action); + + /// Runs a logical write with every other reader and writer on this file excluded, so a reader + /// never observes it half-published. Writing statements must use this rather than + /// : the shared scope cannot be upgraded. + public T WriteExclusive(Func action) => _channel.WriteExclusive(action); + /// Begins a page-level transaction; writes are undoable until . public void BeginTransaction() => _channel.BeginTransaction(); @@ -110,7 +155,8 @@ public void Rollback() { if (!_channel.InTransaction) return; _channel.RollbackTransaction(); - Catalog.Invalidate(); + Catalog.Invalidate(markChanged: false); + RereadDefinitionPage(); // page 0 moves too, when a rolled-back statement raised the format version } /// Opens a savepoint within the current transaction (used to make a single statement atomic @@ -123,7 +169,8 @@ public void Rollback() public void RollbackToSavepoint(Savepoint savepoint) { _channel.RollbackToSavepoint(savepoint); - Catalog.Invalidate(); + Catalog.Invalidate(markChanged: false); + RereadDefinitionPage(); } /// Releases , merging its writes into the enclosing scope. diff --git a/src/LibRed/LibRed.Core/LibRed.Core.csproj b/src/LibRed/LibRed.Core/LibRed.Core.csproj index 3d4baea05..ce7327eb7 100644 --- a/src/LibRed/LibRed.Core/LibRed.Core.csproj +++ b/src/LibRed/LibRed.Core/LibRed.Core.csproj @@ -8,4 +8,24 @@ AnyCPU;x86;x64 + + + + + + + + + + + + + + + diff --git a/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs index bb3180168..f1171904b 100644 --- a/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs +++ b/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs @@ -1,4 +1,5 @@ using System.Buffers.Binary; +using LibRed.Catalog; using LibRed.IO; namespace LibRed.Pages; @@ -28,10 +29,20 @@ public sealed class DatabaseDefinitionPage : Page /// obfuscated sort-order field at . public int DefaultCollationLcid { get; internal set; } - /// The database default sort-order version: 0 = General Legacy, 1 = General (2010+). - /// Matches each column descriptor's byte 0x0E. + /// The database default sort-order version: 0 = the legacy compacted table, 1 = the Access-2010 + /// NLS order. Matches each column descriptor's byte 0x0E. public byte DefaultCollationVersion { get; internal set; } + /// The default collation's sort id — the LCID's high word, from 0x70. Non-zero only for a + /// Windows alternate sort order (German Phone Book, Hungarian Technical), which shares its LANGID with the + /// base locale and is distinguishable by nothing else. Matches column descriptor byte 0x0D. + public byte DefaultCollationSortId { get; internal set; } + + /// The default text collating order, assembled from the three fields of the 0x6E block. + /// This is what a column created in the database inherits. + public Collation Collation => + new((CollatingOrder)DefaultCollationLcid, DefaultCollationVersion, DefaultCollationSortId); + /// Page number of the MSysObjects TDEF (the catalog root), read from the bootstrap /// pointer at . 2 in every observed file. public int CatalogRootPage { get; internal set; } @@ -53,6 +64,7 @@ public override void Read(PageBuffer buffer, Formats.JetFormatBase format) CodePage = BinaryPrimitives.ReadUInt16LittleEndian(clear.Slice(Formats.JetFormatBase.CodePageOffset - b, 2)); DatabaseKey = BinaryPrimitives.ReadInt32LittleEndian(clear.Slice(Formats.JetFormatBase.DatabaseKeyOffset - b, 4)); DefaultCollationLcid = BinaryPrimitives.ReadUInt16LittleEndian(clear.Slice(Formats.JetFormatBase.CollationSortOrderOffset - b, 2)); + DefaultCollationSortId = clear[Formats.JetFormatBase.CollationSortIdOffset - b]; DefaultCollationVersion = clear[Formats.JetFormatBase.CollationVersionOffset - b]; CatalogRootPage = BinaryPrimitives.ReadInt32LittleEndian(clear.Slice(Formats.JetFormatBase.CatalogRootPointerOffset - b, 4)); double days = BinaryPrimitives.ReadDoubleLittleEndian(clear.Slice(Formats.JetFormatBase.CreationDateOffset - b, 8)); diff --git a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs index df9936430..600837e5f 100644 --- a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs +++ b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs @@ -294,11 +294,12 @@ private int ReadColumns(PageBuffer buffer, JetFormatBase format, int columnBlock // rows, so a survivor keeps its original variable index even though ranking would shift it. buffer.ReadUInt16(entry + format.ColumnVariableIndexOffset), numeric ? Collation.GeneralLegacy + // 0x0B..0x0E are one 32-bit LCID with the sort-order version in the top byte: LANGID, + // then the sort id at 0x0D (non-zero only for a Windows alternate sort order, e.g. + // Hungarian Technical), then the version at 0x0E (0 = legacy table, 1 = Access-2010). : new Collation((CollatingOrder)buffer.ReadUInt16(entry + format.ColumnLocaleOffset), - // The sort-order version is the HIGH byte of the 2-byte field at 0x0D — i.e. the byte at - // 0x0E (0 = General legacy, 1 = Access-2010 General). The low byte 0x0D is 0 in every file - // seen; reading it alone (as LibRed used to) hid v1. Watch 0x0D — see Collation.Version. - buffer.ReadByte(entry + format.ColumnCollationVersionOffset + 1))); + buffer.ReadByte(entry + format.ColumnCollationVersionOffset), + buffer.ReadByte(entry + format.ColumnCollationSortIdOffset))); } // Pass 2: column names, in the same order, immediately after the descriptor block. diff --git a/src/LibRed/LibRed.Core/Resources/SortKeyTableV0.bin b/src/LibRed/LibRed.Core/Resources/SortKeyTableV0.bin new file mode 100644 index 000000000..a046e389e Binary files /dev/null and b/src/LibRed/LibRed.Core/Resources/SortKeyTableV0.bin differ diff --git a/src/LibRed/LibRed.Core/Resources/SortKeyTableV1.bin b/src/LibRed/LibRed.Core/Resources/SortKeyTableV1.bin new file mode 100644 index 000000000..8b9c7c750 Binary files /dev/null and b/src/LibRed/LibRed.Core/Resources/SortKeyTableV1.bin differ diff --git a/src/LibRed/LibRed.Core/Resources/SortKeyTableV1Overrides.bin b/src/LibRed/LibRed.Core/Resources/SortKeyTableV1Overrides.bin new file mode 100644 index 000000000..7d8047b63 Binary files /dev/null and b/src/LibRed/LibRed.Core/Resources/SortKeyTableV1Overrides.bin differ diff --git a/src/LibRed/LibRed.Core/SchemaObjectExistsException.cs b/src/LibRed/LibRed.Core/SchemaObjectExistsException.cs new file mode 100644 index 000000000..40b396180 --- /dev/null +++ b/src/LibRed/LibRed.Core/SchemaObjectExistsException.cs @@ -0,0 +1,28 @@ +namespace LibRed; + +/// +/// Raised when DDL would create a schema object whose name is already taken — CREATE TABLE, CREATE VIEW, +/// SELECT ... INTO, and ALTER TABLE ... RENAME all reject a duplicate rather than writing a second catalog +/// row that shadows the existing object. The ADO layer translates it into a LibRedException (a +/// ) at the provider boundary, which is what ADO.NET callers +/// expect for a database-operation error. +/// +/// The distinct type matters for the same reason 's does: provider +/// code has to recognise this failure without reading message text. EF Core's migration lock creates its +/// lock table under a racy exists-then-create guard and relies on catching the loser's "already exists" — +/// but it catches DbException, so an untranslated escapes the +/// guard entirely and fails the migration. ACE raises an OleDbException there, so this is a case +/// where matching the ADO.NET contract is what makes LibRed behave like the engine it replaces. +/// +/// It derives from — what the engine threw before this type +/// existed — so catch (InvalidOperationException) still works. Note that this does NOT keep +/// existing Assert.Throws<InvalidOperationException> assertions passing: xUnit's +/// Assert.Throws requires an exact type match, and Assert.ThrowsAny is the one that +/// accepts derived types. Tests asserting on this path name this type directly. +/// +public sealed class SchemaObjectExistsException(string message, string objectName) + : InvalidOperationException(message) +{ + /// The name that was already taken. + public string ObjectName { get; } = objectName; +} diff --git a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs index f0db3fbd4..003b97b83 100644 --- a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs @@ -29,7 +29,7 @@ public static class DatabaseCreator /// as the raw double so the exact millisecond-precise bit pattern is preserved — the page-0 SID mask is bound /// to those exact bits (see ). public static byte[] BuildDefinitionPage( - byte version, bool isAccdb, int codePage, int collationLcid, byte collationVersion, double creationDays) + byte version, bool isAccdb, int codePage, Collation collation, double creationDays) { var page = new byte[4096]; @@ -64,9 +64,11 @@ public static byte[] BuildDefinitionPage( clear[JetFormatBase.PasswordOffset - b + i] = dateMask[i % 4]; // 0x6A fixed sentinel constant. BinaryPrimitives.WriteInt32LittleEndian(clear[(0x6A - b)..], 0x000011A6); - // 0x6E..0x71 collating sort order: LCID + version byte at 0x71. - BinaryPrimitives.WriteUInt16LittleEndian(clear[(JetFormatBase.CollationSortOrderOffset - b)..], (ushort)collationLcid); - clear[JetFormatBase.CollationVersionOffset - b] = collationVersion; + // 0x6E..0x71 collating sort order: LANGID, sort id at 0x70, version at 0x71 — a 32-bit LCID carrying + // the sort-order version in its unused top byte. Mirrors a column descriptor's 0x0B..0x0E. + BinaryPrimitives.WriteUInt16LittleEndian(clear[(JetFormatBase.CollationSortOrderOffset - b)..], (ushort)collation.Order); + clear[JetFormatBase.CollationSortIdOffset - b] = collation.SortId; + clear[JetFormatBase.CollationVersionOffset - b] = collation.Version; // 0x72..0x79 creation date (OLE double). BinaryPrimitives.WriteDoubleLittleEndian(clear[(JetFormatBase.CreationDateOffset - b)..], days); @@ -149,6 +151,57 @@ public static byte[] BuildDefinitionPage( new("szRelationship", JetDataType.Text, 510, false, ColumnId: 0, SystemFlags: Sys), ]; + + // ---- Complex-column system tables (ACE 12 / Access 2007 and later) -------------------------------------- + // + // Complex columns are Access's multi-value and attachment columns. Their registry is MSysComplexColumns, + // and each supported element type gets a flat storage table. Jet 4 (.mdb) has none of this — the feature + // arrived with ACE 12 — so these are only created from version byte 0x02 up. + // + // MSysComplexColumns is not optional even for a database that never uses a complex column: ACE consults it + // on every CREATE TABLE, and without it DDL through the OLE DB provider fails with "Cannot find table or + // constraint" (isolated in AceDdlOnLibRedDatabaseProbeTest by dropping exactly this table from a working + // DAO-created database). ACE never writes to it — it only has to resolve. + // + // Column ids are the ones the real engine assigns (creation order, which is not the alphabetical order the + // descriptors are stored in), so a byte-comparison against a DAO-created file lines up. + + private static readonly ColumnSpec[] MSysComplexColumnsColumns = + [ + new("ColumnName", JetDataType.Text, 510, false, ColumnId: 0, SystemFlags: Sys), + new("ComplexID", JetDataType.Int32, 4, true, IsAutoNumber: true, ColumnId: 4, SystemFlags: Sys), + new("ComplexTypeObjectID", JetDataType.Int32, 4, true, ColumnId: 1, SystemFlags: Sys), + new("ConceptualTableID", JetDataType.Int32, 4, true, ColumnId: 3, SystemFlags: Sys), + new("FlatTableID", JetDataType.Int32, 4, true, ColumnId: 2, SystemFlags: Sys), + ]; + + /// The flat storage tables, in the order the engine creates them. Each holds a single + /// Value column of its element type; Attachment is the exception, carrying the file metadata. + private static readonly (string Name, ColumnSpec[] Columns)[] MSysComplexTypeTables = + [ + ("MSysComplexType_UnsignedByte", [new("Value", JetDataType.Byte, 1, true, ColumnId: 0, SystemFlags: Sys)]), + ("MSysComplexType_Short", [new("Value", JetDataType.Int16, 2, true, ColumnId: 0, SystemFlags: Sys)]), + ("MSysComplexType_Long", [new("Value", JetDataType.Int32, 4, true, ColumnId: 0, SystemFlags: Sys)]), + ("MSysComplexType_IEEESingle", [new("Value", JetDataType.Single, 4, true, ColumnId: 0, SystemFlags: Sys)]), + ("MSysComplexType_IEEEDouble", [new("Value", JetDataType.Double, 8, true, ColumnId: 0, SystemFlags: Sys)]), + ("MSysComplexType_GUID", [new("Value", JetDataType.Guid, 16, true, ColumnId: 0, SystemFlags: Sys)]), + ("MSysComplexType_Decimal", [new("Value", JetDataType.FixedPoint, 9, false, ColumnId: 0, SystemFlags: Sys)]), + ("MSysComplexType_Text", [new("Value", JetDataType.Text, 510, false, ColumnId: 0, SystemFlags: Sys)]), + ("MSysComplexType_Attachment", + [ + new("FileData", JetDataType.Ole, 0, false, ColumnId: 3, SystemFlags: Sys), + new("FileFlags", JetDataType.Int32, 4, true, ColumnId: 5, SystemFlags: Sys), + new("FileName", JetDataType.Text, 510, false, ColumnId: 1, SystemFlags: Sys), + new("FileTimeStamp", JetDataType.DateTime, 8, true, ColumnId: 4, SystemFlags: Sys), + new("FileType", JetDataType.Text, 510, false, ColumnId: 2, SystemFlags: Sys), + new("FileURL", JetDataType.Memo, 0, false, ColumnId: 0, SystemFlags: Sys), + ]), + ]; + + /// MSysObjects.Flags for the complex tables, as the real engine writes them: the registry carries + /// the plain system flag, the flat storage tables an extra 0x00030000. + private const int ComplexStorageFlags = unchecked((int)0x80030000); + private const int SystemFlag = unchecked((int)0x80000000); // Per-file SID cluster. A database's on-disk 2-byte SIDs are the DEFAULT WORKGROUP's account SIDs XOR'd with @@ -179,21 +232,46 @@ public static byte[] BuildDefinitionPage( /// are added through the ordinary writers. Produces a LibRed-openable, round-trippable file (Access-level /// fidelity — the remaining system tables and the 0xE00 map — is a follow-up). /// - public static void CreateEmpty(string path, byte version = 0x02) + /// + /// The database's default text collating order, written to page 0 and inherited by every column created + /// in it. Defaults to General-Legacy (LCID 1033, version 0), which is what the engine writes; pass + /// for the order Access 2010+ offers as "General". + /// + /// Any order accepts can be created — 30 configurations, the + /// two General orders and every locale in JetLocaleTailoring, each verified by having ACE build an + /// index in the created file and agree on the keys (CreatedDatabaseCollationAccessTests). It + /// cannot be otherwise: the system-table indexes are built here, in this order, so creating a database + /// REQUIRES encoding its collation. That is why a new locale is unavailable to this method until it is + /// implemented, and why measuring one for the first time needs DAO to author the file. + /// + /// + public static void CreateEmpty(string path, byte version = 0x02, Collation? collation = null) { + Collation sortOrder = collation ?? Collation.GeneralLegacy; JetFormatBase format = JetFormatBase.FromVersionByte(version); + + // Only the ACCDB versions can be created. Page 0 is stamped "Standard ACE DB" below, and pairing that + // identifier with a Jet-4 version byte produces exactly the mismatch JetFormatBase.Detect refuses — a + // file this method wrote and could not then reopen. Jet 3 is rejected by FromVersionByte already. + if (!format.IsAccdb) + throw new NotSupportedException( + $"Cannot create a database at version 0x{version:X2} ({format.Version}): LibRed creates ACCDB " + + $"formats only (0x02 ACE 12 through 0x06 ACE 17)."); // The four core system tables live at the exact pages the page-0 bootstrap pointers name (2/3/4/5); // their usage maps follow at 6..9. Access uses those pointers to find the catalog. const int objPage = 2, acesPage = 3, queriesPage = 4, relPage = 5; - var (objTdef, objMap) = BuildSystemTable(format, MSysObjectsColumns, usageMapPage: 6); - var (acesTdef, acesMap) = BuildSystemTable(format, MSysAcesColumns, usageMapPage: 7); - var (queriesTdef, queriesMap) = BuildSystemTable(format, MSysQueriesColumns, usageMapPage: 8); - var (relTdef, relMap) = BuildSystemTable(format, MSysRelationshipsColumns, usageMapPage: 9); + // The system tables take the database collation too: Access writes v1 descriptors on MSys* in a + // General (v1) database, so anything else would be a mixed-collation file it never produces. + var (objTdef, objMap) = BuildSystemTable(format, MSysObjectsColumns, usageMapPage: 6, sortOrder); + var (acesTdef, acesMap) = BuildSystemTable(format, MSysAcesColumns, usageMapPage: 7, sortOrder); + var (queriesTdef, queriesMap) = BuildSystemTable(format, MSysQueriesColumns, usageMapPage: 8, sortOrder); + var (relTdef, relMap) = BuildSystemTable(format, MSysRelationshipsColumns, usageMapPage: 9, sortOrder); const int seedPages = 10; // page 0, page 1, 4 core TDEFs (2..5), 4 usage maps (6..9) byte[][] seed = [ - BuildDefinitionPage(version, isAccdb: true, 1252, 1033, 0, BitConverter.Int64BitsToDouble(SeedCreationDateBits)), + BuildDefinitionPage(version, format.IsAccdb, 1252, sortOrder, + BitConverter.Int64BitsToDouble(SeedCreationDateBits)), BuildFreeMapPage(format, seedPages), // page 1: global free-pages map objTdef, acesTdef, queriesTdef, relTdef, // pages 2..5: core TDEFs objMap, acesMap, queriesMap, relMap, // pages 6..9: their usage maps @@ -255,6 +333,9 @@ void Ace(int objectId, byte[] sid, int acm, bool inherit = false) db.CreateIndex("MSysRelationships", "szObject", [("szObject", false)]); db.CreateIndex("MSysRelationships", "szReferencedObject", [("szReferencedObject", false)]); + // Complex-column system tables — ACE 12 and later only (see CreateComplexSystemTables). + if (version >= 0x02) CreateComplexSystemTables(db); + // Note: MSysAccessStorage and the MSysNavPane* tables are deliberately NOT created here. Verified across // ~135 pure-DAO reference files: none of them carry those tables — Access creates them (plus the nav-pane // long SID) itself on first open. Emitting them ourselves both diverged from real DAO output and produced @@ -262,6 +343,74 @@ void Ace(int objectId, byte[] sid, int acm, bool inherit = false) // DAO: core catalog only, and Access augments on first open. } + /// + /// Creates MSysComplexColumns and the MSysComplexType_* storage tables — the complex-column + /// (multi-value / attachment) infrastructure Access 2007 / ACE 12 introduced. Jet 4 has none of it, + /// so the caller gates this on version byte 0x02 or later. + /// + /// The registry table is required even in a database that never uses a complex column: ACE consults + /// it on every CREATE TABLE, and a database without it rejects DDL through the OLE DB provider with + /// "Cannot find table or constraint". It stays empty — ACE reads it, never writes it (both facts isolated + /// in AceDdlOnLibRedDatabaseProbeTest). The storage tables are created for completeness so a + /// complex column added later has somewhere to live. + /// + /// These go through the ordinary writers, so each gets its TDEF, usage map, catalog row and index + /// roots the same way a user table does; the rows are then corrected to the system flags and owner the + /// real engine writes. Page numbers therefore follow LibRed's own allocation rather than matching a + /// DAO-created file position for position — DAO's numbering is a consequence of how it lays out the core + /// four tables' usage maps and index roots, which LibRed does differently. + /// + private static void CreateComplexSystemTables(JetDatabase db) + { + db.CreateTable("MSysComplexColumns", MSysComplexColumnsColumns); + // Index names, order and flags as the engine writes them: the ComplexID primary key first, then the + // two non-unique lookups the engine uses to find a table's complex columns. + db.CreateIndex("MSysComplexColumns", "IdxID", [("ComplexID", false)], + isUnique: true, isPrimary: true, disallowNull: true, ignoreNulls: true); + db.CreateIndex("MSysComplexColumns", "IdxConceptualTableID", [("ConceptualTableID", false)], + disallowNull: true, ignoreNulls: true); + db.CreateIndex("MSysComplexColumns", "IdxFlatTableID", [("FlatTableID", false)], + disallowNull: true, ignoreNulls: true); + MarkAsSystemTable(db, "MSysComplexColumns", SystemFlag); + + foreach ((string name, ColumnSpec[] columns) in MSysComplexTypeTables) + { + db.CreateTable(name, columns); + MarkAsSystemTable(db, name, ComplexStorageFlags); + } + } + + /// Turns a table the ordinary writers just created into a system object: the MSysObjects row gets + /// the engine's flags and owner, and the TDEF's table-type byte becomes 'S'. Creating it as a user table + /// first and correcting it reuses all the allocation, usage-map and index machinery. + private static void MarkAsSystemTable(JetDatabase db, string name, int flags) + { + TableDef definition = db.Catalog.FindTable(name) + ?? throw new InvalidOperationException($"'{name}' was not found after creating it."); + + Table msysObjects = db.OpenTable("MSysObjects"); + TableDef objectsDef = msysObjects.Definition; + int idIndex = objectsDef.FindColumn("Id")!.Index; + int flagsIndex = objectsDef.FindColumn("Flags")!.Index; + int ownerIndex = objectsDef.FindColumn("Owner")!.Index; + + foreach ((RowId rowId, object?[] values) in msysObjects.Rows().WithIds()) + { + if (values[idIndex] is not { } id || Convert.ToInt32(id) != definition.DefinitionPage) continue; + values[flagsIndex] = flags; + values[ownerIndex] = SidEngine; + msysObjects.Update(rowId, values, new HashSet { flagsIndex, ownerIndex }); + break; + } + + // TDEF table type: 'N' user -> 'S' system. + IO.PageChannel channel = msysObjects.Channel; + byte[] tdef = channel.ReadPageShared(definition.DefinitionPage).Span.ToArray(); + tdef[channel.Format.TdefTableTypeOffset] = (byte)TableType.System; + channel.WritePage(definition.DefinitionPage, tdef); + db.Catalog.Invalidate(); + } + private static void InsertCatalogRow(Table msysObjects, int id, string name, short type, int flags, int parentId = 0, byte[]? owner = null) { var values = new object?[msysObjects.Definition.Columns.Count]; @@ -307,7 +456,7 @@ private static byte[] BuildFreeMapPage(JetFormatBase format, int usedPages) /// row that stores a long value — e.g. an MSysObjects catalog row carrying an LvProp blob — /// has somewhere to record its LVAL page. private static (byte[] Tdef, byte[] UsageMap) BuildSystemTable( - JetFormatBase format, IReadOnlyList columns, int usageMapPage) + JetFormatBase format, IReadOnlyList columns, int usageMapPage, Collation collation) { var longValueCols = columns.Select((c, pos) => (c, id: c.ColumnId ?? pos)) .Where(x => x.c.Type is JetDataType.Memo or JetDataType.Ole).ToList(); @@ -315,7 +464,8 @@ private static (byte[] Tdef, byte[] UsageMap) BuildSystemTable( for (int j = 0; j < longValueCols.Count; j++) longValueSpecs.Add(new LongValueColumnSpec(longValueCols[j].id, UsedRow: 2 + 2 * j, FreeRow: 3 + 2 * j, MapPage: usageMapPage)); - byte[] tdef = TdefBuilder.Build(format, TableType.System, columns, longValueColumns: longValueSpecs).Page; + byte[] tdef = TdefBuilder.Build(format, TableType.System, columns, longValueColumns: longValueSpecs, + collation: collation).Page; tdef[format.TdefOwnedPagesOffset] = 0; WriteInt24(tdef, format.TdefOwnedPagesOffset + 1, usageMapPage); tdef[format.TdefFreePagesOffset] = 1; WriteInt24(tdef, format.TdefFreePagesOffset + 1, usageMapPage); var tdefPage = new byte[format.PageSize]; diff --git a/src/LibRed/LibRed.Core/Storage/IndexKeyDecoder.cs b/src/LibRed/LibRed.Core/Storage/IndexKeyDecoder.cs index 6fbaa54f2..3eb505f79 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexKeyDecoder.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexKeyDecoder.cs @@ -77,7 +77,7 @@ public static class IndexKeyDecoder JetDataType.Int32 => 4, JetDataType.Single => 4, JetDataType.Double or JetDataType.DateTime => 8, - JetDataType.Currency => 8, + JetDataType.Currency or JetDataType.Int64 => 8, _ => -1, }; @@ -95,6 +95,8 @@ private static object DecodeFixed(JetDataType type, Span raw, bool ascendi return (int)DecodeInteger(raw, ascending); case JetDataType.Currency: return DecodeInteger(raw, ascending) / 10000m; + case JetDataType.Int64: + return DecodeInteger(raw, ascending); case JetDataType.Single: return BitConverter.Int32BitsToSingle((int)DecodeFloatBits(raw, ascending)); diff --git a/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs b/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs index 170b8ca29..a6f4bee5c 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs @@ -1,7 +1,9 @@ using System.Buffers.Binary; using System.Globalization; +using System.Runtime.InteropServices; using LibRed.Catalog; using LibRed.Formats; +using LibRed.Storage.Types; namespace LibRed.Storage; @@ -15,16 +17,48 @@ namespace LibRed.Storage; /// 0x80 / 0xFF descending). Fixed/numeric types use the reversible transform (sign-bit flip + /// big-endian for integers; an IEEE transform for floating point); descending inverts the bytes. /// GUID keys are encoded byte-faithfully (string-order halves split by 0x09, terminated by 0x08). -/// Text uses Jet's collation; general Binary keys use the same 0x09-chunked layout for any length. +/// Text uses Jet's collation; general Binary keys — and DATETIME2, whose stored form is already +/// order-preserving — use the same 0x09-chunked layout for any length. /// public static class IndexKeyEncoder { /// Access indexes only the first 255 characters of a Memo (Long Text) value (verified vs ACE). private const int MemoKeyMaxChars = 255; - public static byte[] Encode(IReadOnlyList<(ColumnDef Column, bool Ascending)> columns, object?[] values) + /// + /// The longest index entry ACE stores verbatim. Measured: an entry of exactly 510 bytes comes back + /// byte-for-byte, and one that would be 511 comes back as 510 — the weights cut short and the last two + /// bytes replaced by a value that varies with the string (…0E0602 for one 254-character value, + /// …0EDE2A for the 255-character one). That is a truncated key plus a checksum, which is why two + /// long values never collide, and it is why LibRed cannot simply cut its own key to match. + /// + /// It caps the whole entry rather than each column: two 200-character text columns are about 404 bytes + /// of key each and ACE stores their combined entry hashed at 510. + /// + /// + private const int MaxIndexKeyBytes = 510; + + public static byte[] Encode(IReadOnlyList<(ColumnDef Column, bool Ascending)> columns, object?[] values) => + Encode(columns, values, enforceLengthLimit: true); + + /// + /// The key LibRed would build if ACE had no length limit — the input the truncation works ON. + /// + /// + /// Only the research that is trying to identify ACE's two-byte checksum wants this: recovering the + /// function means pairing what ACE stored against the full key it was derived from, and the ordinary + /// entry point refuses exactly those values. Not a way around the limit — a key this returns is longer + /// than ACE would store and must never be written to a file. + /// + internal static byte[] EncodeWithoutLengthLimit( + IReadOnlyList<(ColumnDef Column, bool Ascending)> columns, object?[] values) => + Encode(columns, values, enforceLengthLimit: false); + + private static byte[] Encode( + IReadOnlyList<(ColumnDef Column, bool Ascending)> columns, object?[] values, bool enforceLengthLimit) { var buffer = new List(); + bool anyWordSortRecord = false; for (int i = 0; i < columns.Count; i++) { @@ -54,21 +88,31 @@ public static byte[] Encode(IReadOnlyList<(ColumnDef Column, bool Ascending)> co // produces byte-for-byte the key of its 255-character prefix. if (column.Type is JetDataType.Text or JetDataType.Memo) { - // Index-key weights are only implemented for General legacy; refuse other collations up front - // rather than emit wrong bytes with the General-v0 table (e.g. a 2010+ General-v1 column, or a - // non-English locale). The collation is read per-column from the descriptor (0x0B–0x0E). + // Weights are implemented for the two General orders plus the locale tailorings in + // JetLocaleTailoring. Refuse anything else up front rather than emit wrong bytes with the + // English table — a wrong key does not fail, it silently disagrees with ACE's. The collation + // is read per-column from the descriptor (0x0B–0x0E). if (!column.Collation.IsIndexKeyEncodable) throw new NotSupportedException( $"Index key encoding for column '{column.Name}' uses collation {column.Collation.Order} " + - $"version {column.Collation.Version}, which is not implemented yet (only General legacy is)."); + $"version {column.Collation.Version}" + + (column.Collation.SortId == 0 ? "" : $" sort id {column.Collation.SortId}") + + ", which is not implemented yet."); string text = (string)value; if (column.Type == JetDataType.Memo && text.Length > MemoKeyMaxChars) text = text[..MemoKeyMaxChars]; var ascendingKey = new List { IndexKeyFlags.AscStart }; - if (!JetTextCollation.TryEncode(text, ascendingKey)) - throw new NotSupportedException($"Text index key '{text}' contains a character whose collation weight is not implemented yet."); + LocaleTailoring? tailoring = JetLocaleTailoring.For(column.Collation); + bool encoded = column.Collation.Version == Collation.GeneralVersion + ? JetTextCollationV1.TryEncode(text, ascendingKey, tailoring, out bool wordSort) + : JetTextCollation.TryEncode(text, ascendingKey, tailoring, out wordSort); + anyWordSortRecord |= wordSort; + if (!encoded) + throw new NotSupportedException( + $"Text index key '{text}' contains a character with no weight in the {column.Collation.Order} " + + $"v{column.Collation.Version} collation table."); if (ascending) { @@ -131,6 +175,20 @@ public static byte[] Encode(IReadOnlyList<(ColumnDef Column, bool Ascending)> co continue; } + // DATETIME2 keys the whole 42-byte stored value through that same chunking, rather than folding + // it to a number the way DateTime folds to its OA double — verified against ACE, which stores + // 7F <8B> 09 … over exactly the bytes on the page. It works because the encoding + // is already order-preserving: both numeric fields are zero-padded to 19 digits, so byte order is + // chronological order. Note the value's 42nd byte is a NUL (see JetTypeCodec) and lands in the key. + if (column.Type == JetDataType.DateTimeExtended) + { + EncodeBinaryChunked( + buffer, + JetTypeCodec.EncodeExtendedDateTime(Convert.ToDateTime(value, CultureInfo.InvariantCulture)), + ascending); + continue; + } + int size = FixedKeySize(column.Type); if (size <= 0) throw new NotSupportedException( @@ -143,7 +201,30 @@ public static byte[] Encode(IReadOnlyList<(ColumnDef Column, bool Ascending)> co buffer.AddRange(raw); } - return [.. buffer]; + // Past 510 bytes ACE keeps the first 508 and replaces the rest with a checksum over what it dropped, + // which is why two long values sharing a prefix still sort apart. The limit is on the WHOLE entry, + // not per column: two 200-character text columns weigh about 404 bytes each, comfortably under the + // cap individually, and ACE stores their combined entry truncated. + if (!enforceLengthLimit || buffer.Count <= MaxIndexKeyBytes) return [.. buffer]; + + // Except where the dropped bytes hold a word-sort record. That case cannot be verified even in + // principle — the record sits in the part ACE discarded, so what it actually contained is + // unobservable, and if ACE recomputes its position when truncating then the checksum's input is not + // what is reconstructed here. Refuse rather than write a key that might disagree, because a wrong + // index key is silent: ACE writes its own into the same index and a seek misses rows. + if (anyWordSortRecord) + throw new NotSupportedException( + $"These values need a {buffer.Count}-byte index key across {columns.Count} column(s), past the " + + $"{MaxIndexKeyBytes} ACE stores, and one of them contains an apostrophe or hyphen. ACE truncates " + + $"and appends a checksum, and for a discarded word-sort record that checksum is not verifiable, " + + $"so LibRed will not guess at it. Shorten the value or drop it from the index."); + + byte[] truncated = new byte[MaxIndexKeyBytes]; + buffer.CopyTo(0, truncated, 0, JetIndexKeyChecksum.KeptBytes); + ushort checksum = JetIndexKeyChecksum.Compute(CollectionsMarshal.AsSpan(buffer)[JetIndexKeyChecksum.KeptBytes..]); + truncated[JetIndexKeyChecksum.KeptBytes] = (byte)(checksum >> 8); + truncated[JetIndexKeyChecksum.KeptBytes + 1] = (byte)checksum; + return truncated; } /// @@ -154,6 +235,9 @@ public static byte[] Encode(IReadOnlyList<(ColumnDef Column, bool Ascending)> co private static void EncodeBinaryChunked(List buffer, byte[] data, bool ascending) { buffer.Add(ascending ? IndexKeyFlags.AscStart : IndexKeyFlags.DescStart); + // ACE represents an empty Binary value by the start flag alone. + if (data.Length == 0) + return; int offset = 0; do @@ -181,7 +265,9 @@ private static void EncodeBinaryChunked(List buffer, byte[] data, bool asc JetDataType.Int32 => 4, JetDataType.Single => 4, JetDataType.Double or JetDataType.DateTime => 8, - JetDataType.Currency => 8, + // Int64/BIGINT keys like Currency — both are an int64, sign bit flipped, big-endian. Its VARIABLE + // storage does not change that: this dispatch is on the type, not on where the row keeps it. + JetDataType.Currency or JetDataType.Int64 => 8, JetDataType.FixedPoint => 17, // sign byte + 16-byte big-endian magnitude _ => -1, }; @@ -199,6 +285,8 @@ private static byte[] EncodeFixed(ColumnDef column, object value) return EncodeInteger(Convert.ToInt32(value, c), 4); case JetDataType.Currency: return EncodeInteger((long)decimal.Round(Convert.ToDecimal(value, c) * 10000m), 8); + case JetDataType.Int64: // BIGINT — verified against ACE across 0, ±1, ±42 and both extremes + return EncodeInteger(Convert.ToInt64(value, c), 8); case JetDataType.Single: return EncodeFloatBits(BitConverter.SingleToInt32Bits(Convert.ToSingle(value, c)), 4); case JetDataType.Double: diff --git a/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs b/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs index 579eaf434..e051cf92e 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs @@ -46,6 +46,12 @@ public static CheckedIndexPage Read(PageChannel channel, int pageNumber, int? ex ValidatePageNumber(channel, tail, "node child-tail"); } + // The shared prefix is measured across the WHOLE entry, trailer included — not just the key. Where + // many rows share a key the trailer's leading bytes are common too (consecutive rows on one data + // page), so ACE compresses those away and the stored remainder can be as little as two bytes. Size + // limits therefore apply to the reconstructed entry, never to what is stored. + int compressed = buffer.ReadUInt16(CompressedByteCountOffset); + var ranges = new List<(int Start, int End)>(); int start = 0; for (int i = EntryMaskOffset; i < EntryDataOffset; i++) @@ -55,26 +61,34 @@ public static CheckedIndexPage Read(PageChannel channel, int pageNumber, int? ex { if ((mask & (1 << bit)) == 0) continue; int end = (i - EntryMaskOffset) * 8 + bit; - if (end - start < 4 || EntryDataOffset + end > buffer.Length) + if (EntryDataOffset + end > buffer.Length) + throw new InvalidDataException( + $"Index page {pageNumber} entry [{start}, {end}) runs past the end of the page."); + // The first entry is stored whole; every later one is the prefix plus what is stored. + int length = ranges.Count == 0 ? end - start : compressed + (end - start); + if (length < 4) throw new InvalidDataException( - $"Index page {pageNumber} entry [{start}, {end}) cannot contain its 4-byte trailer."); + $"Index page {pageNumber} entry [{start}, {end}) reconstructs to {length} bytes, " + + "too few for its 4-byte trailer."); ranges.Add((start, end)); start = end; } } - int compressed = buffer.ReadUInt16(CompressedByteCountOffset); if (ranges.Count == 0 && compressed != 0) throw new InvalidDataException($"Empty index page {pageNumber} declares a compressed prefix."); - if (ranges.Count > 0 && compressed > ranges[0].End - ranges[0].Start - 4) + if (ranges.Count > 0 && compressed > ranges[0].End - ranges[0].Start) throw new InvalidDataException( - $"Index page {pageNumber} compressed prefix {compressed} exceeds its first key."); + $"Index page {pageNumber} compressed prefix {compressed} exceeds its first entry."); + + var page = new CheckedIndexPage(buffer, type, owner, previous, next, tail, compressed, ranges); + // Node children have to be read from the RECONSTRUCTED entry, for the same reason. if (type == PageType.IntermediateIndexPage) - foreach ((_, int end) in ranges) - ValidatePageNumber(channel, ReadInt32BigEndian(buffer, EntryDataOffset + end - 4), "node child"); + foreach ((_, int child) in DecodeEntries(page)) + ValidatePageNumber(channel, child, "node child"); - return new CheckedIndexPage(buffer, type, owner, previous, next, tail, compressed, ranges); + return page; } public static int ReadInt32BigEndian(PageBuffer page, int offset) => @@ -84,18 +98,23 @@ public static int ReadInt32BigEndian(PageBuffer page, int offset) => /// entry is stored whole and its leading CompressedByteCount bytes are the prefix reapplied to every /// following entry. Yields the full key bytes and the 4-byte big-endian trailer (a leaf entry's row pointer /// or a node entry's child page). Shared by the cursor's leaf enumeration and the writer's parse so the - /// prefix rule lives in exactly one place. + /// prefix rule lives in exactly one place. + /// + /// The prefix covers the entry whole, so it can reach into the trailer — with many equal keys the + /// rows are consecutive on one data page and share the trailer's leading bytes too. Both the key and the + /// trailer are therefore taken from the reconstructed entry, never from the stored bytes. + /// public static IEnumerable<(byte[] Key, int Trailer)> DecodeEntries(CheckedIndexPage page) { byte[] prefix = []; bool first = true; foreach ((int start, int end) in page.EntryRanges) { - int trailer = ReadInt32BigEndian(page.Buffer, EntryDataOffset + end - 4); - ReadOnlySpan stored = page.Buffer.Slice(EntryDataOffset + start, end - start - 4); - byte[] key = first ? stored.ToArray() : Concat(prefix, stored); - if (first) { prefix = key[..page.CompressedByteCount]; first = false; } - yield return (key, trailer); + ReadOnlySpan stored = page.Buffer.Slice(EntryDataOffset + start, end - start); + byte[] entry = first ? stored.ToArray() : Concat(prefix, stored); + if (first) { prefix = entry[..page.CompressedByteCount]; first = false; } + int trailer = BinaryPrimitives.ReadInt32BigEndian(entry.AsSpan(entry.Length - 4)); + yield return (entry[..^4], trailer); } } diff --git a/src/LibRed/LibRed.Core/Storage/JetIndexKeyChecksum.cs b/src/LibRed/LibRed.Core/Storage/JetIndexKeyChecksum.cs new file mode 100644 index 000000000..f8ede9292 --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/JetIndexKeyChecksum.cs @@ -0,0 +1,71 @@ +namespace LibRed.Storage; + +/// +/// The two bytes ACE puts at the end of an index entry too long to store whole. +/// +/// +/// An entry of at most 510 bytes is stored as built. Past that ACE keeps the first 508 bytes and replaces the +/// rest with this value, computed over the bytes it dropped — which is why two long values that share a +/// 508-byte prefix still sort apart instead of colliding. +/// +/// Recovered by measurement, not documentation. Three tails differing in one byte showed the function is +/// affine over GF(2) (L(0xA3) ^ L(0x13) = L(0xB0) exactly), and it proved shift-invariant across 173 +/// observations, so a byte at distance d from the end contributes S^(d-1) of itself. Sweeping all +/// 65,536 polynomials in the usual framings found nothing, because the usual framing is wrong: the standard +/// reflected update is crc = (crc >> 8) ^ T[(crc ^ b) & 0xFF], passing the byte THROUGH the table, +/// while ACE computes crc = (crc >> 8) ^ T[crc & 0xFF] ^ b and injects it raw. The step operator +/// was then solved directly by Gaussian elimination over the measured contributions, and predicts all 657 of +/// them. There is no initial value and no final XOR. +/// +/// +/// Not verified where the dropped bytes contain a word-sort record. Those cannot be checked even in +/// principle: the record sits in the part ACE discarded, so what it contained is unobservable, and if ACE +/// recomputes its position when truncating then the input differs from anything reconstructable here. The +/// caller refuses those rather than guess — see . +/// +/// +internal static class JetIndexKeyChecksum +{ + /// Where the kept bytes end and the checksum begins. + public const int KeptBytes = 508; + + /// + /// The step's action on each bit. The upper eight are a plain right shift by eight, which makes the + /// operator the familiar (x >> 8) ^ T(x & 0xFF) of a table-driven CRC; the lower eight are the + /// table itself, measured from ACE. + /// + private static ReadOnlySpan StepBits => + [ + 0x0580, 0x0F80, 0x1B80, 0x3380, 0x6380, 0xC380, 0x8381, 0x0383, + 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080, + ]; + + private static readonly ushort[] Table = BuildTable(); + + private static ushort[] BuildTable() + { + var table = new ushort[256]; + for (int value = 0; value < 256; value++) + { + ushort result = 0; + for (int bit = 0; bit < 8; bit++) if ((value & (1 << bit)) != 0) result ^= StepBits[bit]; + table[value] = result; + } + return table; + } + + /// + /// The checksum over the bytes ACE dropped — everything from on. + /// + /// + /// The terminator is excluded. Running it would advance every other byte one step further, and a byte at + /// distance d contributes S^(d-1), not S^d. It is always 0x00 in any case, and a + /// linear map sends zero to zero, so it could never have contributed anything. + /// + public static ushort Compute(ReadOnlySpan discarded) + { + ushort crc = 0; + foreach (byte b in discarded[..^1]) crc = (ushort)((crc >> 8) ^ Table[crc & 0xFF] ^ b); + return crc; + } +} diff --git a/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs b/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs new file mode 100644 index 000000000..6342f5268 --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs @@ -0,0 +1,63 @@ +namespace LibRed.Storage; + +/// +/// The kana section of an index key, which both sort-order versions build identically. +/// +/// +/// A kana weighs 7F <sound> with voicing as an ordinary secondary, and the small/normal +/// distinction lives in a section of its own rather than in either weight. That is measured behaviour for +/// General Legacy (v0), and it holds byte-for-byte for General (v1) as well: ACE encodes U+304C as +/// 7F 7F0A 01 03 0101 FF 02 80 FF 80 00 under both, the same sound weights and the same section. The +/// two versions disagree about a great deal in the base table, and about kana not at all — so this is shared +/// rather than duplicated, and a fix to it necessarily reaches both. +/// +internal static class JetKanaSection +{ + /// The page byte every kana primary starts with: a kana weighs 7F <sound>. + public const byte KanaPage = 0x7F; + + /// Closes the kana section, after the FF that introduces the prolonged-mark flags. + /// Constant across hiragana, katakana, halfwidth, small and voiced forms in every string measured, so it + /// is emitted literally; what it denotes is not established. + private static ReadOnlySpan Tail => [0x02, 0x80, 0xFF, 0x80]; + + /// + /// Appends 01 01, the packed small/normal flags, the prolonged-mark flags and the closing constant. + /// Emitted whenever the string holds any kana at all, even if every one of them is a normal form. + /// + public static void Append(List output, List small, List prolonged) + { + output.Add(0x01); + output.Add(0x01); + AddFlags(output, small, marked: 0b10, unmarked: 0b11); + output.Add(0xFF); + AddFlags(output, prolonged, marked: 0b11, unmarked: 0b01); + output.AddRange(Tail); + } + + /// + /// Packs one flag per kana, three to a byte, most significant first, under a 10 marker in + /// the top two bits: 11 normal, 10 small, 00 padding. So one small kana is + /// A0, "normal small" is B8, and four kana take two bytes, the second repeating the marker. + /// Verified against ACE over all 30 combinations up to four kana. + /// + /// Nothing is emitted at all when no flag is set, which is why a lone normal kana closes straight into + /// the tail. + /// + /// + private static void AddFlags(List output, List flags, int marked, int unmarked) + { + int last = flags.LastIndexOf(true); + for (int start = 0; start <= last; start += 3) + { + int packed = 0x80; + for (int slot = 0; slot < 3; slot++) + { + int index = start + slot; + int code = index > last ? 0b00 : flags[index] ? marked : unmarked; + packed |= code << (4 - 2 * slot); + } + output.Add((byte)packed); + } + } +} diff --git a/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs b/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs new file mode 100644 index 000000000..96d50fbf0 --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs @@ -0,0 +1,433 @@ +using LibRed.Catalog; + +namespace LibRed.Storage; + +/// +/// The weights one tailoring entry contributes: the primary byte(s), and the secondary (diacritic) weight of +/// the first of them. A language letter typically takes a two-byte primary — a value from a gap in the +/// General letter table plus a sub-position ordering the letters that share that gap — and the default +/// secondary, because it is a letter in its own right rather than an accented one. Some entries carry a real +/// secondary as well (Croatian , Danish aa), and an expansion instead contributes several +/// ordinary primaries (German Phone Book's ä = a+e). +/// +internal readonly record struct TailoredWeight(byte[] Primaries, byte Secondary); + +/// +/// A locale's overrides on top of the General weights, keyed by the character or character sequence +/// they replace. A sort order other than General is General plus a small tailoring — no order measured +/// departs in more than 47 of 193 sampled characters (see +/// docs/format/page-03-04-index-btree.md §10.4). +/// +/// Uppercase keys, except where a locale disagrees with invariant casing. +/// Whether a doubled digraph is written by doubling only its first letter, so +/// ggy weighs as gy+gy rather than g+gy. Hungarian alone does this; +/// Czech, Croatian, Spanish and Danish all take the plain greedy match (cch = c+ch). +internal sealed class LocaleTailoring +{ + public LocaleTailoring( + IReadOnlyDictionary entries, + bool doublesDigraphs = false, + bool reverseDiacritics = false) + { + Entries = entries; + DoublesDigraphs = doublesDigraphs; + ReverseDiacritics = reverseDiacritics; + MaxLength = entries.Count == 0 ? 0 : entries.Keys.Max(k => k.Length); + } + + public IReadOnlyDictionary Entries { get; } + + public bool DoublesDigraphs { get; } + + /// + /// Whether the diacritic section is written from the END of the string backwards. + /// + /// + /// French sorts accents from the end of the word, so coté comes before côte where General + /// puts them the other way round. [MS-UCODEREF] calls it IsReverseDW and states the whole rule: + /// trailing diacritics are dropped from the LEFT rather than the right, and what remains is written right + /// to left. Measured against ACE, byte for byte — côté is [02 12 02 0E], trimmed to + /// [12 02 0E] and stored as 0E 02 12. + /// + /// This is the whole of French: not one tailored letter, just a reversed section. It read as + /// "unclassified" for a long time because a word with ONE accent encodes identically either way, and the + /// sample set that measured every locale against General had no two-accent word in it. + /// + /// + public bool ReverseDiacritics { get; } + + /// Longest key in , bounding how far a match may look ahead. Derived in the + /// constructor, so it cannot fall out of step with the entries it describes — as it would if this were a + /// record whose with copy kept a stale value while the dictionary grew. + public int MaxLength { get; } + + /// + /// Matches the longest entry starting at , or the doubled-digraph form when the + /// locale uses one. Lookup is by the original text before the uppercased text: storing the + /// uppercase form is what folds case, and matching the original first is what lets a locale disagree + /// with invariant casing — which Turkish does, where I is the dotless letter and i is not + /// its lowercase. + /// + /// True when the match is a doubled digraph and must be emitted twice. + public bool TryMatch( + ReadOnlySpan text, int start, out TailoredWeight weight, out int consumed, out bool repeat) + { + // Doubling is tested first, so it does not depend on whether the locale also tailors the single + // letter: "ggy" is g followed by the digraph gy, and weighs as that digraph twice. + if (DoublesDigraphs && start + 2 < text.Length && + char.ToUpperInvariant(text[start]) == char.ToUpperInvariant(text[start + 1]) && + TryLongest(text, start + 1, out weight, out consumed) && consumed >= 2) + { + consumed += 1; + repeat = true; + return true; + } + + repeat = false; + return TryLongest(text, start, out weight, out consumed); + } + + /// + /// The entry for one character, without the contraction matcher. + /// + /// + /// For the components of an expansion, which take the locale's letters but must not re-enter the + /// multi-character match: expanding a ligature could otherwise trip a digraph entry that the original + /// text never contained. + /// + public bool TryMatchSingle(char character, out TailoredWeight weight) => + Entries.TryGetValue(character.ToString(), out weight) || + Entries.TryGetValue(character.ToString().ToUpperInvariant(), out weight); + + private bool TryLongest(ReadOnlySpan text, int start, out TailoredWeight weight, out int consumed) + { + for (int length = Math.Min(MaxLength, text.Length - start); length >= 1; length--) + { + ReadOnlySpan candidate = text.Slice(start, length); + if (Entries.TryGetValue(candidate.ToString(), out weight) || + Entries.TryGetValue(candidate.ToString().ToUpperInvariant(), out weight)) + { + consumed = length; + return true; + } + } + weight = default; + consumed = 0; + return false; + } +} + +/// +/// The locale tailorings LibRed can encode. Only orders expressible with the primitives here are listed; +/// one needing reordering (Thai folds a leading vowel with the consonant it precedes in writing) is +/// still unsupported, as are Ukrainian and Macedonian — single-character tailorings, but of Cyrillic +/// characters the General v0 table does not cover at all. +/// +/// +/// Every weight here was measured from ACE: an indexed text column built by ACE inside a database carrying +/// the order, with the stored index keys read back (ContractionProbeTest, +/// LocaleFixtureCollationProbeTest) and then asserted byte-for-byte against this encoder over the +/// whole of printable ASCII, Latin-1 and Latin Extended-A (LocaleCollationAccessTests). +/// +internal static class JetLocaleTailoring +{ + private const byte DefaultSecondary = 0x02; + + /// The tailoring for a collation, or null when it has none — either because it is General + /// itself, or because LibRed cannot express it. An empty tailoring is meaningful and not the same + /// as null: it records that the order was measured to be indistinguishable from General. + public static LocaleTailoring? For(Collation collation) => Tailorings.GetValueOrDefault(collation); + + private static readonly Dictionary Tailorings = new() + { + // --- Orders measured to be indistinguishable from General; recorded on disk, but no tailoring. --- + [new Collation(CollatingOrder.Georgian, 0, SortId: 1)] = Table([]), + [new Collation(CollatingOrder.Indic, Collation.GeneralVersion)] = Table([]), + + // --- Thai: the five leading vowels contract with the consonant they precede. Built as a rule. --- + [new Collation(CollatingOrder.Thai, 0)] = Thai(), + + // --- Bosnian, Croatian and Serbian at version 1: the same order under three LCIDs. --- + // Each measures 289 values identical to General v1 and the same 47 departures, byte for byte, so one + // table serves all three. These are the FIRST version-1 tailorings: their primaries are two-byte + // (SM, AW) pairs rather than v0's single byte, which is the only thing that made the v1 encoder look + // as though it could not tailor at all. + // + // The letters land where the Croatian alphabet puts them — L 0E48, LJ 0E4A, M 0E51; D 0E1A, DŽ 0E1D, + // Đ 0E1E — so the three digraphs are contractions, exactly as in the v0 orders. + [new Collation(CollatingOrder.Croatian, Collation.GeneralVersion)] = BosnianCroatianSerbian(), + [new Collation(CollatingOrder.Bosnian, Collation.GeneralVersion)] = BosnianCroatianSerbian(), + [new Collation(CollatingOrder.Serbian, Collation.GeneralVersion)] = BosnianCroatianSerbian(), + + // --- French: not one tailored letter — General with the diacritic section REVERSED. --- + // Accents are weighed from the end of the word, so coté sorts before côte where General has it the + // other way round. [MS-UCODEREF] names this IsReverseDW; verified against ACE byte for byte. + [new Collation(CollatingOrder.French, 0)] = Table([], reverseDiacritics: true), + + // --- Spanish Modern: General plus ñ as a letter of its own, between n (0x62) and o (0x64). --- + [new Collation(CollatingOrder.SpanishModern, 0)] = Table([ + ("Ñ", [0x63, 0x04])]), + + // --- Spanish Traditional: Modern plus the two digraphs the 1994 reform dropped. --- + [new Collation(CollatingOrder.Spanish, 0)] = Table([ + ("CH", [0x4E, 0x04]), ("LL", [0x5F, 0x04]), ("Ñ", [0x63, 0x04])]), + + // --- German Phone Book: the umlauts expand to the vowel plus e, exactly as ß expands to SS. --- + [new Collation(CollatingOrder.German, 0, SortId: 1)] = Table([ + ("Ä", [0x4A, 0x51]), // a + e + ("Ö", [0x64, 0x51]), // o + e + ("Ü", [0x6F, 0x51])]), // u + e + + // --- Polish: nine letters, each into the gap after its base. --- + [new Collation(CollatingOrder.Polish, 0)] = Table([ + ("Ą", [0x4B, 0x03]), ("Ć", [0x4E, 0x02]), ("Ę", [0x52, 0x02]), + ("Ł", [0x5F, 0x05]), ("Ń", [0x63, 0x03]), ("Ó", [0x65, 0x02]), + ("Ś", [0x6C, 0x07]), ("Ź", [0x79, 0x03]), ("Ż", [0x79, 0x04])]), + + // --- Romanian Legacy: only the cedilla forms move; â and the comma-below forms keep General's. --- + [new Collation(CollatingOrder.Romanian, 0)] = Table([ + ("Ă", [0x4B, 0x07]), ("Î", [0x5A, 0x03]), + ("Ş", [0x6C, 0x08]), ("Ţ", [0x6E, 0x06])]), + + // --- Turkish: six letters, plus the dotted/dotless i, where the locale disagrees with invariant + // casing. Uppercase I is the DOTLESS letter and sorts before i; dotted İ folds onto plain i with + // no secondary at all, where General gives it 0x10. Both cases are listed explicitly so the + // original-text lookup wins before invariant uppercasing can conflate them. --- + [new Collation(CollatingOrder.Turkish, 0)] = Table([ + ("Ç", [0x4E, 0x03]), ("Ğ", [0x56, 0x02]), ("Ö", [0x65, 0x02]), + ("Ş", [0x6C, 0x07]), ("Ü", [0x70, 0x03]), + ("ı", [0x58, 0x06]), ("I", [0x58, 0x06]), + ("i", [0x59]), ("İ", [0x59]), + // The IJ ligature expands, and follows the locale's casing: uppercase onto the dotless I, + // lowercase onto the dotted i. + ("IJ", [0x58, 0x06, 0x5B]), ("ij", [0x59, 0x5B])]), + + // --- Czech: ch is a letter between h and i, not after c; and the diaeresis is retuned from the + // General 0x13 to 0x05, which is a change to the accent rather than to any letter. --- + [new Collation(CollatingOrder.Czech, 0)] = Table( + [("CH", [0x58, 0x03]), ("Č", [0x4E, 0x03]), ("Ř", [0x6A, 0x04]), ("Š", [0x6C, 0x07]), + ("Ž", [0x79, 0x05])], + [("Ä", [0x4A], 0x05), ("Ë", [0x51], 0x05), ("Ï", [0x59], 0x05), ("Ö", [0x64], 0x05), + ("Ü", [0x6F], 0x05), ("Ÿ", [0x76], 0x05), ("Ċ", [0x4D], 0x04), ("Ė", [0x51], 0x04), + ("Ġ", [0x55], 0x04), ("İ", [0x59], 0x04), ("Ŀ", [0x5E], 0x04), ("Ż", [0x78], 0x04)]), + + // --- Slovak: Czech's ch and letters, with ä and ô of its own. --- + [new Collation(CollatingOrder.Slovak, 0)] = Table( + [("CH", [0x58, 0x03]), ("Ä", [0x4B, 0x02]), ("Ô", [0x65, 0x02]), ("Č", [0x4E, 0x03]), + ("Ř", [0x6A, 0x04]), ("Š", [0x6C, 0x07]), ("Ž", [0x79, 0x05])], + [("Ë", [0x51], 0x05), ("Ï", [0x59], 0x05), ("Ö", [0x64], 0x05), ("Ü", [0x6F], 0x05), + ("Ċ", [0x4D], 0x04), ("Ė", [0x51], 0x04), ("Ġ", [0x55], 0x04), ("İ", [0x59], 0x04), + ("Ŀ", [0x5E], 0x04), ("Ÿ", [0x76], 0x05), ("Ż", [0x78], 0x04)]), + + // --- Croatian Legacy: three digraphs, and dž carries a real secondary of its own. The ligature + // characters DŽ/LJ/NJ ARE encodable here, unlike in General: Croatian makes each digraph a single + // letter, so the ligature is one weight rather than two. --- + [new Collation(CollatingOrder.Croatian, 0)] = Table( + [("LJ", [0x5F, 0x03]), ("NJ", [0x63, 0x04]), ("Ć", [0x4E, 0x03]), ("Č", [0x4E, 0x02]), + ("Đ", [0x50, 0x05]), ("Š", [0x6C, 0x07]), ("Ž", [0x79, 0x05]), + ("LJ", [0x5F, 0x03]), ("NJ", [0x63, 0x04])], + [("DŽ", [0x50, 0x04], 0x04), ("DŽ", [0x50, 0x04], 0x04), + ("Ă", [0x4A], 0x05), ("Ď", [0x4F], 0x04), ("Ĕ", [0x51], 0x05), ("Ě", [0x51], 0x04), + ("Ğ", [0x55], 0x05), ("Ĭ", [0x59], 0x05), ("Ľ", [0x5E], 0x04), ("Ň", [0x62], 0x04), + ("Ŏ", [0x64], 0x05), ("Ř", [0x69], 0x04), ("Ť", [0x6D], 0x04), ("Ŭ", [0x6F], 0x05), + // Latin Extended-B caron letters, which Croatian retunes away from General's 0x14. + ("Ǎ", [0x4A], 0x04), ("Ǐ", [0x59], 0x04), ("Ǒ", [0x64], 0x04), ("Ǔ", [0x6F], 0x04), + ("Ǧ", [0x55], 0x04), ("Ǩ", [0x5C], 0x04), ("Ǯ", [0x79, 0x02], 0x04), ("ǰ", [0x5B], 0x04)]), + + // --- Slovenian: the same letters as Croatian, at different sub-positions. --- + [new Collation(CollatingOrder.Slovenian, 0)] = Table( + [("Ć", [0x4E, 0x10]), ("Č", [0x4E, 0x0F]), ("Đ", [0x50, 0x07]), ("Ś", [0x6C, 0x08]), + ("Š", [0x6C, 0x07]), ("Ź", [0x79, 0x05]), ("Ž", [0x79, 0x04])]), + + // --- Norwegian/Danish: æ ø å after z; "aa" weighs as å with a secondary marking the spelling, and + // ä/ö are æ/ø with an umlaut while ü rides on y. --- + [new Collation(CollatingOrder.Norwegian, 0)] = Table( + [("Æ", [0x79, 0x04]), ("Ø", [0x79, 0x06]), ("Å", [0x79, 0x09])], + [("AA", [0x79, 0x09], 0x03), + ("Ä", [0x79, 0x04], 0x13), ("Ö", [0x79, 0x06], 0x13), ("Ü", [0x76], 0x7B), + ("Ő", [0x79, 0x06], 0x1B), ("Ű", [0x76], 0x1B), + ("Ǣ", [0x79, 0x04], 0x03)]), // Æ with a macron rides on the locale's own Æ + + // --- Swedish/Finnish: å ä ö after z, w is a variant of v, and ü rides on y. --- + [new Collation(CollatingOrder.SwedishFinnish, 0)] = Table( + [("Ä", [0x79, 0x07]), ("Å", [0x79, 0x05]), ("Ö", [0x79, 0x08])], + [("W", [0x71], 0x03), ("Ŵ", [0x71], 0x12), ("Ø", [0x79, 0x08], 0x1E), + ("Ü", [0x76], 0x7B), ("Ő", [0x79, 0x08], 0x1B), ("Ű", [0x76], 0x1B), + // Wynn follows w onto v's primary. Keyed lowercase deliberately: its uppercase U+01F7 is + // ignorable in General, so folding to it would lose the weight. + ("ƿ", [0x71], 0x7B)]), + + // --- Icelandic: the accented vowels are letters, and þ æ ö close the alphabet after z. --- + [new Collation(CollatingOrder.Icelandic, 0)] = Table( + [("Á", [0x4B, 0x03]), ("Æ", [0x79, 0x04]), ("É", [0x52, 0x02]), ("Í", [0x5A, 0x02]), + ("Ð", [0x50, 0x02]), ("Ó", [0x65, 0x02]), ("Ö", [0x79, 0x05]), ("Ú", [0x70, 0x02]), + ("Ý", [0x77, 0x02]), ("Þ", [0x79, 0x03])], + [("Ø", [0x79, 0x05], 0x1E), ("Ǣ", [0x79, 0x04], 0x16)]), + + // --- Estonian: the most radical of these. It rewrites the base alphabet rather than extending it — + // z moves between s and t, v moves down, and õ and ö take over the bare one-byte primaries + // General uses for v and w. --- + [new Collation(CollatingOrder.Estonian, 0)] = Table( + [("V", [0x70, 0x03]), ("Z", [0x6C, 0x07]), ("Ä", [0x72, 0x02]), ("Õ", [0x71]), + ("Ö", [0x73]), ("Ü", [0x74, 0x02]), ("Š", [0x6C, 0x06]), ("Ž", [0x6C, 0x08])], + [("W", [0x70, 0x03], 0x03), ("Ź", [0x6C, 0x07], 0x0E), ("Ż", [0x6C, 0x07], 0x10), + // 0x6C is Estonian's š/z/ž slot, so the long s cannot live there as it does in General; it + // falls back onto s with a secondary. Lowercase-only, so it is matched as itself. + ("ſ", [0x6B], 0x03)]), + + // --- Latvian: seven letters, and the widest sub-positions seen (ķ at 0x12, ņ at 0x0C). --- + [new Collation(CollatingOrder.Latvian, 0)] = Table( + [("Č", [0x4E, 0x02]), ("Ģ", [0x56, 0x02]), ("Ķ", [0x5D, 0x12]), ("Ļ", [0x5F, 0x02]), + ("Ņ", [0x63, 0x0C]), ("Š", [0x6C, 0x07]), ("Ž", [0x79, 0x03])]), + + // --- Lithuanian: y follows i rather than closing the alphabet, and the ogonek letters stay + // secondaries but at 0x0F instead of General's 0x1B. --- + [new Collation(CollatingOrder.Lithuanian, 0)] = Table( + // Fullwidth Y follows the tailoring too — a locale can retailor a fullwidth form, and Estonian + // proves the converse by leaving fullwidth V on General's weight, so these are per-locale facts. + [("Y", [0x5A, 0x02]), ("Y", [0x5A, 0x02])], + [("Ą", [0x4A], 0x0F), ("Ę", [0x51], 0x0F), ("Į", [0x59], 0x0F), ("Ų", [0x6F], 0x0F)]), + + // --- Vietnamese: nine digraphs, and p and r shift to make room. Note "gh" and "ngh" are NOT letters + // — they fall out of greedy matching as g+h and ng+h, which is what ACE stores. --- + [new Collation(CollatingOrder.Vietnamese, 0)] = Table( + [("CH", [0x4E, 0x04]), ("GI", [0x56, 0x02]), ("KH", [0x5D, 0x02]), ("NG", [0x63, 0x02]), + ("NH", [0x63, 0x03]), ("PH", [0x67, 0x03]), ("QU", [0x69]), ("TH", [0x6E, 0x02]), + ("TR", [0x6E, 0x03]), + ("P", [0x67, 0x02]), ("R", [0x6A, 0x02]), ("Â", [0x4B, 0x02]), + ("Ê", [0x52, 0x02]), ("Ô", [0x65, 0x02]), ("Ă", [0x4B, 0x03]), ("Đ", [0x50, 0x02]), + ("Ơ", [0x66]), ("Ư", [0x70, 0x02])]), // the horned vowels, in Latin Extended-B + + // --- Ukrainian and Macedonian: Cyrillic orders, and the smallest tailorings of the lot. They were + // blocked until General v0 carried the Cyrillic block at all; now they are one and two entries. + [new Collation(CollatingOrder.Ukrainian, 0)] = Table([ + ("Ь", [0x79, 0x5D])]), + [new Collation(CollatingOrder.Macedonian, 0)] = Table([ + ("Ѓ", [0x79, 0x34]), ("Ќ", [0x79, 0x4C])]), + + // --- Hungarian: the full digraph set, the only order that doubles them, plus ö and ü as letters. --- + [new Collation(CollatingOrder.Hungarian, 0)] = Table( + [("CS", [0x4E, 0x05]), ("DZ", [0x50, 0x03]), ("DZS", [0x50, 0x05]), ("GY", [0x56, 0x03]), + ("LY", [0x5F, 0x05]), ("NY", [0x63, 0x06]), ("SZ", [0x6C, 0x08]), ("TY", [0x6E, 0x06]), + ("ZS", [0x79, 0x09]), ("Ö", [0x65, 0x02]), ("Ü", [0x70, 0x03])], + [("Ő", [0x65, 0x02], 0x1B), ("Ű", [0x70, 0x03], 0x1B)], + doublesDigraphs: true), + + // --- Hungarian Technical: NOT a digraph order at all, despite the name suggesting a variant of the + // one above. It tailors 46 individual letters — plain g becomes 0x56 03, so "gy" is that g + // followed by an ordinary y rather than a contraction. --- + [new Collation(CollatingOrder.Hungarian, 0, SortId: 1)] = Table( + [("F", [0x56, 0x02]), ("G", [0x56, 0x03]), ("P", [0x67, 0x04]), ("V", [0x73]), + ("W", [0x74, 0x02]), ("Á", [0x4B, 0x02]), ("Â", [0x4B, 0x03]), ("Ä", [0x4B, 0x04]), + ("Ç", [0x4E, 0x02]), ("É", [0x52, 0x02]), ("Ë", [0x53]), ("Í", [0x5A, 0x02]), + ("Î", [0x5A, 0x03]), ("Ó", [0x65, 0x02]), ("Ô", [0x66]), ("Ö", [0x67, 0x02]), + ("Ú", [0x70, 0x02]), ("Ü", [0x70, 0x03]), ("Ý", [0x77, 0x02]), ("ß", [0x6C, 0x05]), + ("Ă", [0x4B, 0x05]), ("Ą", [0x4B, 0x06]), ("Ć", [0x4E, 0x03]), ("Č", [0x4E, 0x04]), + ("Ď", [0x50, 0x02]), ("Đ", [0x50, 0x03]), ("Ę", [0x54, 0x02]), ("Ě", [0x55]), + ("Ĺ", [0x5F, 0x02]), ("Ľ", [0x5F, 0x03]), ("Ł", [0x5F, 0x04]), ("Ń", [0x63, 0x02]), + ("Ň", [0x63, 0x03]), ("Ő", [0x67, 0x03]), ("Ŕ", [0x6A, 0x02]), ("Ř", [0x6A, 0x03]), + ("Ś", [0x6C, 0x02]), ("Ş", [0x6C, 0x03]), ("Š", [0x6C, 0x04]), ("Ţ", [0x6E, 0x02]), + ("Ť", [0x6E, 0x03]), ("Ů", [0x71]), ("Ű", [0x72, 0x02]), ("Ź", [0x79, 0x02]), + ("Ż", [0x79, 0x03]), ("Ž", [0x79, 0x04])]), + }; + + /// + /// Thai — the five leading vowels contract with the consonant they precede. + /// + /// + /// Thai writes เ แ โ ใ ไ BEFORE the consonant they are pronounced after, and collation follows + /// speech. This was long recorded as needing reordering — a device nothing else here uses, and the + /// reason Thai stayed unimplemented. Measurement says otherwise: it is an ordinary CONTRACTION, the same + /// device Croatian's lj uses. + /// + /// ACE gives the pair a SINGLE weight at the consonant's own primary plus a vowel offset — เก is + /// 7C99 where alone is 7C98, แก is 7C9A, and so on to at + /// +5. Every consonant sits on a six-wide block: itself, then a slot for each leading vowel. And it is + /// genuinely a contraction rather than a swap, because the reverse order does not collide with it — + /// กเ stays two weights, 7C98 7C93. + /// + /// + /// Built as the rule rather than as 220 transcribed entries, with each consonant's primary read from the + /// measured v0 table, so there is no hand-copied hex to get wrong. + /// + /// + private static LocaleTailoring Thai() + { + const char firstConsonant = 'ก', lastConsonant = 'ฮ', firstLeadingVowel = 'เ'; + var table = new Dictionary(StringComparer.Ordinal); + + for (char consonant = firstConsonant; consonant <= lastConsonant; consonant++) + { + if (!JetTextCollationTableV0.TryGet(consonant, out TailoredWeight? weight) || weight is null) continue; + for (int offset = 1; offset <= 5; offset++) + { + byte[] primaries = [.. weight.Value.Primaries]; + primaries[^1] = (byte)(primaries[^1] + offset); + table[$"{(char)(firstLeadingVowel + offset - 1)}{consonant}"] = + new TailoredWeight(primaries, weight.Value.Secondary); + } + } + + return new LocaleTailoring(table); + } + + /// + /// Bosnian, Croatian and Serbian at sort-order version 1 — one table, three LCIDs. + /// + /// + /// Generated from ACE rather than transcribed (V1TailoringProbeTest): hand-copying hex is exactly + /// the work that introduces a wrong byte nobody notices, because a wrong index key does not fail — it + /// silently disagrees with ACE. + /// + /// Seven letters of their own, three of them digraphs, and thirteen secondary retunes. The retuned + /// letters are not in the alphabet at all: they are the caron and breve forms, which these orders weigh + /// 04 and 05 where General gives 14 and 15. The same shape as Czech's + /// diaeresis retune in version 0. + /// + /// + /// U+016D is the exception, and it is ACE's, not a mistake here. Every other letter weighs the + /// same in both cases — including all three digraphs in all three of their forms, and + /// and . But Ŭ takes the retuned 05 while lowercase ŭ keeps + /// General's 15, identically in all three locales. It needs an entry of its own because matching + /// tries the original text before the uppercased text, so without one the uppercase entry would claim it. + /// + /// + private static LocaleTailoring BosnianCroatianSerbian() => Table( + [("Ć", [0x0E, 0x0C]), ("Č", [0x0E, 0x0B]), ("Đ", [0x0E, 0x1E]), + ("Š", [0x0E, 0x97]), ("Ž", [0x0E, 0xAD]), + ("LJ", [0x0E, 0x4A]), ("NJ", [0x0E, 0x73])], + [("DŽ", [0x0E, 0x1D], 0x04), + // The caron and breve retunes. Generated from the whole guarded range, not a list of letters that + // seemed likely: a point list produced these thirteen and missed the eight below it, which are just + // as much a part of the rule. + ("Ă", [0x0E, 0x02], 0x05), ("Ď", [0x0E, 0x1A], 0x04), ("Ĕ", [0x0E, 0x21], 0x05), + ("Ě", [0x0E, 0x21], 0x04), ("Ğ", [0x0E, 0x25], 0x05), ("Ĭ", [0x0E, 0x32], 0x05), + ("Ľ", [0x0E, 0x48], 0x04), ("Ň", [0x0E, 0x70], 0x04), ("Ŏ", [0x0E, 0x7C], 0x05), + ("Ř", [0x0E, 0x8A], 0x04), ("Ť", [0x0E, 0x99], 0x04), ("Ŭ", [0x0E, 0x9F], 0x05), + ("ŭ", [0x0E, 0x9F], 0x15), + ("Ǎ", [0x0E, 0x02], 0x04), ("Ǐ", [0x0E, 0x32], 0x04), ("Ǒ", [0x0E, 0x7C], 0x04), + ("Ǔ", [0x0E, 0x9F], 0x04), ("Ǧ", [0x0E, 0x25], 0x04), ("Ǩ", [0x0E, 0x36], 0x04), + // Ezh with caron carries a primary of its own (0EAA), not the base letter's — the one retune here + // that moves a character rather than only its accent. + ("Ǯ", [0x0E, 0xAA], 0x04), + // J with caron has no uppercase form, so it is keyed as itself. + ("ǰ", [0x0E, 0x35], 0x04)]); + + /// Builds a tailoring. take the default secondary — they are letters + /// in their own right; carry one of their own, which is how a locale retunes + /// a diacritic (Czech moves the diaeresis from 0x13 to 0x05) or marks a spelling (Danish + /// aa is å with secondary 0x03). + private static LocaleTailoring Table( + (string Text, byte[] Primaries)[] letters, + (string Text, byte[] Primaries, byte Secondary)[]? accented = null, + bool doublesDigraphs = false, + bool reverseDiacritics = false) + { + var table = new Dictionary(StringComparer.Ordinal); + foreach ((string text, byte[] primaries) in letters) + table[text] = new TailoredWeight(primaries, DefaultSecondary); + foreach ((string text, byte[] primaries, byte secondary) in accented ?? []) + table[text] = new TailoredWeight(primaries, secondary); + return new LocaleTailoring(table, doublesDigraphs, reverseDiacritics); + } +} diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs index 823fe4e3d..7a331d984 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs @@ -28,6 +28,40 @@ internal static class JetTextCollation private const byte InlineMid = 0x06; private const byte ApostropheCode = 0x80; private const byte HyphenCode = 0x82; + private const byte SoftHyphenCode = 0x83; + + /// + /// The word-sort ignorables and their inline codes. These add no primary weight at all; each + /// appends a 80 <pos> 06 <code> record to the trailing section instead, which is what + /// keeps coop and co-op together. Every dash, the Arabic harakat and the fullwidth + /// apostrophe and hyphen are treated the same way — the fullwidth pair share their ASCII counterparts' + /// codes exactly. + /// + /// Written as code points rather than literals: several of these are invisible or are the very + /// characters an editor normalises, and a wrong one here is a silently wrong key. + private static readonly Dictionary Ignorables = new() + { + [(char)0x0027] = ApostropheCode, // ' + [(char)0xFF07] = ApostropheCode, // fullwidth ' + [(char)0x002D] = HyphenCode, // - + [(char)0xFF0D] = HyphenCode, // fullwidth - + [(char)0x00AD] = SoftHyphenCode, // soft hyphen + [(char)0x2010] = 0x84, // hyphen + [(char)0x2011] = 0x85, // non-breaking hyphen + [(char)0x2027] = 0x86, // hyphenation point + [(char)0x2043] = 0x87, // hyphen bullet + [(char)0x2012] = 0x88, // figure dash + [(char)0x2013] = 0x89, // en dash + [(char)0x2014] = 0x8B, // em dash + [(char)0x2015] = 0x8C, // horizontal bar + [(char)0x064B] = 0xA0, // Arabic fathatan + [(char)0x064C] = 0xA1, // dammatan + [(char)0x064D] = 0xA2, // kasratan + [(char)0x064E] = 0xA3, // fatha + [(char)0x064F] = 0xA4, // damma + [(char)0x0650] = 0xA5, // kasra + [(char)0x0652] = 0xA6, // sukun + }; private const byte DefaultSecondary = 0x02; // a character with no accent // Secondary (diacritic) weight per Unicode combining mark — depends only on the accent, not the base @@ -36,11 +70,17 @@ internal static class JetTextCollation { ['́'] = 0x0E, // acute ['̀'] = 0x0F, // grave + ['̇'] = 0x10, // dot above ['̂'] = 0x12, // circumflex ['̈'] = 0x13, // diaeresis / umlaut + ['̌'] = 0x14, // caron / háček + ['̆'] = 0x15, // breve + ['̄'] = 0x17, // macron ['̃'] = 0x19, // tilde ['̊'] = 0x1A, // ring above + ['̨'] = 0x1B, // ogonek ['̧'] = 0x1C, // cedilla + ['̋'] = 0x1D, // double acute }; // Atomic accented letters that have no Unicode canonical decomposition: base letter + secondary weight. @@ -49,6 +89,35 @@ internal static class JetTextCollation { ['Ø'] = ('O', 0x21), ['Ð'] = ('D', 0x68), + // A stroke through the letter is its own diacritic weight — 0x1E on D and H, 0x1F on L. + ['Đ'] = ('D', 0x1E), + ['Ħ'] = ('H', 0x1E), + ['Ł'] = ('L', 0x1F), + ['Ŀ'] = ('L', 0x11), // L with middle dot + ['ĸ'] = ('K', 0x03), // kra; has no uppercase, so it is matched as itself + ['ʼn'] = ('N', 0x48), // n preceded by apostrophe; likewise has no uppercase + // Ordinal indicators: the base letter's primary with a distinguishing secondary, so they sort beside + // 'a'/'o' rather than with the symbols. Harvested from ACE (7F 4A 01 03 00 / 7F 64 01 03 00). + ['ª'] = ('A', 0x03), + ['º'] = ('O', 0x03), + }; + + // Letters that are NOT an A–Z fold: they carry a primary of their own. Looked up by the original + // character, because invariant uppercasing would send them to the base letter and lose the distinction. + private static readonly Dictionary ExtraLetters = new() + { + // U+017F LATIN SMALL LETTER LONG S. ACE gives it its own two-byte primary in the S–T gap rather + // than folding it onto 's' — verified against ACE in every v0 order, General included. + ['ſ'] = new([0x6C, 0x06], DefaultSecondary), + // U+0131 DOTLESS I: the letter i's primary with a secondary of its own. It has to be matched on the + // original character, because invariant uppercasing turns it into a plain 'I'. + ['ı'] = new([0x59], 0x03), + // U+014A ENG, in the N–O gap. + ['Ŋ'] = new([0x63, 0x05], DefaultSecondary), + // U+0166 T WITH STROKE: a two-byte primary that also carries the stroke as a secondary. + ['Ŧ'] = new([0x6E, 0x06], 0x1E), + // U+00A0 NO-BREAK SPACE: a two-byte primary rather than the ordinary space's 0x07. + [' '] = new([0x08, 0x02], DefaultSecondary), }; // Letters that sort as a multi-letter expansion (each expanded letter weighs its normal primary, no @@ -58,6 +127,8 @@ internal static class JetTextCollation ['Æ'] = "AE", ['ß'] = "SS", ['Þ'] = "TH", + ['IJ'] = "IJ", + ['Œ'] = "OE", }; // Primary weight for 'A'..'Z' (general collation; mostly +2 with a few +1 steps). @@ -77,39 +148,182 @@ internal static class JetTextCollation ['<'] = [0x2E], ['='] = [0x30], ['>'] = [0x32], ['^'] = [0x2B, 0x02], ['_'] = [0x2B, 0x03], ['`'] = [0x2B, 0x07], ['{'] = [0x2B, 0x09], ['|'] = [0x2B, 0x0B], ['}'] = [0x2B, 0x0D], ['~'] = [0x2B, 0x0F], + + // Latin-1 punctuation and symbols, harvested from ACE's own index keys (see + // SortKeyComparisonProbeTest). Each group mirrors the order of the corresponding Win32 NLS primaries + // in ACE's compacted one-byte-per-group numbering: + // 0x2B continues the ^_`{|}~ group NLS 0x0751..0x0757 + ['¡'] = [0x2B, 0x10], ['¦'] = [0x2B, 0x11], ['¨'] = [0x2B, 0x12], ['¯'] = [0x2B, 0x13], + ['´'] = [0x2B, 0x14], ['¸'] = [0x2B, 0x15], ['¿'] = [0x2B, 0x16], + // 0x33 mathematical NLS 0x0817..0x081D (both skip the same slots) + ['±'] = [0x33, 0x04], ['«'] = [0x33, 0x05], ['»'] = [0x33, 0x07], + ['×'] = [0x33, 0x09], ['÷'] = [0x33, 0x0A], + // 0x34 currency then symbols — ACE runs two NLS groups (0x0797.. and 0x0A06..) into one + ['¢'] = [0x34, 0xA6], ['£'] = [0x34, 0xA7], ['¤'] = [0x34, 0xA8], ['¥'] = [0x34, 0xA9], + ['§'] = [0x34, 0xAA], ['©'] = [0x34, 0xAB], ['¬'] = [0x34, 0xAC], ['®'] = [0x34, 0xAD], + ['°'] = [0x34, 0xAE], ['µ'] = [0x34, 0xAF], ['¶'] = [0x34, 0xB0], ['·'] = [0x34, 0xB1], + // 0x37 fractions NLS 0x0D0D/0x0D11/0x0D15 (step 4 in both) + ['¼'] = [0x37, 0x12], ['½'] = [0x37, 0x16], ['¾'] = [0x37, 0x1A], + // Superscript digits take the *same* primary as their base digit and no distinguishing secondary, so + // ACE sorts (and compares) '¹' equal to '1'. Verified: both encode to 7F 38 01 00. + ['¹'] = [0x38], ['²'] = [0x3A], ['³'] = [0x3C], }; + /// + /// Ligature characters, which ACE weighs as their decomposition rather than as anything of their own — + /// DŽ encodes exactly as the string , and Ǣ exactly as ĀĒ, so its macron + /// lands on both letters and the key carries two secondary slots. Keyed by the UPPERCASE form, which is + /// what case folding leaves; the title-case and lower-case forms encode identically. + /// + /// Written as code points rather than literals: these are exactly the characters an editor or a + /// tool is liable to normalise into something else, and a wrong one here is a silently wrong key. + private static readonly Dictionary Ligatures = BuildLigatures(); + + private static Dictionary BuildLigatures() + { + var ligatures = new Dictionary(); + void Add(int ligature, params int[] components) + { + string decomposition = new([.. components.Select(component => (char)component)]); + // The upper/title/lower trio all fold to the same key, so all three map to the same components. + for (int form = ligature; form < ligature + 3; form++) ligatures[(char)form] = decomposition; + } + + Add(0x01C4, 0x0044, 0x017D); // DŽ Dž dž = D Ž + Add(0x01C7, 0x004C, 0x004A); // LJ Lj lj = L J + Add(0x01CA, 0x004E, 0x004A); // NJ Nj nj = N J + Add(0x01F1, 0x0044, 0x005A); // DZ Dz dz = D Z + ligatures[(char)0x01E2] = new([(char)0x0100, (char)0x0112]); // Ǣ = Ā Ē (macron on both) + ligatures[(char)0x01FC] = new([(char)0x00C1, (char)0x00C9]); // Ǽ = Á É (acute on both) + return ligatures; + } + /// /// Appends the order-preserving collation key body for (everything /// after the start flag: primary weights, end-of-primary marker, any ignorable-char inline /// codes, and the terminator). Trailing spaces are dropped. Returns false if any character is /// not yet supported. /// - public static bool TryEncode(string value, List output) + /// Per-character overrides for a locale order other than General; null for + /// General itself. See . + public static bool TryEncode(string value, List output, LocaleTailoring? tailoring = null) => + TryEncode(value, output, tailoring, out _); + + /// + /// Whether the key carries an inline word-sort section. The caller needs this to decide whether an + /// over-long entry may be truncated: the checksum that replaces the dropped bytes is unverified when + /// those bytes hold such a record, because the record is precisely what cannot be observed. + /// + /// + public static bool TryEncode( + string value, List output, LocaleTailoring? tailoring, out bool hasWordSortRecord) { + hasWordSortRecord = false; ReadOnlySpan s = value.AsSpan().TrimEnd(' '); // Build the primary weight bytes and a parallel secondary weight per byte (0x02 = no accent). var primaries = new List(); var secondaries = new List(); - // Apostrophe/hyphen carry no primary weight; they record (position, code) for the inline - // section, where position is the count of **primary weight bytes** emitted before them (so a - // multi-byte expansion like ß→SS counts as 2 — verified against ACE). + // Apostrophe/hyphen carry no primary weight; they record (position, code) for the inline section, + // where position is the count of primary **WEIGHTS** emitted before them — not bytes. A two-byte + // weight counts once: ACE puts the hyphen of "£-" at 0x0B (0x07 + 4x1) though £ is 34 A7, while + // "ß-" is 0x0F because ß expands to two one-byte weights. Latin-only strings cannot tell the two + // rules apart, which is why this read as "bytes" for so long. secondaries.Count is the weight count, + // since every weight contributes exactly one secondary slot. var inline = new List<(int Position, byte Code)>(); + // One entry per kana in the string: true for a small form. Emitted as a section of its own. + var kana = new List(); + // True where that kana is a prolonged sound mark. Packed like the small flags but with its own + // codes, into a section of its own. + var prolonged = new List(); + // Index of the weight the last kana produced, so a following halfwidth voicing mark can reach it, + // together with the vowel and small flag a following prolonged mark inherits. + int kanaWeight = -1; + byte kanaVowel = 0; + bool kanaSmall = false; - foreach (char c in s) + // Indexed rather than foreach, because a tailoring entry can consume several characters: a + // contraction is a digraph weighing as one letter (Czech "ch", Hungarian "gy", Danish "aa"). + for (int position = 0; position < s.Length; position++) { + char c = s[position]; char u = char.ToUpperInvariant(c); - if (u == '\'') { inline.Add((primaries.Count, ApostropheCode)); continue; } - if (u == '-') { inline.Add((primaries.Count, HyphenCode)); continue; } - - if (u is >= 'A' and <= 'Z') - Add(Letters[u - 'A']); - else if (u is >= '0' and <= '9') - Add((byte)(0x36 + 2 * (u - '0'))); - else if (Symbols.TryGetValue(u, out byte[]? weights)) - foreach (byte w in weights) Add(w); - else if (!TryAddAccented(u, Add)) + + // Astral characters are wholly ignorable in v0 — ACE stores the empty key 7F 01 00 for every one, + // measured across all of planes 1 and 2 and sampled across all sixteen. Not a gap in the table but + // the order's actual behaviour, and the opposite of v1, which weighs them by their low surrogate. + // Neither half contributes, so both are skipped and an astral character vanishes from the key. + if (char.IsSurrogate(c)) continue; + + // The hand-verified ignorables first, then the measured ones — 296 across the BMP, every dash and + // quotation form, the Arabic harakat, and the CJK and fullwidth punctuation. + if (Ignorables.TryGetValue(c, out byte code) || + JetTextCollationTableV0.TryGetInlineCode(c, out code)) + { + inline.Add((secondaries.Count, code)); + continue; + } + + // Kana take the two-byte primary 7F , with voicing as an ordinary secondary and the + // small/normal distinction recorded in a section of their own. Handled here rather than in + // WeighCharacter because a single kana changes the shape of the WHOLE key. + // The prolonged sound mark lengthens the preceding kana's VOWEL, so it takes that vowel's + // primary — がー is 7F 0A then 7F 02, "ga" lengthened by "a" — and inherits its small flag, + // while marking itself in a second packed section. With no kana ahead of it there is nothing to + // lengthen, and it stays the ordinary FF FF primary the table already holds. + if (c is (char)0x30FC or (char)0xFF70 && kanaVowel != 0 && kanaWeight == secondaries.Count - 1) + { + AddWeight([JetKanaSection.KanaPage,kanaVowel], DefaultSecondary); + kana.Add(kanaSmall); + prolonged.Add(true); + kanaWeight = secondaries.Count - 1; + continue; + } + + if (JetTextCollationTableV0.TryGetKana(c, out byte sound, out byte voicing, out bool small, + out byte vowel)) + { + AddWeight([JetKanaSection.KanaPage,sound], voicing); + kana.Add(small); + prolonged.Add(false); + kanaWeight = secondaries.Count - 1; + kanaVowel = vowel; + kanaSmall = small; + continue; + } + + // The halfwidth voicing marks are COMBINING: ACE folds them into the preceding kana's secondary + // rather than weighing them. ニホンゴ is four primaries with secondaries 02 02 02 03 — the コ voiced + // by the ゙ that follows it. Measured alone the marks look ignorable, which is what hid this. + // With no kana immediately before it there is nothing to voice, and the mark falls through to be + // weighed on its own — ACE stores it as ignorable. Both halves of the guard matter: for a lone + // mark kanaWeight and Count-1 are each -1, which would otherwise pass and index the list at -1. + if (c is (char)0xFF9E or (char)0xFF9F && kanaWeight >= 0 && kanaWeight == secondaries.Count - 1) + { + secondaries[kanaWeight] = c == (char)0xFF9E ? (byte)0x03 : (byte)0x04; + continue; + } + + // A locale tailoring overrides everything below it. + if (tailoring is not null && + tailoring.TryMatch(s, position, out TailoredWeight tailored, out int consumed, out bool repeat)) + { + for (int emit = repeat ? 2 : 1; emit > 0; emit--) + AddWeight(tailored.Primaries, tailored.Secondary); + position += consumed - 1; + } + // A ligature character weighs as its decomposition, one component at a time — ACE stores DŽ + // exactly as it stores the string "DŽ", and Ǣ exactly as "ĀĒ" (A and E each carrying the macron, + // hence two secondary slots). The components are weighed INDIVIDUALLY, never re-entering the + // contraction matcher: expand DZ in a Hungarian database and its "dz" digraph would otherwise + // fire, giving 50 03 where ACE stores 4F 78. And this sits below the tailoring, because some + // locales do not decompose at all — Icelandic's Ǣ is its own Æ plus a secondary. + else if (Ligatures.TryGetValue(u, out string? components)) + { + foreach (char component in components) + if (!WeighCharacter(component)) return false; + } + else if (!WeighCharacter(c)) return false; // not handled yet } @@ -118,20 +332,56 @@ public static bool TryEncode(string value, List output) // Secondary (diacritic) section: only emitted when a character carries a non-default weight; it lists // the secondary weight of every byte from the first up to and including the last accented one. - int lastAccent = secondaries.FindLastIndex(w => w != DefaultSecondary); - for (int i = 0; i <= lastAccent; i++) - output.Add(secondaries[i]); + // + // A French-style order writes it BACKWARDS, so accents are weighed from the end of the word and coté + // sorts before côte. The trimming mirrors too — the run of defaults comes off the LEFT, because that + // is the end that becomes trailing once reversed. [MS-UCODEREF] calls the flag IsReverseDW and states + // both halves; verified against ACE, where côté is [02 12 02 0E] trimmed to [12 02 0E] and stored + // 0E 02 12. + if (tailoring?.ReverseDiacritics == true) + { + int firstAccent = secondaries.FindIndex(w => w != DefaultSecondary); + if (firstAccent >= 0) + for (int i = secondaries.Count - 1; i >= firstAccent; i--) + output.Add(secondaries[i]); + } + else + { + int lastAccent = secondaries.FindLastIndex(w => w != DefaultSecondary); + for (int i = 0; i <= lastAccent; i++) + output.Add(secondaries[i]); + } + + hasWordSortRecord = inline.Count > 0; + + if (kana.Count > 0) JetKanaSection.Append(output, kana, prolonged); - // Apostrophe/hyphen inline (tertiary) section. + // Apostrophe/hyphen inline (tertiary) section. Its introducer depends on whether a kana section came + // first: 01 01 01 on its own, but FF 01 after one — measured from "あ-" and "-あ". if (inline.Count > 0) { - output.Add(0x01); - output.Add(0x01); - output.Add(0x01); + if (kana.Count > 0) + { + output.Add(0xFF); + output.Add(0x01); + } + else + { + output.Add(0x01); + output.Add(0x01); + output.Add(0x01); + } foreach (var (position, code) in inline) { - output.Add(InlineStart); - output.Add((byte)(0x07 + 4 * position)); + // [MS-UCODEREF] SpecialWeightType is (Position: 16 bit integer, ScriptMember, PrimaryWeight), + // and its Position is emitted big-endian — "Byte1 = Position >> 8, Byte2 = Position & 0xff" — + // so this is ONE sixteen-bit field with bit 15 set, not a 0x80 marker followed by a byte. + // Both readings give the same bytes below 0x100 and only the field reading survives past it, + // which is why treating 0x80 as a marker looked right for every short value and silently + // produced a wrong key for anything longer. Measured: a hyphen at character 250 is 83 EF. + int position16 = InlineStart << 8 | (0x07 + 4 * position); + output.Add((byte)(position16 >> 8)); + output.Add((byte)position16); output.Add(InlineMid); output.Add(code); } @@ -139,17 +389,95 @@ public static bool TryEncode(string value, List output) output.Add(EndKey); return true; + // One character's weights, with no contraction and no tailoring: the path a ligature's components + // take, and the tail of the ordinary path once a tailoring has declined the character. + bool WeighCharacter(char character) + { + char upper = char.ToUpperInvariant(character); + // The locale still applies to a single character — only the contraction matcher is bypassed. + // A ligature's components take the locale's letters: Slovenian's DŽ is D plus SLOVENIAN's ž. + if (tailoring is not null && + (tailoring.Entries.TryGetValue(character.ToString(), out TailoredWeight tailoredOne) || + tailoring.Entries.TryGetValue(upper.ToString(), out tailoredOne))) + AddWeight(tailoredOne.Primaries, tailoredOne.Secondary); + else if (ExtraLetters.TryGetValue(character, out TailoredWeight own) || + ExtraLetters.TryGetValue(upper, out own)) + AddWeight(own.Primaries, own.Secondary); + else if (upper is >= 'A' and <= 'Z') + Add(Letters[upper - 'A']); + else if (upper is >= '0' and <= '9') + Add((byte)(0x36 + 2 * (upper - '0'))); + // Look the symbol up by the original character as well as the uppercased one: uppercasing is for + // letters, and it corrupts some symbols — char.ToUpperInvariant('µ') is GREEK CAPITAL LETTER MU, + // which is not what ACE weighs it as (ACE gives it a symbol weight in the 0x34 group). + else if (Symbols.TryGetValue(character, out byte[]? weights) || Symbols.TryGetValue(upper, out weights)) + AddWeight(weights, DefaultSecondary); + // Explicit hand-verified expansions and atomic accents come BEFORE the measured table, because a + // single-character measurement cannot tell one two-byte weight from two one-byte ones. ß is two + // weights (S+S); the table records it as the two bytes 6B 6B and would make it one, which is + // invisible until something counts weights — an accent after it, or an inline record's position. + else if (TryAddExplicit(upper, Add)) + { + // handled by the expansion / atomic-accent tables + } + // The measured table for the rest of the BMP — Greek, Cyrillic, Hebrew, Arabic, the Latin + // extensions, punctuation, CJK and the rest. It covers nothing the hand-verified Latin-1 and + // Latin Extended-A tables above do, so it cannot override anything already proven — but it DOES + // take precedence over the decomposition below, which is guesswork by comparison: a measured + // weight beats a derived one. A null weight means ACE stores nothing at all for the character, + // not even a secondary slot. + // + // Locales share it. A locale CAN reweigh a character here, but measuring all 21 against General + // showed the departures are tiny — most add one or two entries across the whole range, Croatian + // eleven — and every one is listed in its tailoring, which is consulted first. + // `LocaleCollationAccessTests` asserts the whole range for every locale, so a missed departure + // fails rather than writing a silently wrong key. + else if (JetTextCollationTableV0.TryGet(character, out TailoredWeight? block)) + { + if (block is { } weight) AddWeight(weight.Primaries, weight.Secondary); + } + else if (!TryAddAccented(upper, Add)) + return false; + return true; + } + void Add(byte primary, byte secondary = DefaultSecondary) { primaries.Add(primary); secondaries.Add(secondary); } + + // A primary WEIGHT may be one or two bytes, and the secondary section has one entry per weight — + // not per byte. Measured against ACE: Norwegian "ö" is 7F 79 06 01 13 00, two primary bytes and a + // single secondary. The inline apostrophe/hyphen section counts weights too, so both sections index + // the same way; `secondaries.Count` is the weight count for both. + void AddWeight(ReadOnlySpan weight, byte secondary) + { + // A weight with NO primary carries only its secondary, and where something precedes it that + // secondary FOLDS into the one before rather than taking a slot of its own. ACE encodes ไก่ as + // two weights with secondaries 03 06 — the tone mark's 03 added to the ก's 03 — not as three + // weights. Emitting a slot desynchronises the whole section from the primaries. + // + // The same rule the version-1 encoder already had. It reached version 0 only when the Thai block + // entered the conformance range, because it needs a combining mark AFTER a base character and a + // per-character sweep never places one there. It is not a Thai rule: Hebrew niqqud, the Cyrillic + // combining marks and three Greek ones are all in this class, in every order. + if (weight.IsEmpty && secondaries.Count > 0) + { + secondaries[^1] = (byte)(secondaries[^1] + secondary); + return; + } + + foreach (byte b in weight) primaries.Add(b); + secondaries.Add(secondary); + } } - /// Emits the primary+secondary weight(s) for an accented or special Latin-1 letter (uppercased): - /// a multi-letter expansion (ß=SS, Þ=TH, Æ=AE), an atomic accent (Ø, Ð), or a Unicode canonical - /// decomposition (base letter + combining mark). Returns false if the character is unknown. - private static bool TryAddAccented(char u, Action add) + /// The explicit, hand-verified half: a multi-letter expansion (ß=SS, Þ=TH, Æ=AE) or an atomic + /// accent (Ø, Ð). Each expanded letter is its own weight — the part no single-character + /// measurement can capture, since a key cannot show whether two bytes are one weight or two — so this is + /// consulted ahead of the measured table. + private static bool TryAddExplicit(char u, Action add) { if (Expansions.TryGetValue(u, out string? expansion)) { @@ -161,6 +489,18 @@ private static bool TryAddAccented(char u, Action add) add(Letters[atomic.Base - 'A'], atomic.Secondary); return true; } + return false; + } + + /// The derived half: a Unicode canonical decomposition into a base A–Z letter plus one combining + /// mark we hold a weight for. Guesswork beside a measurement, so it runs last of all. + private static bool TryAddAccented(char u, Action add) + { + // Normalize throws on anything that is not a well-formed scalar — an unpaired surrogate, or a + // NONCHARACTER (U+FDD0..U+FDEF and any code point ending FFFE/FFFF). Those are legal in a .NET + // string, so refuse them rather than letting an ArgumentException escape a Try- method. + if (char.IsSurrogate(u) || u is >= (char)0xFDD0 and <= (char)0xFDEF || (u & 0xFFFE) == 0xFFFE) + return false; // Canonical decomposition: a base A–Z letter followed by one combining diacritic we know. string nfd = u.ToString().Normalize(System.Text.NormalizationForm.FormD); diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollationTableV0.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollationTableV0.cs new file mode 100644 index 000000000..e4a3aef10 --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollationTableV0.cs @@ -0,0 +1,158 @@ +using System.IO.Compression; + +namespace LibRed.Storage; + +/// +/// The measured General v0 weights for the whole Basic Multilingual Plane — 63,208 code points, of which +/// 19,186 are ignorable. Consulted by only after its own hand-verified +/// tables, so nothing here can change a weight that was already proven byte for byte. +/// +/// +/// v1's table could be embedded from a published Microsoft file, because its primaries are the NLS +/// weights verbatim. v0's are a Jet-specific compaction of the NT4-era order (see +/// docs/format/page-03-04-index-btree.md §10.4), so no published file describes them and the only +/// source of truth is ACE: SortKeyTableV0GeneratorTest inserts every code point into an indexed text +/// column, reads the stored index keys back, and writes this resource. +/// +/// Layout mirrors the v1 resource — an entry count, then four separately-deflated streams. Splitting them +/// matters: each column is nearly constant on its own and compresses to almost nothing, where interleaved +/// records would not. A primary length of 0xFF marks an ignorable character, which contributes +/// no primary and no secondary slot at all; a length of 0 is a secondary-only combining mark. +/// +/// +internal static class JetTextCollationTableV0 +{ + private const byte IgnorableLength = 0xFF; + + /// The weight for a character, or null when it is ignorable. False when the table has no entry, + /// in which case the caller must refuse the value rather than emit a guess. + public static bool TryGet(char c, out TailoredWeight? weight) + { + Table table = Loaded.Value; + int index = Array.BinarySearch(table.CodePoints, c); + if (index < 0) { weight = null; return false; } + if (table.Lengths[index] == IgnorableLength) { weight = null; return true; } + + int start = table.PrimaryOffsets[index]; + int length = table.Lengths[index]; + weight = new TailoredWeight(table.Primaries[start..(start + length)], table.Secondaries[index]); + return true; + } + + /// The inline code for a word-sort ignorable — a character that adds no weight at all and + /// records 80 <pos> 06 <code> in the trailing section instead. There are 296 of them + /// across the BMP: every dash and quotation form, the Arabic harakat, and the CJK and fullwidth + /// punctuation. + public static bool TryGetInlineCode(char c, out byte code) + { + Table table = Loaded.Value; + int index = Array.BinarySearch(table.InlineCodePoints, c); + code = index < 0 ? (byte)0 : table.InlineCodes[index]; + return index >= 0; + } + + /// A kana's sound index, voicing secondary and small-form flag. Kana take the two-byte primary + /// 7F <sound>; hiragana, katakana and halfwidth katakana share a sound, so they encode + /// identically. Voicing is an ordinary secondary — 03 dakuten, 04 handakuten — though a few + /// characters carry other values. Small forms are recorded in the kana section instead of the + /// primary. + /// The sound a following prolonged mark takes: lengthens the preceding + /// kana's VOWEL, not its sound, so がー is 7F 0A then 7F 02 — "ga" lengthened by + /// "a". Zero where it could not be measured, in which case a following must be refused. + public static bool TryGetKana(char c, out byte sound, out byte secondary, out bool small, out byte vowel) + { + Table table = Loaded.Value; + int index = Array.BinarySearch(table.KanaCodePoints, c); + if (index < 0) { sound = 0; secondary = 0; small = false; vowel = 0; return false; } + sound = table.KanaSounds[index]; + secondary = table.KanaSecondaries[index]; + small = table.KanaSmall[index] != 0; + vowel = table.KanaVowels[index]; + return true; + } + + private sealed record Table( + char[] CodePoints, byte[] Lengths, int[] PrimaryOffsets, byte[] Primaries, byte[] Secondaries, + char[] InlineCodePoints, byte[] InlineCodes, + char[] KanaCodePoints, byte[] KanaSounds, byte[] KanaSecondaries, byte[] KanaSmall, + byte[] KanaVowels); + + // Lazy so the cost is paid only by a database that actually reaches beyond the hand-written tables. + private static readonly Lazy Loaded = new(Load); + + private static Table Load() + { + using Stream stream = typeof(JetTextCollationTableV0).Assembly + .GetManifestResourceStream("LibRed.Resources.SortKeyTableV0.bin") + ?? throw new InvalidOperationException("The v0 sorting weight table resource is missing from the assembly."); + + var reader = new BinaryReader(stream); + int count = reader.ReadInt32(); + int inlineCount = reader.ReadInt32(); + int kanaCount = reader.ReadInt32(); + byte[] deltas = ReadStream(reader); + byte[] lengths = ReadStream(reader); + byte[] primaries = ReadStream(reader); + byte[] secondaries = ReadStream(reader); + byte[] inlineDeltas = ReadStream(reader); + byte[] inlineCodes = ReadStream(reader); + byte[] kanaDeltas = ReadStream(reader); + byte[] kanaSounds = ReadStream(reader); + byte[] kanaSecondaries = ReadStream(reader); + byte[] kanaSmall = ReadStream(reader); + byte[] kanaVowels = ReadStream(reader); + + var codePoints = new char[count]; + var offsets = new int[count]; + int codePoint = 0, cursor = 0, primaryOffset = 0; + for (int i = 0; i < count; i++) + { + codePoint += ReadVarInt(deltas, ref cursor); + codePoints[i] = (char)codePoint; + offsets[i] = primaryOffset; + if (lengths[i] != IgnorableLength) primaryOffset += lengths[i]; + } + + var inlineCodePoints = new char[inlineCount]; + codePoint = 0; + cursor = 0; + for (int i = 0; i < inlineCount; i++) + { + codePoint += ReadVarInt(inlineDeltas, ref cursor); + inlineCodePoints[i] = (char)codePoint; + } + + var kanaCodePoints = new char[kanaCount]; + codePoint = 0; + cursor = 0; + for (int i = 0; i < kanaCount; i++) + { + codePoint += ReadVarInt(kanaDeltas, ref cursor); + kanaCodePoints[i] = (char)codePoint; + } + + return new Table(codePoints, lengths, offsets, primaries, secondaries, inlineCodePoints, inlineCodes, + kanaCodePoints, kanaSounds, kanaSecondaries, kanaSmall, kanaVowels); + } + + private static byte[] ReadStream(BinaryReader reader) + { + byte[] compressed = reader.ReadBytes(reader.ReadInt32()); + var output = new MemoryStream(); + using (var inflate = new ZLibStream(new MemoryStream(compressed), CompressionMode.Decompress)) + inflate.CopyTo(output); + return output.ToArray(); + } + + private static int ReadVarInt(byte[] source, ref int offset) + { + int value = 0, shift = 0; + while (true) + { + byte b = source[offset++]; + value |= (b & 0x7F) << shift; + if ((b & 0x80) == 0) return value; + shift += 7; + } + } +} diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs new file mode 100644 index 000000000..bbefb25cb --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs @@ -0,0 +1,490 @@ +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.IO.Compression; +using System.Reflection; +using System.Text; + +namespace LibRed.Storage; + +/// +/// Index-key weights for the Access-2010+ "General" text collation (sort-order version 1). +/// +/// Unlike General-Legacy (), whose weights are a Jet-era compaction into +/// one byte per character, v1 uses the Windows NLS weights verbatim: the primary is the two-byte +/// (Script Member, Alphabetic Weight) pair and the secondary is the Diacritic Weight, exactly as +/// published in Microsoft's sorting weight tables. The Case Weight is dropped, which is why case and +/// character width both fold (a full-width and A differ only in that discarded weight). +/// +/// The table is the Windows Server 2008 one, frozen: reconstructing measured ACE v1 keys scores +/// 25/25 against it and lower against every other published version. See tools/sortkey-table/generate.ps1 +/// for provenance and how the embedded resource is built. +/// +internal static class JetTextCollationV1 +{ + private const byte EndPrimary = 0x01; + private const byte EndKey = 0x00; + private const byte InlineStart = 0x80; + private const byte DefaultSecondary = 0x02; + + // The script members below were derived by measuring ACE, and [MS-UCODEREF] "GetWindowsSortKey + // Pseudocode" names every one of them. Its constants are UNSORTABLE 0, NONSPACE_MARK 1, EXPANSION 2, + // EASTASIA_SPECIAL 3, JAMO_SPECIAL 4, EXTENSION_A 5, PUNCTUATION 6, SYMBOL_1..6 7-12, DIGIT 13, LATIN 14. + // Everything at or below MAX_SPECIAL_CASE (11 or 12, by Windows version) goes to its SpecialCaseHandler + // rather than being weighed the ordinary way — which is exactly the set of classes needing bespoke + // handling here, arrived at one measurement at a time. + + /// [MS-UCODEREF] PUNCTUATION. Characters that carry no primary weight but are recorded + /// positionally so co-op stays beside coop. The apostrophe and hyphen live here (their + /// 0x80/0x82 inline codes are simply their Alphabetic Weights), which is why exactly those + /// two are special — it is the platform's rule, not an Access one. + private const byte WordSortScriptMember = 6; + + /// [MS-UCODEREF] EXTENSION_A. The CJK ideographs, their extensions, the compatibility + /// forms and the Kangxi radicals. ACE gives every one of them a four-byte primary FD FF AW DW and + /// no secondary, rather than the ordinary (SM, AW) primary with DW as a secondary. The + /// specification's own SCRIPT_MEMBER_EXT_A is 254 and Extension B measures as FE, so the + /// FD here is a third range and stays as measured. + private const byte HanScriptMember = 5; + + /// [MS-UCODEREF] JAMO_SPECIAL. Like Han they put their weights straight into the + /// primary — (AW, DW), no secondary — but with no FD FF marker ahead of them. The composed + /// Hangul syllables are a different class and were always correct. + private const byte HangulJamoScriptMember = 4; + + /// + /// [MS-UCODEREF] EASTASIA_SPECIAL — not "kana", although kana is what reaches it here. + /// + /// + /// The class holds the East Asian characters needing special handling, and the specification gives it two + /// reserved primary weights: PW_REPEAT 0 and PW_CHO_ON 1, up to MAX_SPECIAL_PW. That + /// names something measured the hard way here — the seven characters ACE gives the unweighted + /// FF FF primary are exactly those two. The iteration marks (U+3005, U+309D, + /// U+309E, U+3031, U+3032, U+A015) carry PW_REPEAT, and the lone + /// prolonged sound mark U+FF70 carries PW_CHO_ON. They were treated as an unexplained list + /// of exceptions before this; they are one rule. + /// + private const byte EastAsiaSpecialScriptMember = 3; + + private static readonly Lazy Table = new(Load, LazyThreadSafetyMode.ExecutionAndPublication); + + /// + /// Appends the collation key body for — everything after the start flag. Returns + /// false if any character has no weight in the table (the caller reports it rather than emitting a key + /// that would sort wrongly). + /// + public static bool TryEncode(string value, List output, LocaleTailoring? tailoring = null) => + TryEncode(value, output, tailoring, out _); + + /// Per-character overrides for a version-1 locale order other than General; null + /// for General itself. The same mechanism as version 0 uses, and the same six devices — the entries just + /// carry a two-byte (Script Member, Alphabetic Weight) primary instead of v0's single byte. + /// + /// Whether the key carries an inline word-sort section. The caller needs this to decide whether an + /// over-long entry may be truncated: the checksum that replaces the dropped bytes is unverified when + /// those bytes hold such a record, because the record is precisely what cannot be observed. + /// + public static bool TryEncode( + string value, List output, LocaleTailoring? tailoring, out bool hasWordSortRecord) + { + hasWordSortRecord = false; + WeightTable table = Table.Value; + ReadOnlySpan text = value.AsSpan().TrimEnd(' '); + + var primaries = new List(); + var secondaries = new List(); + // Position is counted in primary *weights*, not bytes. In v0 the two coincide (one byte per weight); + // here a weight is two bytes, and ACE still counts weights — verified against ACE (`O'Brien` puts the + // apostrophe at 0x0B = 0x07 + 4x1 in both orders, though v1 has emitted twice as many bytes by then). + var inline = new List<(int Position, byte ScriptMember, byte AlphabeticWeight)>(); + + // The kana small/normal and prolonged-mark flags, and the running state the prolonged mark needs. + var kana = new List(); + var prolonged = new List(); + int kanaWeight = -1; + byte kanaVowel = 0; + bool kanaSmall = false; + + // Indexed rather than foreach, because a tailoring entry can consume several characters: a + // contraction is a digraph weighing as one letter (Croatian "dž", "lj", "nj"). + for (int position = 0; position < text.Length; position++) + { + char character = text[position]; + + // A locale tailoring overrides everything below it. The mechanism is v0's exactly — the entries + // simply carry a two-byte (SM, AW) primary here instead of one byte, which is the only reason + // this hook did not already exist. One entry is one WEIGHT however many bytes its primary takes, + // so it contributes exactly one secondary: the section counts weights, not bytes. + if (tailoring is not null && + tailoring.TryMatch(text, position, out TailoredWeight tailored, out int consumed, out bool repeat)) + { + for (int emit = repeat ? 2 : 1; emit > 0; emit--) + { + primaries.AddRange(tailored.Primaries); + secondaries.Add(tailored.Secondary); + } + position += consumed - 1; + continue; + } + + // A surrogate the table has no weight for is IGNORABLE, not an error — which is the whole of what + // astral support needs here, because both halves are otherwise weighed like any other character. + // + // ACE weighs an astral character by BOTH halves where it has weights for both: U+10000 is + // 7F B002 B4F8 01 3F 3F 00, the high surrogate D800 weighing B002 and the low DC00 weighing B4F8. + // Only the high surrogates up to U+D87F carry weights, so from plane 3 upward the high half drops + // out and the low one stands alone — U+30000 is 7F B4F8 01 3F 00. Planes 1 and 2 are therefore + // fully distinguished, while planes 3 to 16 collapse onto 1,024 keys and U+30000, U+31000, + // U+34000 and U+40000 all share one. That is ACE's behaviour to reproduce, not to improve. + // + // Written as "skip every high surrogate" first, generalising from plane-3 samples where the high + // half is unweighted. That broke all 131,068 characters of planes 1 and 2, where it is not. + if (char.IsSurrogate(character) && !table.TryGetWeight(character, out _, out _, out _)) continue; + + // Kana are weighed and sectioned exactly as in v0 — same sound weights, same section, verified + // byte-for-byte against ACE under both sort orders. Handled here rather than in Append because a + // single kana changes the shape of the WHOLE key. + // + // The prolonged sound mark lengthens the preceding kana's VOWEL, so it takes that vowel's primary + // and inherits its small flag, while marking itself in a second packed section. With no kana ahead + // of it there is nothing to lengthen, and it falls through to the ordinary table weight. + if (character is (char)0x30FC or (char)0xFF70 && kanaVowel != 0 && + kanaWeight == secondaries.Count - 1) + { + primaries.Add(JetKanaSection.KanaPage); + primaries.Add(kanaVowel); + secondaries.Add(DefaultSecondary); + kana.Add(kanaSmall); + prolonged.Add(true); + kanaWeight = secondaries.Count - 1; + continue; + } + + // The halfwidth voicing marks are COMBINING: ACE folds them into the preceding kana's secondary + // rather than weighing them. Measured alone they look ignorable, which is what hides this — so a + // single-character sweep cannot catch it, and it has to be carried over from v0 deliberately. + // Both halves of the guard matter: for a lone mark kanaWeight and Count-1 are each -1, which + // would otherwise pass and index the list at -1. + if (character is (char)0xFF9E or (char)0xFF9F && kanaWeight >= 0 && + kanaWeight == secondaries.Count - 1) + { + secondaries[kanaWeight] = character == (char)0xFF9E ? (byte)0x03 : (byte)0x04; + continue; + } + + if (JetTextCollationTableV0.TryGetKana( + character, out byte sound, out byte voicing, out bool small, out byte vowel)) + { + // Where the two versions part company: v1 weighs a compatibility form by the sound of the + // kana it decomposes to, while v0 gives it one of its own. The circled katakana are the + // case — ACE v1 weighs ㋐ as ア (02), where the v0 table holds 03 for it, and 46 and 2A + // where v1 wants 03 and 04. Everything else about the section is shared, which is why this + // is a substitution here rather than a second kana path. + string baseForm = character.ToString().Normalize(NormalizationForm.FormKD); + if (baseForm.Length == 1 && baseForm[0] != character && + JetTextCollationTableV0.TryGetKana(baseForm[0], out byte baseSound, out _, out _, out _)) + sound = baseSound; + + primaries.Add(JetKanaSection.KanaPage); + primaries.Add(sound); + secondaries.Add(voicing); + kana.Add(small); + prolonged.Add(false); + kanaWeight = secondaries.Count - 1; + kanaVowel = vowel; + kanaSmall = small; + continue; + } + + // A measured override wins over everything below it, because it came from ACE itself. Its bytes + // are appended verbatim — it states the finished bytes, not a table entry to interpret through + // the Han, Hangul and zero-AW rules below. + // + // Ahead of the kana fallback deliberately: script member 03 also collects characters that are not + // kana letters at all — the iteration marks, the lone prolonged mark, the double hyphen — which + // ACE gives the unweighted FF FF primary and no kana section. Those are measured, so they are + // simply recorded, and only what no measurement covers reaches the inference below. + if (JetTextCollationV1Overrides.IsIgnorable(character)) + { + // ACE contributes nothing for this character — it disappears from the key entirely. + continue; + } + + if (JetTextCollationV1Overrides.TryGet( + character, out ReadOnlySpan measuredPrimaries, + out ReadOnlySpan measuredSecondaries)) + { + foreach (byte b in measuredPrimaries) primaries.Add(b); + foreach (byte b in measuredSecondaries) secondaries.Add(b); + continue; + } + + // The kana the measured v0 table does not carry: the small hiragana ka and ke, the katakana + // phonetic extensions, and the enclosed (circled) katakana. v1's own table classifies them under + // script member 03, and its DW is the voicing — the enclosure rides along there, so a circled + // katakana is simply its kana with secondary EE. + // + // Its AW is the sound and its DW the voicing. Only characters absent from the measured v0 kana + // table reach here, and every one of them is its own base, so there is no decomposition to + // follow — the branch above owns the compatibility forms. + // + // The small flag cannot be assumed from reaching this path — the phonetic extensions are small + // and the circled forms are not, and marking all of them small put a spurious A0 in 45 keys. + // ACE's flag agrees exactly with the Unicode names: SMALL KA and SMALL KE, and the SMALL + // KU..SMALL RO run. The vowel is left at zero, because nothing measured covers a prolonged mark + // following one of these. + if (table.TryGetWeight(character, out byte member, out byte tableSound, out byte tableVoicing) && + member == EastAsiaSpecialScriptMember) + { + bool isSmall = character is (char)0x3095 or (char)0x3096 + or >= (char)0x31F0 and <= (char)0x31FF; + primaries.Add(JetKanaSection.KanaPage); + primaries.Add(tableSound); + secondaries.Add(tableVoicing); + kana.Add(isSmall); + prolonged.Add(false); + kanaWeight = secondaries.Count - 1; + kanaVowel = 0; + kanaSmall = isSmall; + continue; + } + + if (table.TryExpand(character, out char[]? sequence)) + { + // An expanded component takes the LOCALE's letter, not the base table's. The precomposed + // digraph U+01C4 expands to D + Ž, and in these orders Ž is a letter of its own (0EAD) — so + // sending the components straight to the base table gives D + Z-with-caron, which is what ACE + // does not store. The same rule version 0 already follows. + // + // Single-character lookup only: a component must not re-enter the CONTRACTION matcher, or + // expanding a ligature could trip a digraph entry that the original text never contained. + foreach (char expanded in sequence) + { + if (tailoring is not null && + tailoring.TryMatchSingle(expanded, out TailoredWeight component)) + { + primaries.AddRange(component.Primaries); + secondaries.Add(component.Secondary); + continue; + } + if (!Append(expanded)) return false; + } + } + else if (!Append(character)) + { + return false; + } + } + + bool Append(char character) + { + if (!table.TryGetWeight(character, out byte scriptMember, out byte alphabetic, out byte diacritic)) + return false; + + // A wholly zero entry is an ignorable with no record at all (e.g. the soft hyphen, which v0 + // instead records inline as 0x83 — a real difference between the two weight tables). + if (scriptMember == 0 && alphabetic == 0) return true; + + if (scriptMember == WordSortScriptMember) + { + // secondaries.Count is the weight count: every weight contributes exactly one secondary slot, + // whereas primaries.Count/2 assumed each weight is two bytes — which the four-byte Han + // primary above breaks. + inline.Add((secondaries.Count, scriptMember, alphabetic)); + return true; + } + + // A Han character takes a FOUR-byte primary and no secondary at all: the fixed marker FD FF, + // then its own alphabetic and diacritic weights. Splitting it the ordinary way — (SM, AW) as the + // primary and DW as a secondary — is what made every CJK key wrong, some 28,200 of them. + // U+4E00 ACE 7F FD FF 3C 6A 01 00, where the NLS entry is SM 05, AW 3C, DW 6A. + if (scriptMember == HanScriptMember) + { + primaries.Add(0xFD); + primaries.Add(0xFF); + primaries.Add(alphabetic); + primaries.Add(diacritic); + secondaries.Add(DefaultSecondary); + return true; + } + + // Hangul jamo take a two-byte primary of (AW, DW) with no secondary — the same "weights straight + // into the primary" shape as Han above, but without the FD FF marker. U+1100 is C0 02, where the + // NLS entry is SM 04, AW C0, DW 02. + if (scriptMember == HangulJamoScriptMember) + { + primaries.Add(alphabetic); + primaries.Add(diacritic); + secondaries.Add(DefaultSecondary); + return true; + } + + // A zero alphabetic weight means the character carries NO primary — only its secondary. Emitting + // (SM, 0) as a primary put an extra weight into every such key, which is where the last of the + // Greek, Cyrillic, Hebrew and Indic differences came from. + // U+0483 ACE 7F 01 94 00, not 7F 01 00 01 94 00. + // + // And where something precedes it, it does not take a slot of its own either: it FOLDS into the + // preceding weight, adding its diacritic. That is the Hebrew and Arabic presentation forms, whose + // expansions are a letter followed by a point — + // U+FB30 ACE 7F 28 02 01 30 00, where the parts weigh 0x02 and 0x2E and 0x02 + 0x2E = 0x30. + if (alphabetic == 0) + { + if (secondaries.Count == 0) secondaries.Add(diacritic); + else secondaries[^1] = (byte)(secondaries[^1] + diacritic); + return true; + } + + primaries.Add(scriptMember); + primaries.Add(alphabetic); + secondaries.Add(diacritic); + return true; + } + + output.AddRange(primaries); + output.Add(EndPrimary); + + // Secondary section: emitted up to and including the last character carrying a non-default accent. + int lastAccent = secondaries.FindLastIndex(weight => weight != DefaultSecondary); + for (int i = 0; i <= lastAccent; i++) output.Add(secondaries[i]); + + hasWordSortRecord = inline.Count > 0; + + if (kana.Count > 0) JetKanaSection.Append(output, kana, prolonged); + + // Those three 0x01s are not an "introducer" but three SECTION SEPARATORS. [MS-UCODEREF] gives the key + // as primaries SEP diacritics SEP case SEP extra SEP specials TERM, and Access emits the same frame + // while leaving the case section EMPTY — which is why case and width fold, since the Case Weight is + // where width lives. So the run is: end of diacritics, an empty case section, an empty extra section. + // A kana section fills that extra section, and the run shortens accordingly. + if (inline.Count > 0) + { + if (kana.Count > 0) + { + output.Add(0xFF); + output.Add(EndPrimary); + } + else + { + output.Add(EndPrimary); + output.Add(EndPrimary); + output.Add(EndPrimary); + } + foreach ((int position, byte scriptMember, byte alphabetic) in inline) + { + // [MS-UCODEREF] SpecialWeightType is (Position: 16-bit, ScriptMember, PrimaryWeight), and its + // Position is emitted big-endian — "Byte1 = Position >> 8, Byte2 = Position & 0xff" — so this + // is ONE sixteen-bit field with bit 15 set, not a 0x80 marker followed by a byte. Both + // readings give the same bytes below 0x100 and only the field reading survives past it, which + // is why treating 0x80 as a marker looked right for every short value and silently produced a + // wrong key for anything longer. Measured against ACE: a hyphen at character 250 is 83 EF. + int position16 = InlineStart << 8 | (0x07 + 4 * position); + output.Add((byte)(position16 >> 8)); + output.Add((byte)position16); + output.Add(scriptMember); + output.Add(alphabetic); + } + } + + output.Add(EndKey); + return true; + } + + private static WeightTable Load() + { + using Stream stream = typeof(JetTextCollationV1).Assembly + .GetManifestResourceStream("LibRed.Resources.SortKeyTableV1.bin") + ?? throw new InvalidOperationException("The v1 sorting weight table resource is missing from the assembly."); + + Span header = stackalloc byte[8]; + stream.ReadExactly(header); + int weightCount = BinaryPrimitives.ReadInt32LittleEndian(header); + int expansionCount = BinaryPrimitives.ReadInt32LittleEndian(header[4..]); + + byte[] deltas = ReadSection(stream); + byte[] scriptMembers = ReadSection(stream); + byte[] alphabetics = ReadSection(stream); + byte[] diacritics = ReadSection(stream); + byte[] expansionBytes = ReadSection(stream); + + if (scriptMembers.Length != weightCount || alphabetics.Length != weightCount || diacritics.Length != weightCount) + throw new InvalidDataException("The v1 sorting weight table resource is inconsistent with its header."); + + var codePoints = new ushort[weightCount]; + int offset = 0, current = 0; + for (int i = 0; i < weightCount; i++) + { + current += ReadVarInt(deltas, ref offset); + codePoints[i] = (ushort)current; + } + + var expansions = new Dictionary(expansionCount); + offset = 0; current = 0; + for (int i = 0; i < expansionCount; i++) + { + current += ReadVarInt(expansionBytes, ref offset); + int length = expansionBytes[offset++]; + var sequence = new char[length]; + for (int j = 0; j < length; j++) + { + sequence[j] = (char)BinaryPrimitives.ReadUInt16LittleEndian(expansionBytes.AsSpan(offset)); + offset += 2; + } + expansions[(char)current] = sequence; + } + + return new WeightTable(codePoints, scriptMembers, alphabetics, diacritics, expansions); + } + + private static byte[] ReadSection(Stream stream) + { + Span length = stackalloc byte[4]; + stream.ReadExactly(length); + var compressed = new byte[BinaryPrimitives.ReadInt32LittleEndian(length)]; + stream.ReadExactly(compressed); + + using var source = new MemoryStream(compressed); + using var inflate = new ZLibStream(source, CompressionMode.Decompress); + using var target = new MemoryStream(); + inflate.CopyTo(target); + return target.ToArray(); + } + + private static int ReadVarInt(byte[] data, ref int offset) + { + int value = 0, shift = 0; + while (true) + { + byte b = data[offset++]; + value |= (b & 0x7F) << shift; + if ((b & 0x80) == 0) return value; + shift += 7; + } + } + + /// Code points sorted ascending with their weights in parallel arrays — a binary search over + /// ~58k entries, rather than a dictionary, to keep the table near 300 KB resident instead of several MB. + private sealed class WeightTable( + ushort[] codePoints, byte[] scriptMembers, byte[] alphabetics, byte[] diacritics, + Dictionary expansions) + { + public bool TryGetWeight(char character, out byte scriptMember, out byte alphabetic, out byte diacritic) + { + int index = Array.BinarySearch(codePoints, (ushort)character); + if (index < 0) + { + scriptMember = alphabetic = diacritic = 0; + return false; + } + scriptMember = scriptMembers[index]; + alphabetic = alphabetics[index]; + diacritic = diacritics[index]; + return true; + } + + /// The character's expansion (e.g. ßs,s) when it has one. Deliberately + /// not "return the character in a one-element buffer": a shared buffer would race between the + /// concurrent readers the engine now allows, and allocating one per character would be worse. + public bool TryExpand(char character, [NotNullWhen(true)] out char[]? sequence) => + expansions.TryGetValue(character, out sequence); + } +} diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs new file mode 100644 index 000000000..e57809c82 --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs @@ -0,0 +1,147 @@ +using System.IO.Compression; + +namespace LibRed.Storage; + +/// +/// The characters where ACE's version-1 weights disagree with the published table LibRed embeds. +/// +/// +/// v1's primaries are the Windows NLS (Script Member, Alphabetic Weight) pair, and the table was +/// identified as Windows Server 2008 by reconstructing measured ACE keys — 25 of 25 against every +/// published version. That held for what it was tested on, Latin and symbols, and it does not hold +/// everywhere: ACE gives Balinese and Canadian syllabics Latin weights, and differs on the Arabic +/// harakat and several ligature blocks. Those are scripts added or reweighted after Server 2008, so ACE's +/// real table is not exactly the file we parse. +/// +/// Rather than guess which NLS revision ACE carries, the disagreements are measured and embedded: +/// SortKeyTableV1OverrideGeneratorTest (LIBRED_GENERATE_V1=1) encodes every BMP character +/// through ACE and records the weights implied wherever the result differs. 446 characters, 1.2 KB — the +/// same answer v0 needed, at a fraction of the size, because v1 is right about the other 57,594. +/// +/// +/// An entry is a sequence of weights, since one character can imply several: ACE encodes +/// U+1B08 as 0E02 0E21. They are appended verbatim, bypassing the Han and Hangul and zero-AW +/// rules, because an override states the finished bytes rather than a table entry to interpret. +/// +/// +internal static class JetTextCollationV1Overrides +{ + /// + /// Suppresses the overrides, so the encoder falls back to the published table alone. + /// + /// + /// Only the generator sets this, and it must. The resource records where the encoder disagrees with + /// ACE, so measuring an encoder that already consults it would find no disagreements and write an empty + /// file — the measurement would erase its own subject. The generator therefore takes the bare table. + /// + internal static bool Suppressed { get; set; } + + /// + /// The primary and secondary bytes ACE gives this character, or false when it agrees with the table. + /// + public static bool TryGet(char c, out ReadOnlySpan primaries, out ReadOnlySpan secondaries) + { + primaries = secondaries = default; + if (Suppressed) return false; + Table table = Loaded.Value; + int index = Array.BinarySearch(table.CodePoints, c); + if (index < 0) return false; + primaries = table.PrimaryBytes.AsSpan(table.PrimaryOffsets[index], table.PrimaryLengths[index]); + secondaries = table.SecondaryBytes.AsSpan(table.SecondaryOffsets[index], table.SecondaryLengths[index]); + return true; + } + + /// + /// Whether ACE contributes nothing at all for this character — an empty key, 7F 01 00. + /// + /// + /// The published table has no entry for these at all, so without the measured set the encoder refuses + /// them. They are stored as runs rather than weights, since the only fact worth keeping is membership: + /// 5,029 characters collapse into a few hundred ranges. + /// + public static bool IsIgnorable(char c) + { + if (Suppressed) return false; + int[] starts = Loaded.Value.IgnorableStarts; + int index = Array.BinarySearch(starts, (int)c); + if (index >= 0) return true; + index = ~index - 1; + return index >= 0 && c < starts[index] + Loaded.Value.IgnorableLengths[index]; + } + + private sealed record Table( + char[] CodePoints, + byte[] PrimaryLengths, int[] PrimaryOffsets, byte[] PrimaryBytes, + byte[] SecondaryLengths, int[] SecondaryOffsets, byte[] SecondaryBytes, + int[] IgnorableStarts, int[] IgnorableLengths); + + private static readonly Lazy
Loaded = new(Load); + + private static Table Load() + { + using Stream stream = typeof(JetTextCollationV1Overrides).Assembly + .GetManifestResourceStream("LibRed.Resources.SortKeyTableV1Overrides.bin") + ?? throw new InvalidOperationException("The v1 override resource is missing from the assembly."); + + var reader = new BinaryReader(stream); + int count = reader.ReadInt32(); + int rangeCount = reader.ReadInt32(); + byte[] deltas = ReadStream(reader); + byte[] primaryLengths = ReadStream(reader); + byte[] secondaryLengths = ReadStream(reader); + byte[] primaryBytes = ReadStream(reader); + byte[] secondaryBytes = ReadStream(reader); + byte[] rangeStarts = ReadStream(reader); + byte[] rangeLengths = ReadStream(reader); + + var codePoints = new char[count]; + var primaryOffsets = new int[count]; + var secondaryOffsets = new int[count]; + int codePoint = 0, cursor = 0, primary = 0, secondary = 0; + for (int i = 0; i < count; i++) + { + codePoint += ReadVarInt(deltas, ref cursor); + codePoints[i] = (char)codePoint; + primaryOffsets[i] = primary; + secondaryOffsets[i] = secondary; + primary += primaryLengths[i]; + secondary += secondaryLengths[i]; + } + var starts = new int[rangeCount]; + var lengths = new int[rangeCount]; + int start = 0, startCursor = 0, lengthCursor = 0; + for (int i = 0; i < rangeCount; i++) + { + start += ReadVarInt(rangeStarts, ref startCursor); + starts[i] = start; + lengths[i] = ReadVarInt(rangeLengths, ref lengthCursor); + } + + return new Table( + codePoints, + primaryLengths, primaryOffsets, primaryBytes, + secondaryLengths, secondaryOffsets, secondaryBytes, + starts, lengths); + } + + private static byte[] ReadStream(BinaryReader reader) + { + byte[] compressed = reader.ReadBytes(reader.ReadInt32()); + var output = new MemoryStream(); + using (var inflate = new ZLibStream(new MemoryStream(compressed), CompressionMode.Decompress)) + inflate.CopyTo(output); + return output.ToArray(); + } + + private static int ReadVarInt(byte[] source, ref int offset) + { + int value = 0, shift = 0; + while (true) + { + byte b = source[offset++]; + value |= (b & 0x7F) << shift; + if ((b & 0x80) == 0) return value; + shift += 7; + } + } +} diff --git a/src/LibRed/LibRed.Core/Storage/RowInserter.cs b/src/LibRed/LibRed.Core/Storage/RowInserter.cs index 9bd956d00..714833b29 100644 --- a/src/LibRed/LibRed.Core/Storage/RowInserter.cs +++ b/src/LibRed/LibRed.Core/Storage/RowInserter.cs @@ -36,7 +36,7 @@ public void Insert(object?[] values, bool updateIndexes) // Jet SQL omits the AutoNumber column from the insert). An explicitly supplied value is kept // as-is (Jet, unlike SQL Server, permits explicit AutoNumber values); either way the row's // final id drives both the row encoding and the high-water update below. - AssignAutoNumbers(format, values); + bool[]? generatedAutoNumbers = AssignAutoNumbers(format, values); if (updateIndexes) EnforceUniqueIndexes(values); // reject a duplicate before writing anything // Index keys are encoded from the *logical* values. MaterializeLongValues replaces a memo/OLE value @@ -71,7 +71,7 @@ public void Insert(object?[] values, bool updateIndexes) _channel.WritePage(pageNumber, page); - UpdateTdefCounters(format, values); + UpdateTdefCounters(format, values, generatedAutoNumbers); if (updateIndexes) UpdateIndexes(keyValues, new RowId(pageNumber, rowCount)); } @@ -505,13 +505,6 @@ private static int LowestRowOffset(byte[] page, JetFormatBase format, int rowCou return best; } - /// - /// Fills each AutoNumber column the caller left null with the next id — the TDEF high-water value - /// (`0x14`) plus one — matching how Jet assigns AutoNumbers. A value the caller supplied - /// explicitly is left untouched (Jet allows it, and then bumps - /// the high-water to it). Access permits only one AutoNumber column per table, but any number are - /// handled here for safety. - /// /// /// Access stores a memo/OLE value inline only up to 64 bytes (Jackcess /// MAX_INLINE_LONG_VALUE_SIZE, the same for Jet3/Jet4); a larger value goes on its own LVAL @@ -668,16 +661,26 @@ private TableDefinitionPage ReadDefinition() return definition; } - private void AssignAutoNumbers(JetFormatBase format, object?[] values) + /// + /// Fills each AutoNumber column the caller left null with the next id — the TDEF high-water value + /// (`0x14`) plus the increment — matching how Jet assigns AutoNumbers. A value the caller supplied + /// explicitly is left untouched (Jet allows it, and then bumps + /// the high-water to it). Access permits only one AutoNumber column per table, but any number are + /// handled here for safety. Returns a per-value flag array marking the ids this call generated + /// (null when it generated none), which uses to tell a generated + /// id from a caller-supplied one. + /// + private bool[]? AssignAutoNumbers(JetFormatBase format, object?[] values) { bool needed = false; foreach (ColumnDef column in _table.Columns) if (column.IsAutoNumber && values[column.Index] is null or DBNull) { needed = true; break; } - if (!needed) return; + if (!needed) return null; ReadOnlySpan tdef = _channel.ReadPageShared(_table.DefinitionPage).Span; int highWater = BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(format.TdefLastAutoNumberOffset, 4)); + bool[] generated = new bool[values.Length]; foreach (ColumnDef column in _table.Columns) if (column.IsAutoNumber && values[column.Index] is null or DBNull) { @@ -686,10 +689,16 @@ private void AssignAutoNumbers(JetFormatBase format, object?[] values) // sequential counter. Access relies on the PK's uniqueness to reject the rare collision. values[column.Index] = RandomAutoNumber(); else + { // Next id = last-assigned + increment. On a fresh table the last value (0x14) is - // Seed-Increment, so the first assigned id is the Seed. - values[column.Index] = highWater += column.Increment; + // Seed-Increment, so the first assigned id is the Seed. The addition is deliberately + // unchecked: past int.MaxValue the counter wraps to int.MinValue and keeps going, which + // is exactly what ACE does (no overflow error exists — see AceAutoNumberOverflowProbeTest). + values[column.Index] = highWater = unchecked(highWater + column.Increment); + generated[column.Index] = true; + } } + return generated; } /// A random non-zero signed Int32 for a "Random" AutoNumber, mirroring Access's GenUniqueID(). @@ -705,9 +714,11 @@ private static int RandomAutoNumber() /// page: /// /// Row count (`0x10`) — incremented. - /// AutoNumber high-water (`0x14`) — set to the max of its current value and the id just - /// written (Access reads it to pick the *next* id = this + 1; leaving it stale makes Access - /// reissue an existing id and reject the insert as a duplicate primary key). + /// AutoNumber high-water (`0x14`) — set to the id just written whenever that id advances the + /// counter: always for an id this insert generated (including the wrap past int.MaxValue), and for a + /// caller-supplied id only when it moves further in the increment's direction. (Access reads `0x14` + /// to pick the *next* id = this + increment; leaving it stale makes Access reissue an existing id and + /// reject the insert as a duplicate primary key.) /// Per-index **unique-entry count** (`0x3F + ordinal×12`, `+4`) — incremented by one for /// each **unique** index (a unique index gets a distinct key per row). This is the cumulative /// count Access advances on every insert and never decrements. The sibling **total-entry count** @@ -716,7 +727,7 @@ private static int RandomAutoNumber() /// ACE-inserted table reads total `0` while saved Northwind tables read total = row count). /// /// - private void UpdateTdefCounters(JetFormatBase format, object?[] values) + private void UpdateTdefCounters(JetFormatBase format, object?[] values, bool[]? generatedAutoNumbers) { byte[] tdef = _channel.ReadPageShared(_table.DefinitionPage).Span.ToArray(); @@ -732,14 +743,23 @@ private void UpdateTdefCounters(JetFormatBase format, object?[] values) if (column.IsRandomAutoNumber) continue; int assigned = Convert.ToInt32(value); int highWater = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(format.TdefLastAutoNumberOffset, 4)); - // Advance 0x14 to the id just written when it moves further in the counter's direction — for a - // positive increment that's the max seen, for a negative (descending) counter the min. Using max - // unconditionally would let a descending counter reissue the previous id (duplicate key). - // NB deliberate divergence from Access: ACE seeds 0x14 from the *last inserted* value regardless - // of direction (KB 884185), so an explicit lower INSERT drops the counter and the next auto id - // collides with an existing row ("duplicate values in the index/primary key"). LibRed's monotone - // rule is immune — verified both sides in AutoNumberSeed[Immunity]Tests. - bool advances = column.Increment >= 0 ? assigned > highWater : assigned < highWater; + // An id this insert *generated* always becomes the new high-water: it came from 0x14 + increment, + // so it is by construction the next value in the sequence. That includes the wrap past + // int.MaxValue (or int.MinValue for a descending counter), where the new id compares as going + // backwards yet is the correct continuation — ACE wraps and carries on, and the monotone rule + // below would instead pin 0x14 forever and reissue the same wrapped id on every later insert + // (a wedged table). Verified both sides in AceAutoNumberOverflowProbeTest. + // + // For a value the *caller* supplied explicitly, advance 0x14 only when it moves further in the + // counter's direction — for a positive increment that's the max seen, for a negative (descending) + // counter the min. Using max unconditionally would let a descending counter reissue the previous + // id (duplicate key). NB deliberate divergence from Access: ACE seeds 0x14 from the *last + // inserted* value regardless of direction (KB 884185), so an explicit lower INSERT drops the + // counter and the next auto id collides with an existing row ("duplicate values in the + // index/primary key"). LibRed's monotone rule is immune — verified both sides in + // AutoNumberSeed[Immunity]Tests. + bool advances = generatedAutoNumbers?[column.Index] == true + || (column.Increment >= 0 ? assigned > highWater : assigned < highWater); int newHighWater = advances ? assigned : highWater; if (advances) BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(format.TdefLastAutoNumberOffset, 4), assigned); @@ -748,7 +768,7 @@ private void UpdateTdefCounters(JetFormatBase format, object?[] values) // (an ALTER on a table that has an AutoNumber) reconstructs the counter from the cached // ColumnDef.Seed; if left stale, a rebuild after the high rows were deleted resets the counter to its // create-time value (verified: next id dropped to 1 instead of continuing past 6). Seed = next id. - column.Seed = newHighWater + column.Increment; + column.Seed = unchecked(newHighWater + column.Increment); } // TODO(non-unique-index-stats): a non-unique index's unique-entry count must advance only diff --git a/src/LibRed/LibRed.Core/Storage/RowRelocationReader.cs b/src/LibRed/LibRed.Core/Storage/RowRelocationReader.cs index fdd359fe8..928cbc8b8 100644 --- a/src/LibRed/LibRed.Core/Storage/RowRelocationReader.cs +++ b/src/LibRed/LibRed.Core/Storage/RowRelocationReader.cs @@ -10,7 +10,27 @@ internal readonly record struct RelocatedRow(PageBuffer Buffer, RowSlot Slot, in public ReadOnlySpan Bytes => Buffer.Slice(Slot.Offset, Slot.Length); } -/// Validates and follows the 4-byte forward pointer stored in a live overflow row slot. +/// +/// Validates and follows the forward pointer at the START of a live overflow row slot. +/// +/// +/// The slot is normally exactly 4 bytes: ACE's DML and both trim it down to the +/// pointer when a row is relocated. Measured over 317 relocations with no exception, across ACE x64, the +/// ACE 2010 x86 runtime, and LibRed's own writer, under growing and shrinking text, repeated re-relocation, +/// page fragmentation by interleaved deletes, and an OLE column going from NULL to a value. +/// +/// Real files nevertheless contain longer ones. Northwind's MSysAccessStorage has live overflow slots +/// of 45-63 bytes, and their contents are the row as it was BEFORE it moved, with only the leading 4 bytes +/// replaced by the pointer: every field lands where the row format puts it once those 4 bytes are discounted, +/// the keys match the row it forwards to, and the remnant's null bitmap differs from its target's in exactly +/// the OLE column's bit — the value whose arrival grew the row and forced the move. The slot simply kept the +/// old row's width. +/// +/// What wrote them is NOT known: no write path reproduces the shape, including the OLE-column transition the +/// bytes themselves record. So this reads the leading pointer and ignores whatever follows, rather than +/// asserting a width. The checks that matter are unchanged and do the real work — the target must be in the +/// file, owned by the same table, and a nonempty hidden inline row. +/// internal static class RowRelocationReader { public static RelocatedRow Resolve(PageChannel channel, int owningTablePage, @@ -18,11 +38,11 @@ public static RelocatedRow Resolve(PageChannel channel, int owningTablePage, { if (sourceSlot.IsDeleted || !sourceSlot.HasOverflow) throw new InvalidDataException("A relocation source must be a live overflow row slot."); - if (sourceBytes.Length != 4) + if (sourceBytes.Length < 4) throw new InvalidDataException( - $"A relocation source must contain exactly one 4-byte pointer; found {sourceBytes.Length} bytes."); + $"A relocation source must begin with a 4-byte pointer; found {sourceBytes.Length} bytes."); - int pointer = BinaryPrimitives.ReadInt32LittleEndian(sourceBytes); + int pointer = BinaryPrimitives.ReadInt32LittleEndian(sourceBytes[..4]); int pageNumber = pointer >> 8; int rowNumber = pointer & 0xFF; if (pageNumber <= 0 || pageNumber >= channel.PageCount) diff --git a/src/LibRed/LibRed.Core/Storage/Table.cs b/src/LibRed/LibRed.Core/Storage/Table.cs index ab30857ec..d6ca9d2b7 100644 --- a/src/LibRed/LibRed.Core/Storage/Table.cs +++ b/src/LibRed/LibRed.Core/Storage/Table.cs @@ -36,6 +36,10 @@ public Table(PageChannel channel, TableDef definition) private object?[]? GetRow(RowId id, RowDecoder decoder) { + if (id.Page <= 0 || id.Page >= Channel.PageCount) + throw new InvalidDataException( + $"Row pointer {id.Page}:{id.Row} is outside the file's 1..{Channel.PageCount - 1} page range."); + // Read just the one wanted slot straight from the page directory (O(1)), over the shared cache buffer // without copying the 4 KB page out — the bytes are consumed immediately by Decode. Both were the // seek's per-row hot cost. diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 3f8b7f594..cbec63083 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -54,7 +54,7 @@ public void Create( // A table name is unique (case-insensitively) across the database; reject a duplicate rather // than writing a second MSysObjects row that shadows the existing table. if (_catalog.FindTable(name) is not null) - throw new InvalidOperationException($"Table '{name}' already exists."); + throw new SchemaObjectExistsException($"Table '{name}' already exists.", name); // Jet/ACE caps a table at 255 columns. The count/id fields are 2 bytes wide so we could physically // write more, but Access would refuse to open the table — fail early with a clear message instead. @@ -70,7 +70,11 @@ public void Create( int tdefPage = _allocator.Allocate(); int usageMapPage = _allocator.Allocate(); - var longValueCols = columns.Select((c, i) => (Column: c, Id: i)) + // Key the long-value maps by the column's *id*, not its position. The two coincide on an ordinary + // CREATE TABLE, but a spec can carry an explicit id — the faithful-rebuild path does, and ids are + // never reused after a DROP COLUMN — and the TDEF's long-value map is read back by id, so using the + // position there silently points a Memo/OLE column's usage maps at the wrong column. + var longValueCols = columns.Select((c, i) => (Column: c, Id: c.ColumnId ?? i)) .Where(x => x.Column.Type is JetDataType.Memo or JetDataType.Ole) .ToList(); @@ -752,8 +756,9 @@ public bool RenameTable(string oldName, string newName) // allows (and EF's schema "move" degrades to exactly that on a schema-less engine), as is a case-only // change. Both verified — RenameFanOutProbeTest. if (ObjectNameExists(newName, exceptObjectId: table.DefinitionPage)) - throw new InvalidOperationException( - $"ALTER TABLE '{oldName}' RENAME TO '{newName}': a table or query named '{newName}' already exists."); + throw new SchemaObjectExistsException( + $"ALTER TABLE '{oldName}' RENAME TO '{newName}': a table or query named '{newName}' already exists.", + newName); RenameCatalogObject(table.DefinitionPage, newName); RepointRelationshipTables(oldName, newName); diff --git a/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs b/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs index 0e6b0452d..7ef28b0a1 100644 --- a/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs +++ b/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs @@ -23,7 +23,7 @@ public static class JetTypeCodec JetDataType.Int64 or JetDataType.Double or JetDataType.DateTime or JetDataType.Currency => 8, JetDataType.Guid => 16, JetDataType.FixedPoint => 17, - JetDataType.DateTimeExtended => 42, + JetDataType.DateTimeExtended => ExtendedDateTimeLength, _ => -1, }; if (expectedLength >= 0 && value.Length != expectedLength) @@ -48,7 +48,7 @@ public static class JetTypeCodec return BinaryPrimitives.ReadDoubleLittleEndian(value); case JetDataType.DateTime: return DateTime.FromOADate(BinaryPrimitives.ReadDoubleLittleEndian(value)); - case JetDataType.DateTimeExtended: // ACE 16 DATETIME2 + case JetDataType.DateTimeExtended: // ACE 17 DATETIME2 return DecodeExtendedDateTime(value); case JetDataType.Currency: return BinaryPrimitives.ReadInt64LittleEndian(value) / 10000m; @@ -71,7 +71,7 @@ public static class JetTypeCodec } /// - /// Decodes an ACE 16 DATETIME2 value: a fixed 42-byte ASCII string + /// Decodes an ACE 17 DATETIME2 value: a fixed 42-byte ASCII string /// "<day>:<time>:<precision>" where day is the .NET day number and /// time is the count of 100-ns ticks within the day. Both are zero-padded to 19 /// digits so that byte order equals chronological order. @@ -90,6 +90,40 @@ private static DateTime DecodeExtendedDateTime(ReadOnlySpan value) return new DateTime(day * TimeSpan.TicksPerDay + time); } + /// + /// Encodes an ACE 17 DATETIME2 value — the inverse of . The 42 bytes are + /// ASCII "<day>:<time>:<precision>": the .NET day number and the 100-ns ticks within + /// that day, each zero-padded to 19 digits so byte order equals chronological order, then the fractional + /// precision — 41 characters, NUL-padded to the field's 42 (19 + 1 + 19 + 1 + 1 = 41). + /// + /// + /// The padding byte is 0x00, not a space: verified by reading the row bytes ACE itself wrote + /// (… 3A 37 00). It matters beyond byte-faithfulness — the whole 42 bytes go into the index key + /// verbatim (see IndexKeyEncoder), so a space there would put every key we wrote out of step with + /// ACE's and make its seeks miss our rows. + /// The precision is always 7. ACE's DDL accepts no other form: DATETIME2(7) and every other + /// parenthesised spelling is a syntax error, so a Date/Time Extended column can only be declared bare, and + /// the value ACE itself writes for one carries 7 (verified against Microsoft.ACE.OLEDB.16.0). A + /// column's is not consulted: those descriptor bytes carry + /// precision/scale for FixedPoint columns, not this. + /// + internal static byte[] EncodeExtendedDateTime(DateTime value) + { + long day = value.Ticks / TimeSpan.TicksPerDay; + long time = value.Ticks % TimeSpan.TicksPerDay; + + string text = string.Create( + System.Globalization.CultureInfo.InvariantCulture, + $"{day:D19}:{time:D19}:7"); + + byte[] bytes = new byte[ExtendedDateTimeLength]; + Encoding.ASCII.GetBytes(text, bytes); // the 42nd byte stays 0x00 + return bytes; + } + + /// The fixed on-disk width of a DATETIME2 (Date/Time Extended) value. + internal const int ExtendedDateTimeLength = 42; + /// /// Decodes a Jet Decimal/Numeric value (17 bytes): a sign byte (0x80 = negative) followed /// by a 128-bit magnitude stored as four 32-bit little-endian words in big-endian word @@ -164,6 +198,8 @@ public static byte[] Encode(ColumnDef column, object value) return Bytes(8, b => BinaryPrimitives.WriteDoubleLittleEndian(b, Convert.ToDouble(value, c))); case JetDataType.DateTime: return Bytes(8, b => BinaryPrimitives.WriteDoubleLittleEndian(b, ToOaDate(value, c))); + case JetDataType.DateTimeExtended: // ACE 17 DATETIME2 + return EncodeExtendedDateTime(Convert.ToDateTime(value, c)); case JetDataType.Currency: return Bytes(8, b => BinaryPrimitives.WriteInt64LittleEndian(b, (long)decimal.Round(Convert.ToDecimal(value, c) * 10000m))); case JetDataType.Guid: @@ -171,7 +207,7 @@ public static byte[] Encode(ColumnDef column, object value) case JetDataType.Text: return EncodeText(column, AsText(value, c)); case JetDataType.Binary: - return EncodeBinary(column, (byte[])value); + return EncodeBinary(column, AsBinary(value)); case JetDataType.FixedPoint: return EncodeNumeric(Convert.ToDecimal(value, c), column.Scale); @@ -181,7 +217,7 @@ public static byte[] Encode(ColumnDef column, object value) case JetDataType.Memo: return EncodeInlineLongValue(Encoding.Unicode.GetBytes(AsText(value, c))); case JetDataType.Ole: - return EncodeInlineLongValue((byte[])value); + return EncodeInlineLongValue(AsBinary(value)); default: throw new NotSupportedException($"Encoding {column.Type} is not supported yet."); @@ -225,6 +261,26 @@ private static byte[] Bytes(int length, Action> write) return b; } + /// + /// The bytes to store for a binary (BINARY/VARBINARY) or OLE column. + /// + /// + /// A string written to a binary column stores its UTF-16LE bytes, exactly as text does — verified + /// against ACE for VARBINARY and LONGBINARY alike: 'A' stores 4100, 'AB' stores + /// 41004200, 'é' stores E900. The characters are NOT parsed as hex: '41' + /// stores 34003100 — the digits '4' and '1' — not the byte 0x41. + /// + /// The empty string therefore stores zero bytes, and that is the only way to write an empty binary as a + /// literal: Access has no digitless 0x (it rejects it), which is why + /// JetByteArrayTypeMapping emits '' for an empty array. + /// + private static byte[] AsBinary(object value) => value switch + { + byte[] bytes => bytes, + string text => Encoding.Unicode.GetBytes(text), + _ => (byte[])value, + }; + /// The OLE-automation epoch (1899-12-30), which is also Jet's zero date and the base for /// storing a / as a date offset. private static readonly DateTime OleEpoch = new(1899, 12, 30); diff --git a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs index c9584491c..2f632231c 100644 --- a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs @@ -58,7 +58,7 @@ private int AllocateObject(string name, int flags) foreach (object?[] row in new Table(_channel, msysObjects).Rows()) { if (string.Equals(row[nameIndex] as string, name, StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException($"An object named '{name}' already exists."); + throw new SchemaObjectExistsException($"An object named '{name}' already exists.", name); if (row[idIndex] is int id && id < 0 && id >= nextId) nextId = id + 1; } diff --git a/src/LibRed/LibRed.EFCore/Design/Internal/LibRedDesignTimeServices.cs b/src/LibRed/LibRed.EFCore/Design/Internal/LibRedDesignTimeServices.cs index 0c74462e4..8514237a5 100644 --- a/src/LibRed/LibRed.EFCore/Design/Internal/LibRedDesignTimeServices.cs +++ b/src/LibRed/LibRed.EFCore/Design/Internal/LibRedDesignTimeServices.cs @@ -1,24 +1,36 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using EntityFrameworkCore.Jet.Design.Internal; using EntityFrameworkCore.LibRed.Scaffolding.Internal; -using Microsoft.EntityFrameworkCore.Scaffolding; -using Microsoft.Extensions.DependencyInjection; - -namespace EntityFrameworkCore.LibRed.Design.Internal; +using Microsoft.EntityFrameworkCore.Design.Internal; -/// -/// Design-time services for the LibRed provider. Reuses EFCore.Jet's design services but swaps -/// the database-model factory for LibRed's catalog-backed one, so reverse-engineering reads the -/// schema natively instead of via INFORMATION_SCHEMA/ADOX. -/// -public class LibRedDesignTimeServices : JetDesignTimeServices +namespace EntityFrameworkCore.LibRed.Design.Internal { - public override void ConfigureDesignTimeServices(IServiceCollection serviceCollection) + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class LibRedDesignTimeServices : IDesignTimeServices { - base.ConfigureDesignTimeServices(serviceCollection); - // Registered after the base Jet factory; the later registration wins on resolution. Scoped, NOT - // singleton, to match EF Core's conventional lifetime for IDatabaseModelFactory (which the base Jet - // registration gets via TryAdd): the factory depends on the scoped scaffolding logger, so a singleton - // would capture a scoped ILoggerFactory and fail service-provider scope validation. - serviceCollection.AddScoped(); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollection) + { + serviceCollection.AddEntityFrameworkLibRed(); +#pragma warning disable EF1001 // Internal EF Core API usage. + new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection) + .TryAdd() + .TryAdd() +#pragma warning restore EF1001 // Internal EF Core API usage. + .TryAdd() + .TryAdd() + .TryAddCoreServices(); + } } } diff --git a/src/LibRed/LibRed.EFCore/Extensions/LibRedDbContextOptionsBuilderExtensions.cs b/src/LibRed/LibRed.EFCore/Extensions/LibRedDbContextOptionsBuilderExtensions.cs index 6a3af29b0..3777232a2 100644 --- a/src/LibRed/LibRed.EFCore/Extensions/LibRedDbContextOptionsBuilderExtensions.cs +++ b/src/LibRed/LibRed.EFCore/Extensions/LibRedDbContextOptionsBuilderExtensions.cs @@ -31,6 +31,32 @@ public static DbContextOptionsBuilder UseLibRed( return ApplyConfiguration(optionsBuilder, libRedOptionsAction); } + /// + /// Configures the context to connect to a Jet/ACE database via the native LibRed engine, but without + /// initially setting any or connection string. + /// + /// The builder being used to configure the context. + /// + /// generates SQL that the Jet/ACE engine also accepts, using the + /// same SQL generator as the EntityFrameworkCore.Jet provider. uses + /// LibRed's own SQL generator, which is free of the Jet dialect's limitations. + /// + /// An optional action to allow additional LibRed specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseLibRed( + this DbContextOptionsBuilder optionsBuilder, + LibRedSqlMode sqlMode, + Action? libRedOptionsAction = null) + { + ArgumentNullException.ThrowIfNull(optionsBuilder); + + var extension = GetOrCreateExtension(optionsBuilder) + .WithSqlMode(sqlMode); + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + + return ApplyConfiguration(optionsBuilder, libRedOptionsAction); + } + /// /// Configures the context to use the native LibRed engine against a Jet/ACE file. The /// connection string takes the form Data Source=path\to\file.accdb (a bare file path @@ -61,22 +87,24 @@ public static DbContextOptionsBuilder UseLibRed( /// /// The builder being used to configure the context. /// The file name or connection string of the database to connect to. - /// The to be used for all - /// data access operations by the LibRed connection. + /// + /// generates SQL that the Jet/ACE engine also accepts, using the + /// same SQL generator as the EntityFrameworkCore.Jet provider. uses + /// LibRed's own SQL generator, which is free of the Jet dialect's limitations. + /// /// An optional action to allow additional LibRed specific configuration. /// The options builder so that further configuration can be chained. public static DbContextOptionsBuilder UseLibRed( this DbContextOptionsBuilder optionsBuilder, string? connectionString, - DbProviderFactory dataAccessProviderFactory, + LibRedSqlMode sqlMode, Action? libRedOptionsAction = null) { ArgumentNullException.ThrowIfNull(optionsBuilder); - ArgumentNullException.ThrowIfNull(dataAccessProviderFactory); - var extension = (LibRedOptionsExtension)GetOrCreateExtension(optionsBuilder) - .WithConnectionString(connectionString); - extension = extension.WithDataAccessProviderFactory(dataAccessProviderFactory); + var extension = ((LibRedOptionsExtension)GetOrCreateExtension(optionsBuilder) + .WithConnectionString(connectionString)) + .WithSqlMode(sqlMode); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); return ApplyConfiguration(optionsBuilder, libRedOptionsAction); @@ -100,6 +128,30 @@ public static DbContextOptionsBuilder UseLibRed( Action? libRedOptionsAction = null) => UseLibRed(optionsBuilder, connection, false, libRedOptionsAction); + // Note: Decision made to use DbConnection not SqlConnection: Issue #772 + /// + /// Configures the context to use the native LibRed engine. + /// + /// The builder being used to configure the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. + /// + /// + /// generates SQL that the Jet/ACE engine also accepts, using the + /// same SQL generator as the EntityFrameworkCore.Jet provider. uses + /// LibRed's own SQL generator, which is free of the Jet dialect's limitations. + /// + /// An optional action to allow additional LibRed specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseLibRed( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + LibRedSqlMode sqlMode, + Action? libRedOptionsAction = null) + => UseLibRed(optionsBuilder, connection, false, sqlMode, libRedOptionsAction); + // Note: Decision made to use DbConnection not SqlConnection: Issue #772 /// /// Configures the context to use the native LibRed engine. @@ -138,6 +190,51 @@ public static DbContextOptionsBuilder UseLibRed( return ApplyConfiguration(optionsBuilder, libRedOptionsAction); } + // Note: Decision made to use DbConnection not SqlConnection: Issue #772 + /// + /// Configures the context to use the native LibRed engine. + /// + /// The builder being used to configure the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. + /// + /// + /// If , then EF will take ownership of the connection and will + /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still + /// owns the connection and is responsible for its disposal. + /// + /// + /// generates SQL that the Jet/ACE engine also accepts, using the + /// same SQL generator as the EntityFrameworkCore.Jet provider. uses + /// LibRed's own SQL generator, which is free of the Jet dialect's limitations. + /// + /// An optional action to allow additional LibRed specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseLibRed( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + bool contextOwnsConnection, + LibRedSqlMode sqlMode, + Action? libRedOptionsAction = null) + { + ArgumentNullException.ThrowIfNull(optionsBuilder); + ArgumentNullException.ThrowIfNull(connection); + + if (connection is not LibRedConnection) + { + throw new ArgumentException($"The {nameof(connection)} parameter must be of type {nameof(LibRedConnection)}."); + } + + var extension = ((LibRedOptionsExtension)GetOrCreateExtension(optionsBuilder) + .WithConnection(connection, contextOwnsConnection)) + .WithSqlMode(sqlMode); + ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); + + return ApplyConfiguration(optionsBuilder, libRedOptionsAction); + } + /// /// Configures the context to connect to a Jet/ACE database via the native LibRed engine, but without /// initially setting any or connection string. @@ -152,6 +249,26 @@ public static DbContextOptionsBuilder UseLibRed( => (DbContextOptionsBuilder)UseLibRed( (DbContextOptionsBuilder)optionsBuilder, libRedOptionsAction); + /// + /// Configures the context to connect to a Jet/ACE database via the native LibRed engine, but without + /// initially setting any or connection string. + /// + /// The builder being used to configure the context. + /// + /// generates SQL that the Jet/ACE engine also accepts, using the + /// same SQL generator as the EntityFrameworkCore.Jet provider. uses + /// LibRed's own SQL generator, which is free of the Jet dialect's limitations. + /// + /// An optional action to allow additional LibRed specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseLibRed( + this DbContextOptionsBuilder optionsBuilder, + LibRedSqlMode sqlMode, + Action? libRedOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseLibRed( + (DbContextOptionsBuilder)optionsBuilder, sqlMode, libRedOptionsAction); + /// /// Configures the context to use the native LibRed engine against a Jet/ACE file. The /// connection string takes the form Data Source=path\to\file.accdb (a bare file path @@ -176,18 +293,21 @@ public static DbContextOptionsBuilder UseLibRed( /// /// The builder being used to configure the context. /// The file name or connection string of the database to connect to. - /// The to be used for all - /// data access operations by the LibRed connection. + /// + /// generates SQL that the Jet/ACE engine also accepts, using the + /// same SQL generator as the EntityFrameworkCore.Jet provider. uses + /// LibRed's own SQL generator, which is free of the Jet dialect's limitations. + /// /// An optional action to allow additional LibRed specific configuration. /// The options builder so that further configuration can be chained. public static DbContextOptionsBuilder UseLibRed( this DbContextOptionsBuilder optionsBuilder, string? connectionString, - DbProviderFactory dataAccessProviderFactory, + LibRedSqlMode sqlMode, Action? libRedOptionsAction = null) where TContext : DbContext => (DbContextOptionsBuilder)UseLibRed( - (DbContextOptionsBuilder)optionsBuilder, connectionString, dataAccessProviderFactory, libRedOptionsAction); + (DbContextOptionsBuilder)optionsBuilder, connectionString, sqlMode, libRedOptionsAction); // Note: Decision made to use DbConnection not SqlConnection: Issue #772 /// @@ -210,6 +330,33 @@ public static DbContextOptionsBuilder UseLibRed( => (DbContextOptionsBuilder)UseLibRed( (DbContextOptionsBuilder)optionsBuilder, connection, libRedOptionsAction); + // Note: Decision made to use DbConnection not SqlConnection: Issue #772 + /// + /// Configures the context to use the native LibRed engine. + /// + /// The type of context to be configured. + /// The builder being used to configure the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. + /// + /// + /// generates SQL that the Jet/ACE engine also accepts, using the + /// same SQL generator as the EntityFrameworkCore.Jet provider. uses + /// LibRed's own SQL generator, which is free of the Jet dialect's limitations. + /// + /// An optional action to allow additional LibRed specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseLibRed( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + LibRedSqlMode sqlMode, + Action? libRedOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseLibRed( + (DbContextOptionsBuilder)optionsBuilder, connection, sqlMode, libRedOptionsAction); + // Note: Decision made to use DbConnection not SqlConnection: Issue #772 /// /// Configures the context to use the native LibRed engine. @@ -237,6 +384,39 @@ public static DbContextOptionsBuilder UseLibRed( => (DbContextOptionsBuilder)UseLibRed( (DbContextOptionsBuilder)optionsBuilder, connection, contextOwnsConnection, libRedOptionsAction); + // Note: Decision made to use DbConnection not SqlConnection: Issue #772 + /// + /// Configures the context to use the native LibRed engine. + /// + /// The type of context to be configured. + /// The builder being used to configure the context. + /// + /// An existing to be used to connect to the database. If the connection is + /// in the open state then EF will not open or close the connection. If the connection is in the closed + /// state then EF will open and close the connection as needed. + /// + /// + /// If , then EF will take ownership of the connection and will + /// dispose it in the same way it would dispose a connection created by EF. If , then the caller still + /// owns the connection and is responsible for its disposal. + /// + /// + /// generates SQL that the Jet/ACE engine also accepts, using the + /// same SQL generator as the EntityFrameworkCore.Jet provider. uses + /// LibRed's own SQL generator, which is free of the Jet dialect's limitations. + /// + /// An optional action to allow additional LibRed specific configuration. + /// The options builder so that further configuration can be chained. + public static DbContextOptionsBuilder UseLibRed( + this DbContextOptionsBuilder optionsBuilder, + DbConnection connection, + bool contextOwnsConnection, + LibRedSqlMode sqlMode, + Action? libRedOptionsAction = null) + where TContext : DbContext + => (DbContextOptionsBuilder)UseLibRed( + (DbContextOptionsBuilder)optionsBuilder, connection, contextOwnsConnection, sqlMode, libRedOptionsAction); + private static LibRedOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.Options.FindExtension() ?? new LibRedOptionsExtension(); diff --git a/src/LibRed/LibRed.EFCore/Extensions/LibRedServiceCollectionExtensions.cs b/src/LibRed/LibRed.EFCore/Extensions/LibRedServiceCollectionExtensions.cs index 6588608b5..8744b6e37 100644 --- a/src/LibRed/LibRed.EFCore/Extensions/LibRedServiceCollectionExtensions.cs +++ b/src/LibRed/LibRed.EFCore/Extensions/LibRedServiceCollectionExtensions.cs @@ -1,10 +1,22 @@ -using EntityFrameworkCore.Jet.Infrastructure.Internal; +using EntityFrameworkCore.Jet.Diagnostics.Internal; +using EntityFrameworkCore.Jet.Internal; +using EntityFrameworkCore.Jet.Metadata.Internal; +using EntityFrameworkCore.Jet.Migrations.Internal; +using EntityFrameworkCore.LibRed.Migrations.Internal; +using EntityFrameworkCore.Jet.Query; +using EntityFrameworkCore.Jet.Query.ExpressionTranslators.Internal; +using EntityFrameworkCore.Jet.Query.Internal; +using EntityFrameworkCore.Jet.Query.Sql.Internal; using EntityFrameworkCore.Jet.Storage.Internal; +using EntityFrameworkCore.Jet.Update.Internal; +using EntityFrameworkCore.Jet.Utilities; +using EntityFrameworkCore.Jet.ValueGeneration.Internal; using EntityFrameworkCore.LibRed.Infrastructure.Internal; +using EntityFrameworkCore.LibRed.Internal; +using EntityFrameworkCore.LibRed.Query.Internal; +using EntityFrameworkCore.LibRed.Query.Sql.Internal; using EntityFrameworkCore.LibRed.Storage.Internal; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.Extensions.DependencyInjection.Extensions; +using EntityFrameworkCore.LibRed.Update.Internal; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; @@ -13,69 +25,51 @@ namespace Microsoft.Extensions.DependencyInjection; public static class LibRedServiceCollectionExtensions { /// - /// Registers the EFCore.Jet provider services, then overrides the LibRed-specific pieces. - /// Today that is the relational connection (native engine instead of ODBC/OLE DB), the - /// database creator, and the default execution strategy; the SQL generator and others are - /// inherited from EFCore.Jet for now. The later registration wins when a single service is - /// resolved, so this overrides the Jet defaults. + /// Registers the EFCore.LibRed provider services /// public static IServiceCollection AddEntityFrameworkLibRed(this IServiceCollection serviceCollection) { - serviceCollection.AddEntityFrameworkJet(); - // ILibRedRelationalConnection is the concrete registration LibRed owns; IJetRelationalConnection - // (and therefore IRelationalConnection, which Jet forwards to it) resolve to the same instance. - // Registering on the LibRed interface — not IJetRelationalConnection — lets tests and downstream - // code override "the LibRed connection" without naming a Jet type. - // TryAdd/Replace (never plain Add) so a second AddEntityFrameworkLibRed() call is idempotent and Jet's - // original registration doesn't linger beside ours — the service-collection conformance test checks both - // (Repeated_calls_to_add_do_not_modify_collection, Required_services_are_registered_with_expected_lifetimes). - serviceCollection.TryAddScoped(); - serviceCollection.Replace(ServiceDescriptor.Scoped( - p => p.GetRequiredService())); - // Answer existence / has-tables from LibRed's catalog instead of INFORMATION_SCHEMA + ADOX. - serviceCollection.Replace(ServiceDescriptor.Scoped()); - // Substitute the driver-free `long` mapping (DbType reflects the decimal(20,0) it's stored as) for - // EFCore.Jet's, which only reports Decimal via an OLE DB/ODBC reflection poke a native engine can't use. - // - // This MUST go through the EF services builder (not serviceCollection.AddSingleton) so it lands in - // EF Core's internal service provider as a *per-options* singleton — one instance per unique - // DbContextOptions, each built with that context's own IJetOptions. A plain application singleton is - // constructed once with whichever context resolves it first and then shared across every context, - // baking in the wrong UseShortTextForSystemString (the model-building context, False, wins over the - // store context, True) — which made unbounded strings scaffold/migrate as `longchar` instead of - // `varchar(255)`. TryAdd won't replace Jet's existing registration, so drop it first — but drop ONLY - // EFCore.Jet's own JetTypeMappingSource, NOT a custom source a fixture/downstream registered before us. - // (RemoveAll wiped those too: e.g. the EverythingIsStrings/EverythingIsBytes - // fixtures register their own all-string/all-byte IRelationalTypeMappingSource, then call AddProviderServices - // → us; when one is present Jet's TryAdd already no-op'd, so there's no Jet mapping to remove and our TryAdd - // below correctly no-ops too, leaving the custom source in place.) - foreach (ServiceDescriptor jetMapping in serviceCollection.Where(d => - d.ServiceType == typeof(IRelationalTypeMappingSource) && - d.ImplementationType == typeof(JetTypeMappingSource)).ToList()) - serviceCollection.Remove(jetMapping); - new EntityFrameworkRelationalServicesBuilder(serviceCollection) - .TryAdd(); - serviceCollection.Replace(ServiceDescriptor.Scoped()); - // Report LibRed's own provider identity in logs/diagnostics ("EntityFrameworkCore.LibRed", not - // "EntityFrameworkCore.Jet"): IDatabaseProvider's Name is DatabaseProvider's - // options-extension assembly name. Swap Jet's registration (keyed on JetOptionsExtension) for one keyed - // on LibRedOptionsExtension. Same remove-then-TryAdd shape as the other service swaps. - foreach (ServiceDescriptor jetProvider in serviceCollection.Where(d => - d.ServiceType == typeof(IDatabaseProvider) && - d.ImplementationType == typeof(DatabaseProvider)).ToList()) - serviceCollection.Remove(jetProvider); - new EntityFrameworkRelationalServicesBuilder(serviceCollection) - .TryAdd>(); - // EFCore.Jet's JetTransaction disables savepoints (ACE has none). LibRed's engine and ADO layer both - // support them, so swap in a factory that builds EF Core's base RelationalTransaction, which honours - // savepoints via the ADO transaction. Same shape as the type-mapping swap above: drop Jet's descriptor - // (TryAdd won't replace it), then re-add ours through the services builder at the conventional lifetime. - foreach (ServiceDescriptor jetTxFactory in serviceCollection.Where(d => - d.ServiceType == typeof(IRelationalTransactionFactory) && - d.ImplementationType == typeof(JetTransactionFactory)).ToList()) - serviceCollection.Remove(jetTxFactory); - new EntityFrameworkRelationalServicesBuilder(serviceCollection) - .TryAdd(); + Check.NotNull(serviceCollection, nameof(serviceCollection)); + + var builder = new EntityFrameworkRelationalServicesBuilder(serviceCollection) + .TryAdd() + .TryAdd>() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd(p => p.GetRequiredService()) + .TryAdd() + .TryAdd() + .TryAdd(p => p.GetRequiredService()) + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd(p => p.GetRequiredService()) + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAdd() + .TryAddProviderSpecificServices( + b => b + .TryAddSingleton() + .TryAddSingleton() + .TryAddScoped()); + + builder.TryAddCoreServices(); + return serviceCollection; } } diff --git a/src/LibRed/LibRed.EFCore/Infrastructure/Internal/ILibRedOptions.cs b/src/LibRed/LibRed.EFCore/Infrastructure/Internal/ILibRedOptions.cs new file mode 100644 index 000000000..8f30ba427 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Infrastructure/Internal/ILibRedOptions.cs @@ -0,0 +1,14 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace EntityFrameworkCore.LibRed.Infrastructure.Internal +{ + /// + /// Options set at the singleton level to control LibRed specific options. + /// + public interface ILibRedOptions : ISingletonOptions + { + string? ConnectionString { get; } + bool UseShortTextForSystemString { get; } + LibRedSqlMode SqlMode { get; } + } +} diff --git a/src/LibRed/LibRed.EFCore/Infrastructure/Internal/LibRedOptionsExtension.cs b/src/LibRed/LibRed.EFCore/Infrastructure/Internal/LibRedOptionsExtension.cs index f67a527f7..0b5b77d0d 100644 --- a/src/LibRed/LibRed.EFCore/Infrastructure/Internal/LibRedOptionsExtension.cs +++ b/src/LibRed/LibRed.EFCore/Infrastructure/Internal/LibRedOptionsExtension.cs @@ -1,45 +1,168 @@ -using System.Data.Common; -using EntityFrameworkCore.Jet.Infrastructure.Internal; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.DependencyInjection; - -namespace EntityFrameworkCore.LibRed.Infrastructure.Internal; - -/// -/// Options extension for the LibRed provider. It reuses EFCore.Jet's options extension wholesale -/// (so every Jet service that looks for a still finds one) but -/// applies the LibRed service overrides instead of the plain Jet ones. -/// -public class LibRedOptionsExtension : JetOptionsExtension +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; +using System.Text; + +namespace EntityFrameworkCore.LibRed.Infrastructure.Internal { - public LibRedOptionsExtension() + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class LibRedOptionsExtension : RelationalOptionsExtension { - } + private DbContextOptionsExtensionInfo? _info; + private bool _useShortTextForSystemString; + private LibRedSqlMode _sqlMode; - protected LibRedOptionsExtension(LibRedOptionsExtension copyFrom) - : base(copyFrom) - { - } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public LibRedOptionsExtension() + { + } + + // NB: When adding new options, make sure to update the copy ctor below. + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected LibRedOptionsExtension(LibRedOptionsExtension copyFrom) + : base(copyFrom) + { + _useShortTextForSystemString = copyFrom._useShortTextForSystemString; + _sqlMode = copyFrom._sqlMode; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override DbContextOptionsExtensionInfo Info + => _info ??= new ExtensionInfo(this); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalOptionsExtension Clone() + => new LibRedOptionsExtension(this); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool UseShortTextForSystemString => _useShortTextForSystemString; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual LibRedOptionsExtension WithUseShortTextForSystemString(bool enabled) + { + var clone = (LibRedOptionsExtension)Clone(); + + clone._useShortTextForSystemString = enabled; - public override void ApplyServices(IServiceCollection services) - => services.AddEntityFrameworkLibRed(); + return clone; + } - // Must preserve the derived type: WithConnectionString/WithConnection clone the extension, - // and a plain JetOptionsExtension clone would silently revert ApplyServices to AddEntityFrameworkJet. - protected override RelationalOptionsExtension Clone() - => new LibRedOptionsExtension(this); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual LibRedSqlMode SqlMode => _sqlMode; - // Covariant overrides: the base With...() methods clone via the (overridden) Clone() above, so the - // returned instance is already a LibRedOptionsExtension at runtime; this just fixes the static type. - public override LibRedOptionsExtension WithUseOuterSelectSkipEmulationViaDataReader(bool enabled) - => (LibRedOptionsExtension)base.WithUseOuterSelectSkipEmulationViaDataReader(enabled); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual LibRedOptionsExtension WithSqlMode(LibRedSqlMode sqlMode) + { + var clone = (LibRedOptionsExtension)Clone(); - public override LibRedOptionsExtension WithEnableMillisecondsSupport(bool enabled) - => (LibRedOptionsExtension)base.WithEnableMillisecondsSupport(enabled); + clone._sqlMode = sqlMode; - public override LibRedOptionsExtension WithUseShortTextForSystemString(bool enabled) - => (LibRedOptionsExtension)base.WithUseShortTextForSystemString(enabled); + return clone; + } - public override LibRedOptionsExtension WithDataAccessProviderFactory(DbProviderFactory dataAccessProviderFactory) - => (LibRedOptionsExtension)base.WithDataAccessProviderFactory(dataAccessProviderFactory); -} + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override void ApplyServices(IServiceCollection services) + => services.AddEntityFrameworkLibRed(); + + private sealed class ExtensionInfo(IDbContextOptionsExtension extension) : RelationalExtensionInfo(extension) + { + private int? _serviceProviderHash; + private string? _logFragment; + + private new LibRedOptionsExtension Extension + => (LibRedOptionsExtension)base.Extension; + + public override bool IsDatabaseProvider => true; + + public override string LogFragment + { + get + { + if (_logFragment == null) + { + var builder = new StringBuilder(); + + builder.Append(base.LogFragment); + + if (Extension._useShortTextForSystemString) + { + builder.Append("UseShortTextForSystemString "); + } + + _logFragment = builder.ToString(); + } + + return _logFragment; + } + } + + public override int GetServiceProviderHashCode() + { + _serviceProviderHash ??= ((base.GetServiceProviderHashCode() * 397) ^ + (Extension._useShortTextForSystemString.GetHashCode() * 397)) ^ + (Extension._sqlMode.GetHashCode() * 397); + + return _serviceProviderHash.Value; + } + + public override void PopulateDebugInfo(IDictionary debugInfo) + { + debugInfo["Jet:" + nameof(LibRedDbContextOptionsBuilder.UseShortTextForSystemString)] + = Extension._useShortTextForSystemString.GetHashCode().ToString(CultureInfo.InvariantCulture); + debugInfo["Jet:" + nameof(LibRedDbContextOptionsBuilder.UseSqlMode)] + = Extension._sqlMode.GetHashCode().ToString(CultureInfo.InvariantCulture); + } + } + } +} \ No newline at end of file diff --git a/src/LibRed/LibRed.EFCore/Infrastructure/LibRedDbContextOptionsBuilder.cs b/src/LibRed/LibRed.EFCore/Infrastructure/LibRedDbContextOptionsBuilder.cs index 11d8b2420..e1114e55b 100644 --- a/src/LibRed/LibRed.EFCore/Infrastructure/LibRedDbContextOptionsBuilder.cs +++ b/src/LibRed/LibRed.EFCore/Infrastructure/LibRedDbContextOptionsBuilder.cs @@ -18,36 +18,6 @@ namespace EntityFrameworkCore.LibRed.Infrastructure public class LibRedDbContextOptionsBuilder(DbContextOptionsBuilder optionsBuilder) : RelationalDbContextOptionsBuilder(optionsBuilder) { - - /// - /// Use a ROW_NUMBER() in queries instead of OFFSET/FETCH. This method is backwards-compatible to Jet 2005. - /// - // [Obsolete("Row-number paging is no longer supported. See https://aka.ms/AA6h122 for more information.")] - // public virtual LibRedDbContextOptionsBuilder UseRowNumberForPaging(bool useRowNumberForPaging = true) - // => WithOption(e => e.WithRowNumberPaging(useRowNumberForPaging)); - - /// - /// Jet/ACE doesn't natively support row skipping. When this option is enabled, row skipping will be - /// emulated in the most outer SELECT statement, by letting the JetDataReader ignore as many returned rows - /// as should have been skipped by the database. - /// This will only work when `JetCommand.ExecuteDataReader()` is beeing used to execute the `JetCommand`. - /// It is recommanded to not use this option, but to switch to client evaluation instead, by inserting a - /// call to either `AsEnumerable()`, `AsAsyncEnumerable()`, `ToList()`, or `ToListAsync()` and only then - /// to use `Skip()`. This will work in all cases and independent of the specific `JetCommand.Execute()` - /// method called. - /// - [Obsolete("This method exists for backward compatibility reasons only. Switch to client evaluation instead.")] - public virtual LibRedDbContextOptionsBuilder UseOuterSelectSkipEmulationViaDataReader(bool enabled = true) - => WithOption(e => e.WithUseOuterSelectSkipEmulationViaDataReader(enabled)); - - /// - /// Configures the context support milliseconds in `DateTime`, `DateTimeOffset` and `TimeSpan` values when - /// accessing Jet databases. Jet has no native support for milliseconds, therefore this feature is opt-in. - /// - public virtual LibRedDbContextOptionsBuilder EnableMillisecondsSupport(bool enabled = true) - => WithOption(e => e.WithEnableMillisecondsSupport(enabled)); - - /// /// Set this to enabled to map the System.String CLR type to the Jet `Short Text` data type instead of the /// Long Text data type. This will limit the maximum length of strings to 255 characters. @@ -59,6 +29,15 @@ public virtual LibRedDbContextOptionsBuilder EnableMillisecondsSupport(bool enab public virtual LibRedDbContextOptionsBuilder UseShortTextForSystemString(bool enabled = true) => WithOption(e => e.WithUseShortTextForSystemString(enabled)); + /// + /// Selects the SQL dialect the provider generates. generates SQL + /// that the Jet/ACE engine also accepts, using the same SQL generator as the EntityFrameworkCore.Jet + /// provider; uses LibRed's own SQL generator, which is free of the + /// Jet dialect's limitations and is the default. + /// + public virtual LibRedDbContextOptionsBuilder UseSqlMode(LibRedSqlMode sqlMode = LibRedSqlMode.Compatible) + => WithOption(e => e.WithSqlMode(sqlMode)); + /// /// Configures the context to use the default retrying . /// diff --git a/src/LibRed/LibRed.EFCore/Infrastructure/LibRedSqlMode.cs b/src/LibRed/LibRed.EFCore/Infrastructure/LibRedSqlMode.cs new file mode 100644 index 000000000..f09a2ebfe --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Infrastructure/LibRedSqlMode.cs @@ -0,0 +1,21 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace EntityFrameworkCore.LibRed.Infrastructure +{ + /// + /// The SQL dialect the LibRed provider generates. + /// + public enum LibRedSqlMode + { + /// + /// LibRed uses its own SQL generator, which is free of the Jet dialect's limitations. This is the default. + /// + Extended = 0, + + /// + /// LibRed generates SQL that the Jet/ACE engine also accepts, using the same SQL generator as the + /// EntityFrameworkCore.Jet provider. + /// + Compatible = 1, + } +} diff --git a/src/LibRed/LibRed.EFCore/Internal/LibRedOptions.cs b/src/LibRed/LibRed.EFCore/Internal/LibRedOptions.cs new file mode 100644 index 000000000..81b680db7 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Internal/LibRedOptions.cs @@ -0,0 +1,77 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using EntityFrameworkCore.LibRed.Infrastructure; +using EntityFrameworkCore.LibRed.Infrastructure.Internal; + +namespace EntityFrameworkCore.LibRed.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class LibRedOptions : ILibRedOptions + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual void Initialize(IDbContextOptions options) + { + var libRedOptions = options.Extensions.OfType().FirstOrDefault() ?? new LibRedOptionsExtension(); + + ConnectionString = libRedOptions.Connection?.ConnectionString ?? libRedOptions.ConnectionString!; + UseShortTextForSystemString = libRedOptions.UseShortTextForSystemString; + SqlMode = libRedOptions.SqlMode; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual void Validate(IDbContextOptions options) + { + var libRedOptions = options.Extensions.OfType().FirstOrDefault() ?? new LibRedOptionsExtension(); + + if (UseShortTextForSystemString != libRedOptions.UseShortTextForSystemString) + { + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(LibRedOptionsExtension.UseShortTextForSystemString), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } + + if (SqlMode != libRedOptions.SqlMode) + { + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(LibRedOptionsExtension.SqlMode), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public bool UseShortTextForSystemString { get; private set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public LibRedSqlMode SqlMode { get; private set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string? ConnectionString { get; private set; } + } +} diff --git a/src/LibRed/LibRed.EFCore/LibRed.EFCore.csproj b/src/LibRed/LibRed.EFCore/LibRed.EFCore.csproj index 3ae641df6..03f801522 100644 --- a/src/LibRed/LibRed.EFCore/LibRed.EFCore.csproj +++ b/src/LibRed/LibRed.EFCore/LibRed.EFCore.csproj @@ -1,26 +1,50 @@ - + $(JetTargetFramework) EntityFrameworkCore.LibRed EntityFrameworkCore.LibRed Entity Framework Core provider built on the LibRed ADO.NET provider and native engine. - - $(NoWarn);EF1001;CA1416 AnyCPU;x86;x64 - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/LibRed/LibRed.EFCore/Metadata/Conventions/LibRedConventionSetBuilder.cs b/src/LibRed/LibRed.EFCore/Metadata/Conventions/LibRedConventionSetBuilder.cs new file mode 100644 index 000000000..6c53c1ba8 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Metadata/Conventions/LibRedConventionSetBuilder.cs @@ -0,0 +1,61 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using LibRed.Data; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + public class LibRedConventionSetBuilder( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies, + ISqlGenerationHelper sqlGenerationHelper) + : RelationalConventionSetBuilder(dependencies, relationalDependencies) + { + public override ConventionSet CreateConventionSet() + { + var conventionSet = base.CreateConventionSet(); + + conventionSet.Add(new JetValueGenerationStrategyConvention(Dependencies, RelationalDependencies)); + conventionSet.Add(new RelationalMaxIdentifierLengthConvention(64, Dependencies, RelationalDependencies)); + conventionSet.Add(new JetIndexConvention(Dependencies, RelationalDependencies, sqlGenerationHelper)); + + conventionSet.Replace( + new JetOnDeleteConvention(Dependencies, RelationalDependencies)); + conventionSet.Replace( + new JetStoreGenerationConvention(Dependencies, RelationalDependencies)); + conventionSet.Replace( + new JetValueGenerationConvention(Dependencies, RelationalDependencies)); + conventionSet.Replace( + new JetSharedTableConvention(Dependencies, RelationalDependencies)); + + return conventionSet; + } + + public static ConventionSet Build() + { + using var serviceScope = CreateServiceScope(); + using var context = serviceScope.ServiceProvider.GetRequiredService(); + return ConventionSet.CreateConventionSet(context); + } + + public static ModelBuilder CreateModelBuilder() + { + using var serviceScope = CreateServiceScope(); + using var context = serviceScope.ServiceProvider.GetRequiredService(); + return new ModelBuilder(ConventionSet.CreateConventionSet(context), context.GetService()); + } + + private static IServiceScope CreateServiceScope() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkLibRed() + .AddDbContext((p, o) => o + .UseLibRed( + LibRedConnection.GetConnectionString("LibRed.accdb")) + .UseInternalServiceProvider(p)) + .BuildServiceProvider(); + + return serviceProvider.GetRequiredService().CreateScope(); + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Migrations/Internal/LibRedHistoryRepository.cs b/src/LibRed/LibRed.EFCore/Migrations/Internal/LibRedHistoryRepository.cs new file mode 100644 index 000000000..2726b1715 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Migrations/Internal/LibRedHistoryRepository.cs @@ -0,0 +1,450 @@ +using System.Text; +using EntityFrameworkCore.Jet.Internal; +using EntityFrameworkCore.Jet.Migrations.Internal; +using EntityFrameworkCore.Jet.Utilities; + +namespace EntityFrameworkCore.LibRed.Migrations.Internal +{ + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + /// + /// The service lifetime is . This means that each + /// instance will use its own instance of this service. + /// The implementation may depend on other services registered with any lifetime. + /// The implementation does not need to be thread-safe. + /// + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class LibRedHistoryRepository(HistoryRepositoryDependencies dependencies) : HistoryRepository(dependencies) + { + // 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); + + /// How long to keep trying for the migration lock before giving up. + /// + /// 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 + /// sp_getapplock timeout does. + /// + private static readonly TimeSpan _lockTimeout = TimeSpan.FromMinutes(1); + + public override LockReleaseBehavior LockReleaseBehavior => LockReleaseBehavior.Explicit; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public const string DefaultLockTableName = "__EFMigrationsLock"; + + /// + /// The name of the table that will serve as a database-wide lock for migrations. + /// + protected virtual string LockTableName { get; } = DefaultLockTableName; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override string ExistsSql => CreateExistsSql(TableName); + + private string CreateExistsSql(string tableName) + { + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + return $""" +SELECT * FROM `INFORMATION_SCHEMA.TABLES` WHERE `TABLE_NAME` = {stringTypeMapping.GenerateSqlLiteral(tableName)}; +"""; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override bool InterpretExistsResult(object? value) + { + // The exists query (`SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = …`) returns a row only + // when the table exists. An empty result is "not found" — but ADO.NET's ExecuteScalar returns C# null + // for an empty result set (per contract), while ACE's OLE DB path returns DBNull.Value. Treat BOTH as + // not-found, else a null (e.g. from LibRed's spec-correct reader) is misread as "exists" and the lock/ + // history table is never created. + return value is not null && value != DBNull.Value; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override string GetInsertScript(HistoryRow row) + { + Check.NotNull(row, nameof(row)); + + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + return new StringBuilder().Append("INSERT INTO ") + .Append(SqlGenerationHelper.DelimitIdentifier(TableName)) + .Append(" (") + .Append(SqlGenerationHelper.DelimitIdentifier(MigrationIdColumnName)) + .Append(", ") + .Append(SqlGenerationHelper.DelimitIdentifier(ProductVersionColumnName)) + .AppendLine(")") + .Append("VALUES (") + .Append(stringTypeMapping.GenerateSqlLiteral(row.MigrationId)) + .Append(", ") + .Append(stringTypeMapping.GenerateSqlLiteral(row.ProductVersion)) + .AppendLine(");") + .ToString(); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override string GetDeleteScript(string migrationId) + { + Check.NotEmpty(migrationId, nameof(migrationId)); + + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + return new StringBuilder().Append("DELETE FROM ") + .AppendLine(SqlGenerationHelper.DelimitIdentifier(TableName)) + .Append("WHERE ") + .Append(SqlGenerationHelper.DelimitIdentifier(MigrationIdColumnName)) + .Append(" = ") + .Append(stringTypeMapping.GenerateSqlLiteral(migrationId)) + .AppendLine(";") + .ToString(); + } + + public override IMigrationsDatabaseLock AcquireDatabaseLock() + { + Dependencies.MigrationsLogger.AcquiringMigrationLock(); + + if (!InterpretExistsResult( + Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName)) + .ExecuteScalar(CreateRelationalCommandParameters()))) + { + try + { + CreateLockTableCommand().ExecuteNonQuery(CreateRelationalCommandParameters()); + } + catch (DbException e) + { + if (!e.Message.Contains("already exists")) throw; + } + } + + var retryDelay = _retryDelay; + var deadline = DateTime.UtcNow + _lockTimeout; + while (true) + { + 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 + { + insertCount = (int?)CreateInsertLockCommand(DateTimeOffset.UtcNow) + .ExecuteScalar(CreateRelationalCommandParameters()); + } + catch (DbException e) + { + if (!e.Message.Contains("duplicate")) throw; + } + if ((int)insertCount! == 1) + { + // Built only once the lock is actually ours; the old loop constructed one per attempt and + // dropped it on every miss. + return CreateMigrationDatabaseLock(); + } + + if (DateTime.UtcNow >= deadline) + { + throw new TimeoutException(LockTimeoutMessage()); + } + + Thread.Sleep(JitteredDelay(retryDelay)); + retryDelay = EscalateDelay(retryDelay); + } + } + + public override async Task AcquireDatabaseLockAsync( + CancellationToken cancellationToken = default) + { + Dependencies.MigrationsLogger.AcquiringMigrationLock(); + + if (!InterpretExistsResult( + await Dependencies.RawSqlCommandBuilder.Build(CreateExistsSql(LockTableName)) + .ExecuteScalarAsync(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) + { + 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 CreateMigrationDatabaseLock(); + } + + if (DateTime.UtcNow >= deadline) + { + throw new TimeoutException(LockTimeoutMessage()); + } + + await Task.Delay(JitteredDelay(retryDelay), cancellationToken).ConfigureAwait(false); + retryDelay = EscalateDelay(retryDelay); + } + } + + /// + /// 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. + /// + private static TimeSpan JitteredDelay(TimeSpan delay) + => TimeSpan.FromTicks((long)(delay.Ticks * (0.75 + (Random.Shared.NextDouble() / 2.0)))); + + /// Doubles the backoff up to and holds there. + 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}` ( + `Id` INTEGER NOT NULL CONSTRAINT `PK_{LockTableName}` PRIMARY KEY, + `Timestamp` TEXT NOT NULL +); +"""); + + /// + /// Takes the migration lock, reporting 1 when this connection wrote the row and 0 when someone else + /// already holds it — the contract SQLite gets from INSERT OR IGNORE …; SELECT changes(). + /// + /// Jet/ACE has no INSERT OR IGNORE or MERGE, so the insert is made conditional with a + /// WHERE NOT EXISTS guard and reports what it actually wrote via @@ROWCOUNT. That keeps + /// losing the race on the ordinary result path instead of raising a duplicate-key error the caller has + /// to recognise by message text. + /// + /// + /// Unlike Jet/ACE, LibRed accepts a FROM-less SELECT, so the source needs no DUAL stand-in and no + /// (SELECT COUNT(*) FROM …) wrapper around it. + /// + /// + /// The guard is not atomic — unlike SQLite's OR IGNORE, two connections can both evaluate + /// it as true — so the caller keeps its duplicate-key catch as the backstop. The difference is that the + /// exception becomes the rare racing path rather than the normal contention path. + /// + /// + private IRelationalCommand CreateInsertLockCommand(DateTimeOffset timestamp) + { + var timestampLiteral = Dependencies.TypeMappingSource.GetMapping(typeof(DateTimeOffset)).GenerateSqlLiteral(timestamp); + + return Dependencies.RawSqlCommandBuilder.Build($""" +INSERT INTO `{LockTableName}` (`Id`, `Timestamp`) +SELECT 1, {timestampLiteral} +WHERE NOT EXISTS (SELECT * FROM `{LockTableName}` WHERE `Id` = 1); +SELECT @@ROWCOUNT; +"""); + } + + private IRelationalCommand CreateDeleteLockCommand(int? id = null) + { + var sql = $""" +DELETE FROM `{LockTableName}` +"""; + if (id != null) + { + sql += $""" WHERE `Id` = {id}"""; + } + sql += ";"; + return Dependencies.RawSqlCommandBuilder.Build(sql); + } + + private JetMigrationDatabaseLock CreateMigrationDatabaseLock() + => new(CreateDeleteLockCommand(), CreateRelationalCommandParameters(), this); + + private RelationalCommandParameterObject CreateRelationalCommandParameters() + => new( + Dependencies.Connection, + null, + null, + Dependencies.CurrentContext.Context, + Dependencies.CommandLogger, CommandSource.Migrations); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override string GetCreateIfNotExistsScript() + { + var builder = new IndentedStringBuilder(); + + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + builder + .Append("IF NOT EXISTS (SELECT * FROM `INFORMATION_SCHEMA.TABLES` WHERE `TABLE_NAME` = ") + .Append(stringTypeMapping.GenerateSqlLiteral(TableName)) + .Append(") THEN "); + using (builder.Indent()) + { + builder.AppendLines(GetCreateScript()); + } + builder.AppendLine(";"); + + return builder.ToString(); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override string GetBeginIfNotExistsScript(string migrationId) + { + throw new NotSupportedException(JetStrings.MigrationScriptGenerationNotSupported); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override string GetBeginIfExistsScript(string migrationId) + { + throw new NotSupportedException(JetStrings.MigrationScriptGenerationNotSupported); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override string GetEndIfScript() + { + throw new NotSupportedException(JetStrings.MigrationScriptGenerationNotSupported); + } + + public override IReadOnlyList GetAppliedMigrations() + { + var rows = new List(); + //Note the exists check opens a new connection with adox/dao. If doing within a transaction it wont find the table until the transaction is committed. Just read anyway + //No op if the table does not exist + //if (Exists()) + { + var command = Dependencies.RawSqlCommandBuilder.Build(GetAppliedMigrationsSql); + + using var reader = command.ExecuteReader( + new RelationalCommandParameterObject( + Dependencies.Connection, + null, + null, + Dependencies.CurrentContext.Context, + Dependencies.CommandLogger, CommandSource.Migrations)); + while (reader.Read()) + { + rows.Add(new HistoryRow(reader.DbDataReader.GetString(0), reader.DbDataReader.GetString(1))); + } + } + + return rows; + } + + public override async Task> GetAppliedMigrationsAsync(CancellationToken cancellationToken = new CancellationToken()) + { + var rows = new List(); + //Note the exists check opens a new connection with adox/dao. If doing within a transaction it wont find the table until the transaction is committed. Just read anyway + //No op if the table does not exist + //if (await ExistsAsync(cancellationToken).ConfigureAwait(false)) + { + var command = Dependencies.RawSqlCommandBuilder.Build(GetAppliedMigrationsSql); + + var reader = await command.ExecuteReaderAsync( + new RelationalCommandParameterObject( + Dependencies.Connection, + null, + null, + Dependencies.CurrentContext.Context, + Dependencies.CommandLogger, CommandSource.Migrations), + cancellationToken).ConfigureAwait(false); + + await using var _ = reader.ConfigureAwait(false); + + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + rows.Add(new HistoryRow(reader.DbDataReader.GetString(0), reader.DbDataReader.GetString(1))); + } + } + + return rows; + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Migrations/LibRedMigrationsSqlGenerator.cs b/src/LibRed/LibRed.EFCore/Migrations/LibRedMigrationsSqlGenerator.cs new file mode 100644 index 000000000..0979bb561 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Migrations/LibRedMigrationsSqlGenerator.cs @@ -0,0 +1,1128 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using EntityFrameworkCore.Jet.Internal; +using EntityFrameworkCore.Jet.Metadata; +using EntityFrameworkCore.Jet.Metadata.Internal; +using EntityFrameworkCore.Jet.Migrations.Operations; +using EntityFrameworkCore.Jet.Storage.Internal; +using EntityFrameworkCore.Jet.Update.Internal; +using EntityFrameworkCore.Jet.Utilities; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using System.Text; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Migrations +{ + /// + /// + /// Jet-specific implementation of . + /// + /// + /// The service lifetime is . This means that each + /// instance will use its own instance of this service. + /// The implementation may depend on other services registered with any lifetime. + /// The implementation does not need to be thread-safe. + /// + /// + /// + /// Creates a new instance. + /// + /// Parameter object containing dependencies for this service. + /// The command batch preparer. + public class LibRedMigrationsSqlGenerator( + MigrationsSqlGeneratorDependencies dependencies, + ICommandBatchPreparer commandBatchPreparer) : MigrationsSqlGenerator(dependencies) + { + private IReadOnlyList _operations = null!; + private readonly ICommandBatchPreparer _commandBatchPreparer = commandBatchPreparer; + + /// + /// Generates commands from a list of operations. + /// + /// The operations. + /// The target model which may be if the operations exist without a model. + /// The options to use when generating commands. + /// The list of commands to be executed or scripted. + public override IReadOnlyList Generate( + IReadOnlyList operations, + IModel? model = null, + MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default) + { + _operations = operations; + try + { + return base.Generate(operations, model, options); + } + finally + { + _operations = null!; + } + } + + /// + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// + /// This method uses a double-dispatch mechanism to call one of the 'Generate' methods that are + /// specific to a certain subtype of . Typically database providers + /// will override these specific methods rather than this method. However, providers can override + /// this methods to handle provider-specific operations. + /// + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + switch (operation) + { + case JetCreateDatabaseOperation createDatabaseOperation: + Generate(createDatabaseOperation, model, builder); + break; + case JetDropDatabaseOperation dropDatabaseOperation: + Generate(dropDatabaseOperation, model, builder); + break; + default: + base.Generate(operation, model, builder); + break; + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + AddColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + if (IsIdentity(operation)) + { + // NB: This gets added to all added non-nullable columns by MigrationsModelDiffer. We need to suppress + // it, here because Jet can't have both IDENTITY and a DEFAULT constraint on the same column. + operation.DefaultValue = null; + } + + base.Generate(operation, model, builder, terminate: false); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + AddForeignKeyOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + AddPrimaryKeyOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + AlterColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + if (operation[RelationalAnnotationNames.ColumnOrder] != operation.OldColumn[RelationalAnnotationNames.ColumnOrder]) + { + Dependencies.MigrationsLogger.ColumnOrderIgnoredWarning(operation); + } + + IEnumerable? indexesToRebuild = null; + var column = model?.GetRelationalModel().FindTable(operation.Table, operation.Schema) + ?.Columns.FirstOrDefault(c => c.Name == operation.Name); + + if (operation.ComputedColumnSql != operation.OldColumn.ComputedColumnSql) + { + var dropColumnOperation = new DropColumnOperation + { + Schema = operation.Schema, + Table = operation.Table, + Name = operation.Name + }; + if (column != null) + { + dropColumnOperation.AddAnnotations(column.GetAnnotations()); + } + + var addColumnOperation = new AddColumnOperation + { + Schema = operation.Schema, + Table = operation.Table, + Name = operation.Name, + ClrType = operation.ClrType, + ColumnType = operation.ColumnType, + IsUnicode = operation.IsUnicode, + IsFixedLength = operation.IsFixedLength, + MaxLength = operation.MaxLength, + Precision = operation.Precision, + Scale = operation.Scale, + IsRowVersion = operation.IsRowVersion, + IsNullable = operation.IsNullable, + DefaultValue = operation.DefaultValue, + DefaultValueSql = operation.DefaultValueSql, + ComputedColumnSql = operation.ComputedColumnSql, + IsStored = operation.IsStored, + Comment = operation.Comment, + Collation = operation.Collation + }; + addColumnOperation.AddAnnotations(operation.GetAnnotations()); + + // TODO: Use a column rebuild instead + indexesToRebuild = GetIndexesToRebuild(column, operation).ToList(); + DropIndexes(indexesToRebuild, builder); + Generate(dropColumnOperation, model, builder, terminate: false); + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + Generate(addColumnOperation, model, builder); + CreateIndexes(indexesToRebuild, builder); + builder.EndCommand(); + + return; + } + + var columnType = operation.ColumnType + ?? GetColumnType( + operation.Schema, + operation.Table, + operation.Name, + operation, + model); + + var narrowed = false; + var oldColumnSupported = IsOldColumnSupported(model); + + if (oldColumnSupported) + { + if (IsIdentity(operation) != IsIdentity(operation.OldColumn)) + { + throw new InvalidOperationException(JetStrings.AlterIdentityColumn); + } + + var oldType = operation.OldColumn.ColumnType + ?? GetColumnType( + operation.Schema, + operation.Table, + operation.Name, + operation.OldColumn, + model); + narrowed = columnType != oldType + || operation.Collation != operation.OldColumn.Collation + || operation is { IsNullable: false, OldColumn.IsNullable: true }; + } + + if (narrowed) + { + indexesToRebuild = GetIndexesToRebuild(column, operation).ToList(); + DropIndexes(indexesToRebuild, builder); + } + + var newAnnotations = operation.GetAnnotations().Where(a => a.Name != JetAnnotationNames.Identity); + var oldAnnotations = operation.OldColumn.GetAnnotations().Where(a => a.Name != JetAnnotationNames.Identity); + + var alterStatementNeeded = narrowed + || !oldColumnSupported + || operation.ClrType != operation.OldColumn.ClrType + || columnType != operation.OldColumn.ColumnType + || operation.IsUnicode != operation.OldColumn.IsUnicode + || operation.IsFixedLength != operation.OldColumn.IsFixedLength + || operation.MaxLength != operation.OldColumn.MaxLength + || operation.Precision != operation.OldColumn.Precision + || operation.Scale != operation.OldColumn.Scale + || operation.IsRowVersion != operation.OldColumn.IsRowVersion + || operation.IsNullable != operation.OldColumn.IsNullable + || operation.Collation != operation.OldColumn.Collation + || operation.DefaultValue != operation.OldColumn.DefaultValue + || HasDifferences(newAnnotations, oldAnnotations); + + var (oldDefaultValue, oldDefaultValueSql) = (operation.OldColumn.DefaultValue, operation.OldColumn.DefaultValueSql); + + if (alterStatementNeeded + || !Equals(operation.DefaultValue, oldDefaultValue) + || operation.DefaultValueSql != oldDefaultValueSql) + { + DropDefaultConstraint(operation.Table, operation.Name, builder); + (oldDefaultValue, oldDefaultValueSql) = (null, null); + } + + // The column is being made non-nullable. Generate an update statement before doing that, to convert any existing null values to + // the default value + if (operation is { IsNullable: false, OldColumn.IsNullable: true } + && (operation.DefaultValueSql is not null || operation.DefaultValue is not null)) + { + string defaultValueSql; + if (operation.DefaultValueSql is not null) + { + defaultValueSql = operation.DefaultValueSql; + } + else + { + Check.DebugAssert(operation.DefaultValue is not null, "operation.DefaultValue is not null"); + + var typeMapping = (columnType != null + ? Dependencies.TypeMappingSource.FindMapping(operation.DefaultValue.GetType(), columnType) + : null) + ?? Dependencies.TypeMappingSource.GetMappingForValue(operation.DefaultValue); + + defaultValueSql = typeMapping.GenerateSqlLiteral(operation.DefaultValue); + } + + var updateBuilder = new StringBuilder() + .Append("UPDATE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" SET ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" = ") + .Append(defaultValueSql) + .Append(" WHERE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" IS NULL"); + + if (Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) + { + builder + .Append("EXEC(N'") + .Append(updateBuilder.ToString().TrimEnd('\n', '\r', ';').Replace("'", "''")) + .Append("')"); + } + else + { + builder.Append(updateBuilder.ToString()); + } + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + if (alterStatementNeeded) + { + builder + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" ALTER COLUMN "); + + // NB: ComputedColumnSql, IsStored, DefaultValueSql, Comment, ValueGenerationStrategy, and Identity are + // handled elsewhere. Don't copy them here. + //JET: DefaultValue is part of the ALTER COLUMN. Not a separate ALTER + var definitionOperation = new AlterColumnOperation + { + Schema = operation.Schema, + Table = operation.Table, + Name = operation.Name, + ClrType = operation.ClrType, + ColumnType = operation.ColumnType, + IsUnicode = operation.IsUnicode, + IsFixedLength = operation.IsFixedLength, + MaxLength = operation.MaxLength, + Precision = operation.Precision, + Scale = operation.Scale, + IsRowVersion = operation.IsRowVersion, + IsNullable = operation.IsNullable, + Collation = operation.Collation, + OldColumn = operation.OldColumn, + DefaultValue = operation.DefaultValue, + DefaultValueSql = operation.DefaultValueSql + }; + definitionOperation.AddAnnotations( + operation.GetAnnotations().Where( + a => a.Name != JetAnnotationNames.ValueGenerationStrategy + && a.Name != JetAnnotationNames.Identity)); + + ColumnDefinition( + operation.Schema, + operation.Table, + operation.Name, + definitionOperation, + model, + builder); + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + if (narrowed) + { + CreateIndexes(indexesToRebuild!, builder); + } + + builder.EndCommand(); + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + RenameIndexOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + if (string.IsNullOrEmpty(operation.Table)) + { + throw new InvalidOperationException(JetStrings.IndexTableRequired); + } + + // Provider pseudo-SQL (Jet/ACE has no rename syntax): kept in the same `ALTER TABLE
RENAME + // TO ` family as the table and column renames, rather than a dotted + // `RENAME INDEX
.` form that named the table differently from its siblings. + builder.Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table)) + .Append(" RENAME INDEX ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" TO ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.NewName)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + CreateTableOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + RenameTableOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + // CHECK: Rename table operations require extensions like ADOX or DAO. + // A native way to do this would be to: + // 1. CREATE TABLE `destination table` + // 2. INSERT INTO ... SELECT ... FROM + // 3. DROP TABLE `source table` + // 4. Recrete indices and references. + builder.Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" RENAME TO ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.NewName!)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + DropTableOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + CreateIndexOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + DropPrimaryKeyOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + } + + /// + /// Generates a SQL fragment for extras (filter, included columns, options) of an index from a . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to add the SQL fragment. + protected override void IndexOptions(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + if (operation is CreateIndexOperation createIndexOperation + && !string.IsNullOrEmpty(createIndexOperation.Filter)) + { + builder + .Append(" WITH ") + .Append(createIndexOperation.Filter); + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected virtual void Generate( + JetCreateDatabaseOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + builder + .Append("CREATE DATABASE ") + .Append(stringTypeMapping.GenerateSqlLiteral(operation.Name)); + + if (!string.IsNullOrEmpty(operation.Password)) + { + builder + .Append(" PASSWORD ") + .Append(stringTypeMapping.GenerateSqlLiteral(operation.Password)); + } + + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: true); + } + + // TODO: The file name and data directory should be expanded when the CREATE/DROP DATABASE operation gets + // executed, not when it gets created. + /* + private static string ExpandFileName(string fileName) + { + Check.NotNull(fileName, nameof(fileName)); + + if (fileName.StartsWith("|DataDirectory|", StringComparison.OrdinalIgnoreCase)) + { + var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory") as string; + if (string.IsNullOrEmpty(dataDirectory)) + { + dataDirectory = AppDomain.CurrentDomain.BaseDirectory; + } + + fileName = Path.Combine(dataDirectory, fileName.Substring("|DataDirectory|".Length)); + } + + return Path.GetFullPath(fileName); + } + */ + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected virtual void Generate( + JetDropDatabaseOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + builder + .Append("DROP DATABASE ") + //.Append(ExpandFileName(operation.Name)) + .Append(stringTypeMapping.GenerateSqlLiteral(operation.Name)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: true); + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + AlterDatabaseOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + } + + /// + /// Builds commands for the given by making calls on the given + /// , and then terminates the final command. + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + DropForeignKeyOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + DropIndexOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + if (string.IsNullOrEmpty(operation.Table)) + { + throw new InvalidOperationException(JetStrings.IndexTableRequired); + } + builder + .Append("DROP INDEX ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" ON ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table!)); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + DropColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + DropDefaultConstraint(operation.Table, operation.Name, builder); + base.Generate(operation, model, builder, terminate: false); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + RenameColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + builder.Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table)) + .Append(" RENAME COLUMN ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" TO ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.NewName)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + + /// + /// Builds commands for the given by making calls on the given + /// , and then terminates the final command. + /// + /// The operation. + /// The target model which may be null if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate(SqlOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + builder.AppendLine(operation.Sql); + + EndStatement(builder, operation.SuppressTransaction); + } + + protected override void Generate( + InsertDataOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + var sqlBuilder = new StringBuilder(); + + var modificationCommands = GenerateModificationCommands(operation, model).ToList(); + var updateSqlGenerator = (IJetUpdateSqlGenerator)Dependencies.UpdateSqlGenerator; + + foreach (var batch in _commandBatchPreparer.CreateCommandBatches(modificationCommands, moreCommandSets: true)) + { + updateSqlGenerator.AppendBulkInsertOperation(sqlBuilder, batch.ModificationCommands, commandPosition: 0); + } + + if (Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) + { + builder + .Append("EXEC('") + .Append(sqlBuilder.ToString().TrimEnd('\n', '\r', ';').Replace("'", "''")) + .Append("')") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + else + { + builder.Append(sqlBuilder.ToString()); + } + + if (terminate) + { + builder.EndCommand(); + } + } + + protected override void ColumnDefinition( + string? schema, + string table, + string name, + ColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + if (operation.ComputedColumnSql != null) + { + if (decimal.TryParse(operation.ComputedColumnSql, out decimal result)) + { + operation.DefaultValue = result; + } + else + { + ComputedColumnDefinition(schema, table, name, operation, model, builder); + return; + } + } + + var columnType = GetColumnType(schema, table, name, operation, model); + //int has no size - ignore + if (columnType != null && columnType.StartsWith("int(")) + { + columnType = "int"; + } + builder + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)) + .Append(" ") + .Append(columnType ?? ""); + + builder.Append(operation.IsNullable ? " NULL" : " NOT NULL"); + + DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, builder); + } + + protected override string GetColumnType( + string? schema, + string table, + string name, + ColumnOperation operation, + IModel? model) + { + var storeType = operation.ColumnType; + + if (IsIdentity(operation) && + (storeType == null || Dependencies.TypeMappingSource.FindMapping(storeType) is IntTypeMapping or ShortTypeMapping)) + { + // This column represents the actual identity. + storeType = "counter"; + } + else if (storeType != null && + IsExplicitIdentityColumnType(storeType)) + { + // While this column uses an identity type (e.g. counter), it is not an actual identity column, because + // it was not marked as one. + storeType = "integer"; + } + + storeType ??= base.GetColumnType(schema, table, name, operation, model); + + if (string.Equals(storeType, "counter", StringComparison.OrdinalIgnoreCase) && + operation[JetAnnotationNames.Identity] is string identity && + !string.IsNullOrEmpty(identity) && + identity != "1, 1") + { + storeType += $"({identity})"; + } + + if (storeType != null && storeType.Contains("bigint", StringComparison.OrdinalIgnoreCase)) + { + storeType = storeType.Replace("bigint", "decimal"); + } + + if (storeType != null) + { + return storeType; + } + + var fullTableName = schema != null ? $"{schema}.{table}" : table; + throw new InvalidOperationException( + RelationalStrings.UnsupportedTypeForColumn(fullTableName, name, operation.ClrType?.Name ?? "unknown")); + } + + /// + /// Generates a SQL fragment for the given referential action. + /// + /// The referential action. + /// The command builder to use to add the SQL fragment. + protected override void ForeignKeyAction(ReferentialAction referentialAction, MigrationCommandListBuilder builder) + { + if (referentialAction == ReferentialAction.Restrict) + { + builder.Append("NO ACTION"); + } + else + { + base.ForeignKeyAction(referentialAction, builder); + } + } + + /// + /// Generates a SQL fragment for the default constraint of a column. + /// + /// The default value for the column. + /// The SQL expression to use for the column's default constraint. + /// Store/database type of the column. + /// The command builder to use to add the SQL fragment. + protected override void DefaultValue( + object? defaultValue, + string? defaultValueSql, + string? columnType, + MigrationCommandListBuilder builder) + { + if (defaultValueSql != null) + { + builder + .Append(" DEFAULT ") + .Append(defaultValueSql); + } + else if (defaultValue != null) + { + var typeMapping = (columnType != null + ? Dependencies.TypeMappingSource.FindMapping(defaultValue.GetType(), columnType) + : null) ?? + Dependencies.TypeMappingSource.GetMappingForValue(defaultValue); + + var defaultClauseLiteral = typeMapping.GetType().FullName == "EntityFrameworkCore.Jet.Storage.Internal.JetDateTimeTypeMapping" + ? typeMapping.GetType().GetMethod( + "GenerateNonNullSqlLiteral", + BindingFlags.Instance | BindingFlags.Public, + [typeof(object), typeof(bool)]) + : null; + + defaultValue = defaultClauseLiteral != null + ? (string)defaultClauseLiteral.Invoke(typeMapping, [defaultValue, true])! + : typeMapping.GenerateSqlLiteral(defaultValue); + + builder + .Append(" DEFAULT ") + .Append((string)defaultValue); + } + } + + /// + /// Generates a SQL fragment to drop default constraints for a column. + /// + /// The table that contains the column. + /// The column. + /// The command builder to use to add the SQL fragment. + protected virtual void DropDefaultConstraint( + string tableName, + string columnName, + MigrationCommandListBuilder builder) + { + builder + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName)) + .Append(" ALTER COLUMN ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(columnName)) + .Append(" DROP DEFAULT") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + /// + /// Gets the list of indexes that need to be rebuilt when the given column is changing. + /// + /// The column. + /// The operation which may require a rebuild. + /// The list of indexes affected. + protected virtual IEnumerable GetIndexesToRebuild( + IColumn? column, + MigrationOperation currentOperation) + { + if (column == null) + { + yield break; + } + + var table = column.Table; + var createIndexOperations = _operations.SkipWhile(o => o != currentOperation) + .Skip(1) + .OfType() + .ToList(); + foreach (var index in table.Indexes) + { + var indexName = index.Name; + if (createIndexOperations.Any(o => o.Name == indexName)) + { + continue; + } + + if (index.Columns.Any(c => c == column)) + { + yield return index; + } + else if (index[JetAnnotationNames.Include] is IReadOnlyList includeColumns + && includeColumns.Contains(column.Name)) + { + yield return index; + } + } + } + + /// + /// Generates SQL to drop the given indexes. + /// + /// The indexes to drop. + /// The command builder to use to build the commands. + protected virtual void DropIndexes( + IEnumerable indexes, + MigrationCommandListBuilder builder) + { + foreach (var index in indexes) + { + var table = index.Table; + var operation = new DropIndexOperation + { + Schema = table.Schema, + Table = table.Name, + Name = index.Name + }; + operation.AddAnnotations(index.GetAnnotations()); + + Generate(operation, table.Model.Model, builder, terminate: false); + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + } + + /// + /// Generates SQL to create the given indexes. + /// + /// The indexes to create. + /// The command builder to use to build the commands. + protected virtual void CreateIndexes( + IEnumerable indexes, + MigrationCommandListBuilder builder) + { + foreach (var index in indexes) + { + Generate(CreateIndexOperation.CreateFrom(index), index.Table.Model.Model, builder, terminate: false); + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + } + + private static bool IsIdentity(ColumnOperation operation) + => operation[JetAnnotationNames.Identity] != null + || operation[JetAnnotationNames.ValueGenerationStrategy] as JetValueGenerationStrategy? + == JetValueGenerationStrategy.IdentityColumn; + + private static bool IsExplicitIdentityColumnType(string columnType) + => string.Equals("counter", columnType, StringComparison.OrdinalIgnoreCase) || + string.Equals("identity", columnType, StringComparison.OrdinalIgnoreCase) || + string.Equals("autoincrement", columnType, StringComparison.OrdinalIgnoreCase); + + private static bool HasDifferences(IEnumerable source, IEnumerable target) + { + var targetAnnotations = target.ToDictionary(a => a.Name); + + var count = 0; + foreach (var sourceAnnotation in source) + { + if (!targetAnnotations.TryGetValue(sourceAnnotation.Name, out var targetAnnotation) + || !Equals(sourceAnnotation.Value, targetAnnotation.Value)) + { + return true; + } + + count++; + } + + return count != targetAnnotations.Count; + } + + #region Schemas not supported + + protected override void Generate(EnsureSchemaOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + } + + protected override void Generate(DropSchemaOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + } + + #endregion + + #region Sequences not supported + + // JET does not have sequences + protected override void Generate(RestartSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + throw new NotSupportedException("JET does not support sequences"); + } + + protected override void Generate(CreateSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + throw new NotSupportedException("JET does not support sequences"); + } + + protected override void Generate(RenameSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + throw new NotSupportedException("JET does not support sequences"); + } + + protected override void Generate(AlterSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + throw new NotSupportedException("JET does not support sequences"); + } + + protected override void Generate(DropSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + throw new NotSupportedException("JET does not support sequences"); + } + + #endregion + + } +} \ No newline at end of file diff --git a/src/LibRed/LibRed.EFCore/Query/Internal/LibRedParameterBasedSqlProcessor.cs b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedParameterBasedSqlProcessor.cs new file mode 100644 index 000000000..89112bd07 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedParameterBasedSqlProcessor.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using EntityFrameworkCore.Jet.Query.Internal; +using EntityFrameworkCore.Jet.Utilities; +using EntityFrameworkCore.LibRed.Infrastructure; + +namespace EntityFrameworkCore.LibRed.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class LibRedParameterBasedSqlProcessor( + RelationalParameterBasedSqlProcessorDependencies dependencies, + RelationalParameterBasedSqlProcessorParameters parameters, + LibRedSqlMode sqlMode) : RelationalParameterBasedSqlProcessor(dependencies, parameters) +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override Expression Process(Expression queryExpression, ParametersCacheDecorator parametersDecorator) + { + var optimizedQueryExpression = new JetZeroLimitConverter(Dependencies.SqlExpressionFactory) + .Process(queryExpression, parametersDecorator); + + optimizedQueryExpression = new JetDateTimeRangeConverter(Dependencies.SqlExpressionFactory) + .Process(optimizedQueryExpression, parametersDecorator); + + var afterBaseProcessing = base.Process(optimizedQueryExpression, parametersDecorator); + + var afterSearchConditionConversion = afterBaseProcessing;/*new SearchConditionConverter(Dependencies.SqlExpressionFactory) + .Visit(afterBaseProcessing); + + */ + + // Both of these are Jet-dialect workarounds, so they run in compatible mode only. + if (sqlMode == LibRedSqlMode.Compatible) + { + // Guard row-independent projections inside LEFT JOIN subqueries. This must run AFTER base.Process: the + // guard is CASE WHEN IS NULL THEN NULL ELSE END, and the anchor is non-nullable within + // the subquery, so SqlNullabilityProcessor proves the test false and folds the CASE straight back to its + // ELSE branch. Applied here it survives to SQL generation. + afterSearchConditionConversion = new JetOuterJoinProjectionGuardExpressionVisitor( + Dependencies.SqlExpressionFactory).Visit(afterSearchConditionConversion); + + // Run the compatibility checks as late in the query pipeline (before the actual SQL translation happens) as reasonable. + // In extended mode these constructs - ROW_NUMBER, CROSS/OUTER APPLY, EXCEPT, INTERSECT - are exactly what + // LibRed is meant to attempt rather than reject. + afterSearchConditionConversion = new JetCompatibilityExpressionVisitor().Visit(afterSearchConditionConversion); + } + + return afterSearchConditionConversion; + } + + /// + protected override Expression ProcessSqlNullability(Expression selectExpression, ParametersCacheDecorator Decorator) + { + return new JetSqlNullabilityProcessor(Dependencies, Parameters).Process( + selectExpression, Decorator); + } +} diff --git a/src/LibRed/LibRed.EFCore/Query/Internal/LibRedParameterBasedSqlProcessorFactory.cs b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedParameterBasedSqlProcessorFactory.cs new file mode 100644 index 000000000..0a0c7430e --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedParameterBasedSqlProcessorFactory.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using EntityFrameworkCore.LibRed.Infrastructure.Internal; + +namespace EntityFrameworkCore.LibRed.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class LibRedParameterBasedSqlProcessorFactory( + RelationalParameterBasedSqlProcessorDependencies dependencies, + ILibRedOptions options) : IRelationalParameterBasedSqlProcessorFactory +{ + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalParameterBasedSqlProcessorDependencies Dependencies { get; } = dependencies; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public RelationalParameterBasedSqlProcessor Create(RelationalParameterBasedSqlProcessorParameters parameters) + => new LibRedParameterBasedSqlProcessor(Dependencies, parameters, options.SqlMode); +} diff --git a/src/LibRed/LibRed.EFCore/Query/Internal/LibRedQueryTranslationPostprocessor.cs b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedQueryTranslationPostprocessor.cs new file mode 100644 index 000000000..a7337b32f --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedQueryTranslationPostprocessor.cs @@ -0,0 +1,110 @@ +using System.Diagnostics.CodeAnalysis; +using EntityFrameworkCore.Jet.Internal; +using EntityFrameworkCore.Jet.Query.Internal; +using EntityFrameworkCore.LibRed.Infrastructure; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace EntityFrameworkCore.LibRed.Query.Internal +{ + public class LibRedQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor + { + private static readonly FieldInfo SelectExpressionIdentifierField = typeof(SelectExpression).GetField( + "_identifier", + BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("Could not find SelectExpression._identifier."); + + private readonly IRelationalTypeMappingSource _relationalTypeMappingSource; + private readonly SkipWithoutOrderByInSplitQueryVerifier _skipWithoutOrderByInSplitQueryVerifier = new(); + private readonly JetLiftOrderByPostprocessor _liftOrderByPostprocessor; + private readonly JetSkipTakePostprocessor _skipTakePostprocessor; + private readonly LibRedSqlMode _sqlMode; + + public LibRedQueryTranslationPostprocessor( + QueryTranslationPostprocessorDependencies dependencies, + RelationalQueryTranslationPostprocessorDependencies relationalDependencies, + RelationalQueryCompilationContext queryCompilationContext, + IRelationalTypeMappingSource relationalTypeMappingSource, + LibRedSqlMode sqlMode) + : base(dependencies, relationalDependencies, queryCompilationContext) + { + _relationalTypeMappingSource = relationalTypeMappingSource; + _sqlMode = sqlMode; + _liftOrderByPostprocessor = new JetLiftOrderByPostprocessor(relationalTypeMappingSource, relationalDependencies.SqlExpressionFactory, queryCompilationContext.SqlAliasManager); + _skipTakePostprocessor = new JetSkipTakePostprocessor(relationalTypeMappingSource, + relationalDependencies.SqlExpressionFactory, ((RelationalQueryCompilationContext)QueryCompilationContext).QuerySplittingBehavior); + } + + public override Expression Process(Expression query) + { + query = _skipTakePostprocessor.Process(query); + //query = _liftOrderByPostprocessor.Process(query); + query = base.Process(query); + + var identifiers = GetIdentifiers(query); + + if (identifiers.Count > 0 + && query is ShapedQueryExpression { QueryExpression: SelectExpression selectExpression } + && !selectExpression.Orderings.Any( + ordering => ordering.Expression.Equals(identifiers[^1].Column)) && selectExpression.Orderings.Any()) + { + selectExpression.AppendOrdering( + new OrderingExpression(identifiers[^1].Column, ascending: true)); + } + + //query = _skipTakePostprocessor.Process(query); + //query = _skipWithoutOrderByInSplitQueryVerifier.Visit(query); + //query = _skipTakePostprocessor.Process(query); + + // The ORDER BY lift is a Jet-dialect workaround; extended mode uses the plainer form and skips it. + if (_sqlMode == LibRedSqlMode.Compatible) + { + query = _liftOrderByPostprocessor.Process(query); + } + + return query; + } + + private static IReadOnlyList<(ColumnExpression Column, ValueComparer Comparer)> GetIdentifiers(Expression query) + { + if (query is not ShapedQueryExpression { QueryExpression: SelectExpression selectExpression }) + { + return []; + } + + return (IReadOnlyList<(ColumnExpression Column, ValueComparer Comparer)>)SelectExpressionIdentifierField.GetValue(selectExpression)!; + } + + private sealed class SkipWithoutOrderByInSplitQueryVerifier : ExpressionVisitor + { + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + switch (expression) + { + case ShapedQueryExpression shapedQueryExpression: + Visit(shapedQueryExpression.ShaperExpression); + return shapedQueryExpression; + + case RelationalSplitCollectionShaperExpression relationalSplitCollectionShaperExpression: + foreach (var table in relationalSplitCollectionShaperExpression.SelectExpression.Tables) + { + Visit(table); + } + + Visit(relationalSplitCollectionShaperExpression.InnerShaper); + + return relationalSplitCollectionShaperExpression; + + case SelectExpression { Offset: not null, Orderings.Count: 0 }: + throw new InvalidOperationException(JetStrings.SplitQueryOffsetWithoutOrderBy); + + case UpdateExpression or DeleteExpression: + return expression; + + default: + return base.Visit(expression); + } + } + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Query/Internal/LibRedQueryTranslationPostprocessorFactory.cs b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedQueryTranslationPostprocessorFactory.cs new file mode 100644 index 000000000..57f81cf64 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedQueryTranslationPostprocessorFactory.cs @@ -0,0 +1,33 @@ +using EntityFrameworkCore.LibRed.Infrastructure.Internal; + +namespace EntityFrameworkCore.LibRed.Query.Internal +{ + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + /// + /// The service lifetime is . This means a single instance + /// is used by many instances. The implementation must be thread-safe. + /// This service cannot depend on services registered as . + /// + /// + public class LibRedQueryTranslationPostprocessorFactory( + QueryTranslationPostprocessorDependencies dependencies, + RelationalQueryTranslationPostprocessorDependencies relationalDependencies, + IRelationalTypeMappingSource relationalTypeMappingSource, + ILibRedOptions options) + : IQueryTranslationPostprocessorFactory + { + public virtual QueryTranslationPostprocessor Create(QueryCompilationContext queryCompilationContext) + => new LibRedQueryTranslationPostprocessor( + dependencies, + relationalDependencies, + (RelationalQueryCompilationContext)queryCompilationContext, + relationalTypeMappingSource, + options.SqlMode); + } +} diff --git a/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs b/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs new file mode 100644 index 000000000..03f32750f --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs @@ -0,0 +1,1416 @@ +using EntityFrameworkCore.Jet.Infrastructure; +using EntityFrameworkCore.Jet.Query.Sql.Internal; +using EntityFrameworkCore.Jet.Utilities; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage.Internal; + +namespace EntityFrameworkCore.LibRed.Query.Sql.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class LibRedQuerySqlGenerator : QuerySqlGenerator, IJetExpressionVisitor + { + private static readonly Dictionary _convertMappings = new() + { + { nameof(Boolean), "CBOOL" }, + { nameof(Byte), "CBYTE" }, + { nameof(SByte), "CINT" }, + { nameof(Int16), "CINT" }, + { nameof(Int32), "CLNG" }, + { nameof(Int64), "CLNG" }, + { nameof(Single), "CSNG" }, + { nameof(Double), "CDBL" }, + { nameof(Decimal), "CDEC" }, + { nameof(DateTime), "CDATE" }, + { nameof(TimeOnly), "TIMEVALUE" }, + }; + + // VBA functions that raise on a NULL in a numeric argument - a length, start, count or code - instead of + // propagating it, with the positions that need guarding. Verified against ACE in + // LibRed.Core.Tests.AceNullArgumentProbeTest; add an entry as others turn up. + private static readonly Dictionary _nullHostileArguments = new(StringComparer.OrdinalIgnoreCase) + { + { "MID", [1, 2] }, + }; + + private readonly ITypeMappingSource _typeMappingSource; + private readonly ISqlGenerationHelper _sqlGenerationHelper; + private List _nullNumerics = []; + private Stack parent = new(); + private CoreTypeMapping? _boolTypeMapping; + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public LibRedQuerySqlGenerator( + QuerySqlGeneratorDependencies dependencies, + ITypeMappingSource typeMappingSource) + : base(dependencies) + { + _typeMappingSource = typeMappingSource; + _sqlGenerationHelper = dependencies.SqlGenerationHelper; + _boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool)); + } + + protected override bool TryGenerateWithoutWrappingSelect(SelectExpression selectExpression) + { + parent.TryPeek(out var exp); + if (exp is InExpression) + { + return false; + } + return base.TryGenerateWithoutWrappingSelect(selectExpression); + } + + protected override Expression VisitSelect(SelectExpression selectExpression) + { + // Copy & pasted from `QuerySqlGenerator` to implement Jet's non-standard JOIN syntax and DUAL table + // workaround. + // Should be kept in sync with the base class. + + IDisposable? subQueryIndent = null; + + if (selectExpression.Alias != null) + { + Sql.AppendLine("("); + subQueryIndent = Sql.Indent(); + } + + if (!TryGenerateWithoutWrappingSelect(selectExpression)) + { + + Sql.Append("SELECT "); + + if (selectExpression.IsDistinct) + { + Sql.Append("DISTINCT "); + } + + GenerateTop(selectExpression); + + if (selectExpression.Projection.Any()) + { + GenerateList(selectExpression.Projection, e => Visit(e)); + } + else + { + if (selectExpression.Predicate == null) + { + Sql.Append("1"); + } + else + { + //The WHERE clause can only refer to columns in the projection + List cols = []; + if (selectExpression.Predicate is SqlBinaryExpression binaryExpression) + { + cols = ExtractColumnExpressions(binaryExpression); + } + else if (selectExpression.Predicate is SqlUnaryExpression unaryExpression) + { + cols = ExtractColumnExpressions(unaryExpression); + } + + var collist = cols.Where(c => + selectExpression.Tables.Select(d => d.Alias).Contains(c.TableAlias)).ToList(); + parent.TryPeek(out var parentExpression); + if (selectExpression.Tables.Count > 1 && selectExpression.Tables.Any(c => c is InnerJoinExpression) && collist.Count > 0 && parentExpression is SqlUnaryExpression or SqlBinaryExpression) + { + Visit(collist[0]); + } + else + { + Sql.Append("1"); + } + } + } + + VisitJetTables(selectExpression.Tables, true, out var colexp); + + if (selectExpression.Predicate != null || colexp.Count > 0) + { + Sql.AppendLine() + .Append("WHERE "); + + if (selectExpression.Predicate != null) + { + if (colexp.Count > 0) Sql.Append("("); + parent.Push(selectExpression.Predicate); + Visit(selectExpression.Predicate); + parent.Pop(); + if (colexp.Count > 0) Sql.Append(")"); + } + + if (selectExpression.Predicate != null && colexp.Count > 0) + { + Sql.Append(" AND ("); + } + + if (colexp.Count > 0) + { + int ct = 0; + foreach (var exp in colexp) + { + if (!string.IsNullOrEmpty(exp.TableAlias)) + { + Sql.Append($"`{exp.TableAlias}`."); + } + + Sql.Append($"`{exp.Name}` IS NOT NULL"); + // Fix: check then append, don't mix increment into the block + if (ct < colexp.Count - 1) + Sql.Append(" AND "); + ct++; + } + } + + if (selectExpression.Predicate != null && colexp.Count > 0) + { + Sql.Append(")"); + } + } + + if (selectExpression.GroupBy.Count > 0) + { + Sql.AppendLine() + .Append("GROUP BY "); + + GenerateList(selectExpression.GroupBy, e => Visit(e)); + } + + if (selectExpression.Having != null) + { + Sql.AppendLine() + .Append("HAVING "); + + Visit(selectExpression.Having); + } + + GenerateOrderings(selectExpression); + GenerateLimitOffset(selectExpression); + + } + + if (selectExpression.Alias != null) + { + subQueryIndent!.Dispose(); + + Sql.AppendLine() + .Append(")") + .Append(AliasSeparator) + .Append(_sqlGenerationHelper.DelimitIdentifier(selectExpression.Alias)); + } + + return selectExpression; + } + + private void VisitJetTables(IReadOnlyList tables, bool addFromSql, out List colexp) + { + colexp = []; + if (!tables.Any()) + { + GeneratePseudoFromClause(); + return; + } + + if (addFromSql) + Sql.AppendLine().Append("FROM "); + + if (tables.Any(t => t is CrossJoinExpression or CrossApplyExpression)) + VisitJetTablesGrouped(tables, colexp); + else + VisitJetTablesLinear(tables, colexp); + } + + // No cross joins: existing linear parenthesization algorithm. + private void VisitJetTablesLinear(IReadOnlyList tables, List colexp) + { + const int maxTablesWithoutBrackets = 2; + var nonCrossTableCount = tables.Count(t => t is not CrossJoinExpression and not CrossApplyExpression); + Sql.Append(new string('(', Math.Max(0, nonCrossTableCount - maxTablesWithoutBrackets))); + + var nonCrossTablesSeen = 0; + for (var index = 0; index < tables.Count; index++) + { + var tableExpression = tables[index]; + var isCrossExpression = tableExpression is CrossJoinExpression or CrossApplyExpression; + + if (tableExpression is CrossApplyExpression or OuterApplyExpression) + throw new UnreachableException(); + + if (!isCrossExpression) + nonCrossTablesSeen++; + + if (index > 0) + { + if (isCrossExpression) + Sql.Append(","); + else if (nonCrossTablesSeen > maxTablesWithoutBrackets) + Sql.Append(")"); + + Sql.AppendLine(); + } + + if (tableExpression is InnerJoinExpression innerJoin) + { + var tempcolexp = innerJoin.JoinPredicate switch + { + SqlBinaryExpression bin => ExtractColumnExpressions(bin), + SqlUnaryExpression unary => ExtractColumnExpressions(unary), + // A bare boolean column used directly as the ON condition (e.g. `ON g.HasSoulPatch`, + // once `= True` has been simplified away). Without this case it falls through to an + // empty list, the tables[0] check fails, and the INNER JOIN is wrongly emitted as a + // LEFT JOIN with nothing hoisted to WHERE — turning an inner join into an outer one + // (extra unmatched rows). See GearsOfWar Join_predicate_value. + ColumnExpression col => [col], + _ => (List)[] + }; + + if (tempcolexp.Any(col => col.TableAlias == tables[0].Alias)) + { + Visit(tableExpression); + } + else + { + colexp.AddRange(tempcolexp); + Sql.Append("LEFT JOIN "); + Visit(innerJoin.Table); + Sql.Append(" ON "); + Visit(innerJoin.JoinPredicate); + } + } + else + { + Visit(tableExpression); + } + } + } + + // Cross joins present: group each primary table with its associated joins so that + // each group is parenthesized independently before being comma-cross-joined. + // Jet rejects mixing comma (cross-join) and explicit JOIN at the same paren level. + private void VisitJetTablesGrouped(IReadOnlyList tables, List colexp) + { + var groups = new List<(TableExpressionBase Primary, List Joins, HashSet OwnedAliases)>(); + + foreach (var table in tables) + { + switch (table) + { + case CrossApplyExpression or OuterApplyExpression: + throw new UnreachableException(); + case CrossJoinExpression cj: + groups.Add((cj.Table, [], new HashSet { cj.Table.Alias! })); + break; + case PredicateJoinExpressionBase join: + var predicateAliases = ExtractTableAliases(join.JoinPredicate); + var owner = groups.FirstOrDefault(g => g.OwnedAliases.Overlaps(predicateAliases)); + if (owner.Primary != null) + { + owner.Joins.Add(join); + owner.OwnedAliases.Add(join.Table.Alias!); + } + else + { + var last = groups[^1]; + last.Joins.Add(join); + last.OwnedAliases.Add(join.Table.Alias!); + } + break; + default: + groups.Add((table, [], new HashSet { table.Alias! })); + break; + } + } + + for (var i = 0; i < groups.Count; i++) + { + if (i > 0) + Sql.Append(",").AppendLine(); + + var (primary, joins, _) = groups[i]; + var groupTableCount = 1 + joins.Count; + + if (joins.Count > 0) + Sql.Append("("); + + Sql.Append(new string('(', Math.Max(0, groupTableCount - 2))); + + Visit(primary); + + var nonSeen = 1; + foreach (var join in joins) + { + nonSeen++; + if (nonSeen > 2) + Sql.Append(")"); + + Sql.AppendLine(); + EmitJoinInGroup(join, primary, colexp); + } + + if (joins.Count > 0) + Sql.Append(")"); + } + } + + private void EmitJoinInGroup(PredicateJoinExpressionBase join, TableExpressionBase groupPrimary, List colexp) + { + if (join is InnerJoinExpression innerJoin) + { + var tempcolexp = innerJoin.JoinPredicate switch + { + SqlBinaryExpression bin => ExtractColumnExpressions(bin), + SqlUnaryExpression unary => ExtractColumnExpressions(unary), + // A bare boolean column used directly as the ON condition (e.g. `ON g.HasSoulPatch`, + // once `= True` is simplified away). Without this case it falls through to an empty + // list, the tables[0] check fails, and the INNER JOIN is wrongly emitted as a LEFT + // JOIN with nothing hoisted to WHERE — turning an inner join into an outer one. + ColumnExpression col => [col], + _ => (List)[] + }; + + if (tempcolexp.Any(col => col.TableAlias == groupPrimary.Alias)) + Visit(join); + else + { + colexp.AddRange(tempcolexp); + Sql.Append("LEFT JOIN "); + Visit(innerJoin.Table); + Sql.Append(" ON "); + Visit(innerJoin.JoinPredicate); + } + } + else + { + Visit(join); + } + } + + private HashSet ExtractTableAliases(SqlExpression expression) + { + var result = new HashSet(); + CollectTableAliases(expression, result); + return result; + } + + private static void CollectTableAliases(SqlExpression expression, HashSet result) + { + switch (expression) + { + case ColumnExpression col: + result.Add(col.TableAlias); + break; + case SqlBinaryExpression bin: + CollectTableAliases(bin.Left, result); + CollectTableAliases(bin.Right, result); + break; + case SqlUnaryExpression unary: + CollectTableAliases(unary.Operand, result); + break; + } + } + + private List ExtractColumnExpressions(SqlBinaryExpression binaryexp) + { + List result = []; + switch (binaryexp.Left) + { + case SqlBinaryExpression left: + result.AddRange(ExtractColumnExpressions(left)); + break; + case ColumnExpression colLeft: + result.Add(colLeft); + break; + } + + switch (binaryexp.Right) + { + case SqlBinaryExpression right: + result.AddRange(ExtractColumnExpressions(right)); + break; + case ColumnExpression colRight: + result.Add(colRight); + break; + } + + return result; + } + private List ExtractColumnExpressions(SqlUnaryExpression unaryexp) + { + List result = []; + switch (unaryexp.Operand) + { + case SqlBinaryExpression left: + result.AddRange(ExtractColumnExpressions(left)); + break; + case ColumnExpression colLeft: + result.Add(colLeft); + break; + } + + return result; + } + + protected override Expression VisitProjection(ProjectionExpression projectionExpression) + { + if (projectionExpression.Expression is SqlConstantExpression { Value: null } constantExpression && (constantExpression.Type == typeof(int) || constantExpression.Type == typeof(double) || constantExpression.Type == typeof(float) || constantExpression.Type == typeof(decimal) || constantExpression.Type == typeof(short))) + { + _nullNumerics.Add(projectionExpression.Alias); + } + parent.Push(projectionExpression); + var result = base.VisitProjection(projectionExpression); + parent.Pop(); + return result; + } + + protected override Expression VisitColumn(ColumnExpression columnExpression) + { + if (columnExpression.IsNullable && _nullNumerics.Contains(columnExpression.Name) && _convertMappings.TryGetValue(columnExpression.Type.Name, out var function)) + { + + bool useValCStrCol = false;//columnExpression.Type.Name is nameof(Decimal) or nameof(Int64); + if (parent.TryPeek(out var exp) && exp is SqlBinaryExpression) + { + Sql.Append("IIF("); + base.VisitColumn(columnExpression); + Sql.Append(" IS NULL, NULL, "); + if (useValCStrCol) + { + Sql.Append("Val(CStr("); + base.VisitColumn(columnExpression); + Sql.Append("))"); + } + else + { + Sql.Append(function); + Sql.Append("("); + base.VisitColumn(columnExpression); + Sql.Append(")"); + } + Sql.Append(")"); + return columnExpression; + } + } + return base.VisitColumn(columnExpression); + } + + protected override Expression VisitJsonScalar(JsonScalarExpression jsonScalarExpression) + { + var path = jsonScalarExpression.Path; + if (path.Count == 0) + { + Visit(jsonScalarExpression.Json); + return jsonScalarExpression; + } + + throw new UnreachableException(); + } + + private bool IsNonComposedSetOperation(SelectExpression selectExpression) + => selectExpression.Offset == null + && selectExpression.Limit == null + && selectExpression is { IsDistinct: false, Predicate: null, Having: null, Orderings.Count: 0, GroupBy.Count: 0, Tables: [SetOperationBase setOperation] } + && selectExpression.Projection.Count == setOperation.Source1.Projection.Count + && selectExpression.Projection.Select( + (pe, index) => pe.Expression is ColumnExpression column + && string.Equals(column.TableAlias, setOperation.Alias, + StringComparison.OrdinalIgnoreCase) + && string.Equals( + column.Name, setOperation.Source1.Projection[index] + .Alias, StringComparison.OrdinalIgnoreCase)) + .All(e => e); + + protected override void GeneratePseudoFromClause() + { + Sql.AppendLine() + .Append("FROM " + "(SELECT COUNT(*) FROM `" + JetDualTable.Name + "`)"); + } + + private void GenerateList( + IReadOnlyList items, + Action generationAction, + Action? joinAction = null) + { + joinAction ??= (isb => isb.Append(", ")); + + for (var i = 0; i < items.Count; i++) + { + if (i > 0) + { + joinAction(Sql); + } + + generationAction(items[i]); + } + } + + protected override Expression VisitOrdering(OrderingExpression orderingExpression) + { + // Jet uses the value -1 as True, so ordering by a boolean expression will first list the True values + // before the False values, which is the opposite of what .NET and other DBMS do, which are using 1 as True. + + if (orderingExpression.Expression.TypeMapping is BoolTypeMapping + && orderingExpression.Expression.TypeMapping.GetType() == _boolTypeMapping?.GetType()) + { + orderingExpression = new OrderingExpression( + new SqlUnaryExpression( + ExpressionType.Not, + orderingExpression.Expression, + orderingExpression.Expression.Type, + orderingExpression.Expression.TypeMapping), + orderingExpression.IsAscending); + } + + if (orderingExpression.Expression is SqlConstantExpression or SqlParameterExpression) + { + Sql.Append("1"); + } + else + { + Visit(orderingExpression.Expression); + } + + if (!orderingExpression.IsAscending) + { + Sql.Append(" DESC"); + } + + return orderingExpression; + } + + protected override Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression) + { + // Matched on the EF Core base mapping, not on JetDateTimeTypeMapping/JetTimeOnlyTypeMapping: a derived + // provider (LibRed) substitutes its own mapping off the same base, and it needs the same coercion. The + // CLR-type test in front is what actually narrows this - the mapping test only rules out a DateTime + // that some other mapping claimed. + if (sqlParameterExpression.Type == typeof(DateTime) && sqlParameterExpression.TypeMapping is DateTimeTypeMapping or NullTypeMapping) + { + Sql.Append("CDATE("); + base.VisitSqlParameter(sqlParameterExpression); + Sql.Append(")"); + return sqlParameterExpression; + } + if (sqlParameterExpression.Type == typeof(TimeOnly) && sqlParameterExpression.TypeMapping is TimeOnlyTypeMapping) + { + Sql.Append("TIMEVALUE("); + base.VisitSqlParameter(sqlParameterExpression); + Sql.Append(")"); + return sqlParameterExpression; + } + + + //GroupBy_param_Select_Sum_Min_Key_Max_Avg + //Subquery has parameter as a projection with alias + //Parent query references that alias in the GroupBy and in the outer projection + //Query returns a variant object of value NULL instead of the expected type (in this case integer) + //Nullable object must have a value + //Specifically converting the parameter to its type fixes the problem + //This does do it for all cases which changes the SQL + //Don't have the required info in this function to detect the specific prerequisites for this problem + //TODO: Optimize elsewhere if possible - Expression Visitor? + if (parent.TryPeek(out var parentexp)) + { + if (parentexp is ProjectionExpression { Alias: not null }) + { + if (_convertMappings.TryGetValue(sqlParameterExpression.Type.Name, out var conv)) + { + /*if (sqlParameterExpression.Type.Name is nameof(Decimal) or nameof(Int64)) + { + Sql.Append("Val(CStr("); + base.VisitSqlParameter(sqlParameterExpression); + Sql.Append("))"); + } + else*/ + { + Sql.Append($"{conv}("); + base.VisitSqlParameter(sqlParameterExpression); + Sql.Append(")"); + } + return sqlParameterExpression; + } + } + } + return base.VisitSqlParameter(sqlParameterExpression); + } + + protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpression) + { + Check.NotNull(sqlBinaryExpression, nameof(sqlBinaryExpression)); + + if (sqlBinaryExpression.OperatorType == ExpressionType.Coalesce) + { + SqlConstantExpression nullcons = new(null, typeof(string), RelationalTypeMapping.NullMapping); + SqlUnaryExpression isnullexp = new(ExpressionType.Equal, sqlBinaryExpression.Left, typeof(bool), null); + List whenclause = [new CaseWhenClause(isnullexp, sqlBinaryExpression.Right)]; + CaseExpression caseexp = new(whenclause, sqlBinaryExpression.Left); + Visit(caseexp); + return sqlBinaryExpression; + } + // String concatenation propagates NULL for EF, but Access's '&' coerces a NULL operand to a + // zero-length string instead (see GetOperator for why '+', which does propagate, is not usable). + // Restore the propagation around the concat rather than in it, and only for operands that can + // actually be NULL - a concat of non-nullable operands generates exactly as it did before. + if (sqlBinaryExpression.OperatorType == ExpressionType.Add + && sqlBinaryExpression.Type == typeof(string) + && (MayBeNull(sqlBinaryExpression.Left) || MayBeNull(sqlBinaryExpression.Right))) + { + // Guard shape follows EF's own: IS NOT NULL checks ANDed together, concat in the THEN. The two + // operands are frequently the same expression (x + x), so check each distinct one once. + var checks = new List(2); + foreach (var operand in new[] { sqlBinaryExpression.Left, sqlBinaryExpression.Right }) + { + if (MayBeNull(operand) && !checks.Any(c => c.Equals(operand))) + { + checks.Add(operand); + } + } + + Sql.Append("IIF("); + + for (var i = 0; i < checks.Count; i++) + { + if (i > 0) + { + Sql.Append(" AND "); + } + + Visit(checks[i]); + Sql.Append(" IS NOT NULL"); + } + + Sql.Append(", "); + + parent.Push(sqlBinaryExpression); + base.VisitSqlBinary(sqlBinaryExpression); + parent.Pop(); + + Sql.Append(", NULL)"); + return sqlBinaryExpression; + } + + parent.Push(sqlBinaryExpression); + var res = base.VisitSqlBinary(sqlBinaryExpression); + parent.Pop(); + return res; + } + + /// + /// Whether an operand of a string concatenation needs a NULL guard: only a bare nullable column does. + /// Anything composed - a COALESCE, a CASE, a function, a parameter - is left alone, because EF has + /// already expressed whatever null handling it wants there. Guarding those as well produced + /// IIF(IIF(x IS NULL, '', x) IS NOT NULL, ...), a null check on an expression that cannot be null, + /// with the operand duplicated three times. + /// + private static bool MayBeNull(SqlExpression expression) + => expression is ColumnExpression { IsNullable: true }; + + /// The nullable columns an expression reads, so constants and parameters contribute nothing. + private static IEnumerable NullableColumns(SqlExpression expression) + { + switch (expression) + { + case ColumnExpression { IsNullable: true } column: + yield return column; + break; + + case SqlUnaryExpression unary: + foreach (var column in NullableColumns(unary.Operand)) + { + yield return column; + } + + break; + + case SqlBinaryExpression binary: + foreach (var column in NullableColumns(binary.Left).Concat(NullableColumns(binary.Right))) + { + yield return column; + } + + break; + + case SqlFunctionExpression { Arguments: { } functionArguments }: + foreach (var column in functionArguments.SelectMany(NullableColumns)) + { + yield return column; + } + + break; + } + } + + protected override void GenerateIn(InExpression inExpression, bool negated) + { + ///TODO: recheck how this works in net 8 + /*var valuesConstant = (SqlConstantExpression?)inExpression.Values; + if (valuesConstant != null) + { + var isdt = (IEnumerable)valuesConstant?.Value!; + var enumerable = isdt.ToList(); + if (enumerable.Any()) + { + var dtf = enumerable.FirstOrDefault(); + //Need to use a specific Jet DateTime format - when used in an IN section the mapping isn't automatic so set it up explicitly + if (dtf is DateTime) + { + var newexp = new InExpression(inExpression.Item, + valuesConstant!.ApplyTypeMapping(new JetDateTimeTypeMapping("datetime", _options)), + new JetDateTimeTypeMapping("datetime", _options)); + base.GenerateIn(newexp, negated); + } + } + }*/ + parent.Push(inExpression); + base.GenerateIn(inExpression, negated); + parent.Pop(); + } + + protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression) + { + if (sqlConstantExpression.TypeMapping == RelationalTypeMapping.NullMapping && sqlConstantExpression.Value is DateTime) + { + sqlConstantExpression = (SqlConstantExpression)sqlConstantExpression.ApplyTypeMapping( + (RelationalTypeMapping?)_typeMappingSource.FindMapping(typeof(DateTime))); + } + + parent.TryPeek(out var exp); + if (sqlConstantExpression.Value is null && exp is ProjectionExpression && (sqlConstantExpression.Type.IsNumeric() || sqlConstantExpression.Type.IsEnum || sqlConstantExpression.Type == typeof(bool))) + { + Sql.Append("CVar("); + Sql.Append(sqlConstantExpression.TypeMapping!.GenerateSqlLiteral(sqlConstantExpression.Value)); + Sql.Append(")"); + return sqlConstantExpression; + } + + if (sqlConstantExpression.TypeMapping is BoolTypeMapping + && sqlConstantExpression.TypeMapping.GetType() != _boolTypeMapping?.GetType()) + { + Sql.Append((bool)sqlConstantExpression.Value! ? "TRUE" : "FALSE"); + return sqlConstantExpression; + } + return base.VisitSqlConstant(sqlConstantExpression); + } + + protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpression) + { + switch (sqlUnaryExpression.OperatorType) + { + case ExpressionType.Convert: + return VisitJetConvertExpression(sqlUnaryExpression); + case ExpressionType.Not when sqlUnaryExpression.Type != typeof(bool): + { + Sql.Append(" (BNOT"); + + var requiresBrackets = RequiresParentheses(sqlUnaryExpression, sqlUnaryExpression.Operand); + if (requiresBrackets) + { + Sql.Append("("); + } + + Visit(sqlUnaryExpression.Operand); + if (requiresBrackets) + { + Sql.Append(")"); + } + + Sql.Append(")"); + + return sqlUnaryExpression; + } + default: + return base.VisitSqlUnary(sqlUnaryExpression); + } + } + + + private Expression VisitJetConvertExpression(SqlUnaryExpression convertExpression) + { + var typeMapping = convertExpression.TypeMapping ?? throw new InvalidOperationException( + RelationalStrings.UnsupportedType(convertExpression.Type.ShortDisplayName())); + + // We are explicitly converting to the target type (convertExpression.Type) and not the CLR type of the + // associated type mapping. This allows for conversions on the database side (e.g. CDBL()) but handling + // of the returned value using a different (unaligned) type mapping (e.g. date/time related ones). + if (_convertMappings.TryGetValue(convertExpression.Type.Name, out var function)) + { + SqlExpression checksqlexp = convertExpression.Operand; + SqlExpression? notnullsqlexp = null; + + bool useValCStr = false;//convertExpression.Type.Name is nameof(Decimal) or nameof(Int64); + + /*SqlFunctionExpression WrapConvert(SqlExpression inner) => + useValCStr + ? new SqlFunctionExpression("Val", + [new SqlFunctionExpression("CStr", [inner], false, [false], typeof(string), null)], + false, [false], typeMapping.ClrType, null) + : new SqlFunctionExpression(function, [inner], false, [false], typeMapping.ClrType, null);*/ + + SqlFunctionExpression WrapConvert(SqlExpression inner) => + useValCStr + ? new SqlFunctionExpression("CVar", [inner], + false, [false], typeMapping.ClrType, null) + : new SqlFunctionExpression(function, [inner], false, [false], typeMapping.ClrType, null); + + if (convertExpression.TypeMapping is ByteArrayTypeMapping) + { + notnullsqlexp = checksqlexp; + } + else + { + // A bool operand arrives already flipped to 0/1 by JetSqlExpressionFactory.Convert, so + // CBYTE/CINT/CLNG receive .NET's values rather than VARIANT_BOOL's 0/-1. + notnullsqlexp = WrapConvert(convertExpression.Operand); + } + + SqlConstantExpression nullcons = new(null, typeof(string), RelationalTypeMapping.NullMapping); + SqlUnaryExpression isnullexp = new(ExpressionType.Equal, checksqlexp, typeof(bool), null); + List whenclause = + [ + new CaseWhenClause(isnullexp, nullcons) + ]; + CaseExpression caseexp = new(whenclause, notnullsqlexp); + + switch (checksqlexp) + { + case ColumnExpression { IsNullable: true }: + Visit(caseexp); + break; + case ColumnExpression: + Visit(notnullsqlexp); + break; + case SqlFunctionExpression { IsNullable: true, ArgumentsPropagateNullability: not null } functionExpression + when functionExpression.ArgumentsPropagateNullability.Any(d => d): + Visit(caseexp); + break; + case SqlFunctionExpression: + Visit(notnullsqlexp); + break; + case SqlBinaryExpression binaryExpression: + { + static bool IsNullable(SqlExpression? e) => + e is ColumnExpression { IsNullable: true } + or SqlFunctionExpression { IsNullable: true }; + + if (IsNullable(binaryExpression.Left) || IsNullable(binaryExpression.Right)) + Visit(caseexp); + else + Visit(notnullsqlexp); + + break; + } + case SqlUnaryExpression: + case SqlConstantExpression { Value: not null }: + Visit(notnullsqlexp); + break; + default: + Visit(caseexp); + break; + } + + return convertExpression; + } + + if (typeMapping.ClrType.Name == nameof(String)) + { + Sql.Append("("); + Visit(convertExpression.Operand); + Sql.Append(@" & '')"); + return convertExpression; + } + + //Just pass the operand in the default case + //If we have a type mapping on the operand, then it seems to work fine + //Jet appears to be fairly flexible when types aren't specifically mentioned + //Keep an eye on this for any further problems - doesn't show anything in the tests right now + Visit(convertExpression.Operand); + return convertExpression; + } + + protected override string GetOperator(SqlBinaryExpression binaryExpression) + => binaryExpression.OperatorType switch + { + ExpressionType.Add when binaryExpression.Type == typeof(string) => " & ", + ExpressionType.And => " BAND ", + ExpressionType.Modulo => " MOD ", + ExpressionType.Or => " BOR ", + ExpressionType.Not => " BNOT ", + ExpressionType.ExclusiveOr => " BXOR ", + ExpressionType.Divide when binaryExpression.Type == typeof(int) => " \\ ", + _ => base.GetOperator(binaryExpression), + }; + + protected override Expression VisitCrossJoin(CrossJoinExpression crossJoinExpression) + { + Visit(crossJoinExpression.Table); + return crossJoinExpression; + } + + protected override Expression VisitLeftJoin(LeftJoinExpression leftJoinExpression) + { + var equalityColumns = new HashSet(); + CollectEqualityColumns(leftJoinExpression.JoinPredicate, equalityColumns); + + var predicate = RemoveRedundantNullChecks(leftJoinExpression.JoinPredicate, equalityColumns); + return base.VisitLeftJoin(predicate == leftJoinExpression.JoinPredicate + ? leftJoinExpression + : leftJoinExpression.Update(leftJoinExpression.Table, predicate!)); + + static SqlExpression? RemoveRedundantNullChecks( + SqlExpression expression, + HashSet equalityColumns) + { + if (expression is SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } andAlso) + { + var left = RemoveRedundantNullChecks(andAlso.Left, equalityColumns); + var right = RemoveRedundantNullChecks(andAlso.Right, equalityColumns); + return left is null ? right : right is null ? left : andAlso.Update(left, right); + } + + return expression is SqlUnaryExpression + { + OperatorType: ExpressionType.NotEqual, + Operand: ColumnExpression column + } && equalityColumns.Contains(column) + ? null + : expression; + } + + static void CollectEqualityColumns(SqlExpression expression, HashSet result) + { + if (expression is SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } andAlso) + { + CollectEqualityColumns(andAlso.Left, result); + CollectEqualityColumns(andAlso.Right, result); + } + else if (expression is SqlBinaryExpression + { + OperatorType: ExpressionType.Equal, + Left: ColumnExpression left, + Right: ColumnExpression right + }) + { + result.Add(left); + result.Add(right); + } + } + } + + private Expression VisitRowValuePrivate(RowValueExpression rowValueExpression, IReadOnlyList columnNames) + { + var values = rowValueExpression.Values; + var count = values.Count; + for (var i = 0; i < count; i++) + { + if (i > 0) + { + Sql.Append(", "); + } + + Visit(values[i]); + Sql.Append(" AS "); + Sql.Append(_sqlGenerationHelper.DelimitIdentifier(columnNames[i])); + } + + return rowValueExpression; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitValues(ValuesExpression valuesExpression) + { + base.VisitValues(valuesExpression); + + return valuesExpression; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override void GenerateValues(ValuesExpression valuesExpression) + { + if (valuesExpression.RowValues is null || valuesExpression.RowValues.Count == 0) + { + throw new InvalidOperationException(RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + } + + var rowValues = valuesExpression.RowValues; + + for (var i = 0; i < rowValues.Count; i++) + { + Sql.Append("SELECT "); + + VisitRowValuePrivate(valuesExpression.RowValues[i], valuesExpression.ColumnNames); + GeneratePseudoFromClause(); + var alias = valuesExpression.Alias; + Sql.Append(" AS "); + Sql.Append(_sqlGenerationHelper.DelimitIdentifier(alias + "_" + i)); + if (i != rowValues.Count - 1) + { + Sql.AppendLine(); + Sql.AppendLine("UNION"); + } + } + } + + + /// Generates the TOP part of the SELECT statement, + /// The select expression. + protected override void GenerateTop(SelectExpression selectExpression) + { + Check.NotNull(selectExpression, nameof(selectExpression)); + + if (selectExpression.Offset != null) + { + throw new InvalidOperationException( + "Jet does not support skipping rows. Switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync() if needed."); + } + + if (selectExpression.Limit != null) + { + Sql.Append("TOP "); + parent.Push(selectExpression); + Visit(selectExpression.Limit); + Sql.Append(" "); + parent.Pop(); + } + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override void GenerateLimitOffset(SelectExpression selectExpression) + { + // This has already been applied by GenerateTop(). + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression) + { + if (sqlFunctionExpression.Name.StartsWith("@@", StringComparison.Ordinal)) + { + Sql.Append(sqlFunctionExpression.Name); + return sqlFunctionExpression; + } + + // The guard has to be applied here rather than in the query tree: EF removes a CASE that merely + // replicates SQL's native null propagation (dotnet/efcore#34127), which is what this looks like to + // every dialect where these functions do propagate. IIF short-circuits, so the call is not evaluated. + if (_nullHostileArguments.TryGetValue(sqlFunctionExpression.Name, out var guardedPositions) + && sqlFunctionExpression.Arguments is { Count: > 0 } arguments) + { + // Nullability coming from the value argument itself needs no guard: ACE returns NULL for the + // whole call when the value is NULL, before it coerces the numeric arguments. Only a NULL + // arriving from somewhere else - MID(note, 1, LEN(otherTable.Name)) - reaches the coercion. + var valueColumns = NullableColumns(arguments[0]).ToHashSet(); + + var nullable = guardedPositions + .Where(position => position < arguments.Count) + .Where(position => NullableColumns(arguments[position]).Any(column => !valueColumns.Contains(column))) + .Select(position => arguments[position]) + .ToList(); + + if (nullable.Count > 0) + { + Sql.Append("IIF("); + for (var i = 0; i < nullable.Count; i++) + { + if (i > 0) + { + Sql.Append(" OR "); + } + + Visit(nullable[i]); + Sql.Append(" IS NULL"); + } + + Sql.Append(", NULL, "); + base.VisitSqlFunction(sqlFunctionExpression); + Sql.Append(")"); + return sqlFunctionExpression; + } + } + + if (sqlFunctionExpression.Name.Equals("POW", StringComparison.OrdinalIgnoreCase) && sqlFunctionExpression.Arguments != null) + { + Visit(sqlFunctionExpression.Arguments[0]); + Sql.Append("^"); + Visit(sqlFunctionExpression.Arguments[1]); + return sqlFunctionExpression; + } + + if (sqlFunctionExpression.Name.Equals("COALESCE", StringComparison.OrdinalIgnoreCase) && sqlFunctionExpression.Arguments is + { + Count: > 1 + }) + { + int start = sqlFunctionExpression.Arguments.Count - 1; + CaseExpression? lastcaseexp = null; + for (int A = start; A >= 1; A--) + { + SqlUnaryExpression isnullexp = new(ExpressionType.Equal, sqlFunctionExpression.Arguments[A - 1], typeof(bool), null); + List whenclause = + [new CaseWhenClause(isnullexp, lastcaseexp ?? sqlFunctionExpression.Arguments[A])]; + lastcaseexp = new CaseExpression(whenclause, sqlFunctionExpression.Arguments[A - 1]); + } + + Visit(lastcaseexp); + return sqlFunctionExpression; + } + + if (sqlFunctionExpression.Name.Equals("REPLACE", StringComparison.OrdinalIgnoreCase) && + sqlFunctionExpression.Arguments is { Count: 3 }) + { + // Access VBA's Replace() throws "Type mismatch" when ANY argument is NULL rather than + // propagating NULL as relational semantics require. Access IIF is also non-short-circuit + // (evaluates both branches), so a simple IIF wrapper doesn't prevent the crash. + // Solution: outer IIF returns NULL when any nullable arg IS NULL, while inner IIFs + // substitute safe non-NULL placeholders so REPLACE never actually receives NULL. + static SqlExpression? GetNullableTarget(SqlExpression arg) => arg switch + { + ColumnExpression { IsNullable: true } col => col, + SqlUnaryExpression { OperatorType: ExpressionType.Convert, Operand: ColumnExpression { IsNullable: true } inner } => inner, + SqlUnaryExpression { OperatorType: ExpressionType.Convert, Operand: SqlFunctionExpression { IsNullable: true } inner } => inner, + _ => null + }; + + var arg0Check = GetNullableTarget(sqlFunctionExpression.Arguments[0]); + var arg1Check = GetNullableTarget(sqlFunctionExpression.Arguments[1]); + var arg2Check = GetNullableTarget(sqlFunctionExpression.Arguments[2]); + + if (arg0Check != null || arg1Check != null || arg2Check != null) + { + Sql.Append("IIF("); + var nullChecks = new SqlExpression?[] { arg0Check, arg1Check, arg2Check } + .Where(c => c != null).ToList(); + for (int i = 0; i < nullChecks.Count; i++) + { + if (i > 0) Sql.Append(" OR "); + Visit(nullChecks[i]!); + Sql.Append(" IS NULL"); + } + Sql.Append(", NULL, REPLACE("); + + // Arg 0 (expression): '' prevents Type mismatch if NULL slips past outer IIF + if (arg0Check != null) + { + Sql.Append("IIF("); Visit(arg0Check); Sql.Append(" IS NULL, '', "); + Visit(sqlFunctionExpression.Arguments[0]); Sql.Append(")"); + } + else Visit(sqlFunctionExpression.Arguments[0]); + + Sql.Append(", "); + + // Arg 1 (find): CHR(1) is a safe non-empty placeholder unlikely to appear in data + if (arg1Check != null) + { + Sql.Append("IIF("); Visit(arg1Check); Sql.Append(" IS NULL, CHR(1), "); + Visit(sqlFunctionExpression.Arguments[1]); Sql.Append(")"); + } + else Visit(sqlFunctionExpression.Arguments[1]); + + Sql.Append(", "); + + // Arg 2 (replacewith): CHR(1) placeholder; result is discarded by outer IIF anyway + if (arg2Check != null) + { + Sql.Append("IIF("); Visit(arg2Check); Sql.Append(" IS NULL, CHR(1), "); + Visit(sqlFunctionExpression.Arguments[2]); Sql.Append(")"); + } + else Visit(sqlFunctionExpression.Arguments[2]); + + Sql.Append("))"); + return sqlFunctionExpression; + } + } + + if (sqlFunctionExpression.Name.Equals("MID", StringComparison.OrdinalIgnoreCase) && + sqlFunctionExpression.Arguments is { Count: > 2 }) + { + if (sqlFunctionExpression.Arguments[2] is ColumnExpression { IsNullable: true }) + { + Sql.Append("IIF("); + Visit(sqlFunctionExpression.Arguments[2]); + Sql.Append(" IS NULL, NULL, "); + base.VisitSqlFunction(sqlFunctionExpression); + Sql.Append(")"); + return sqlFunctionExpression; + } + if (sqlFunctionExpression.Arguments[2] is SqlUnaryExpression { OperatorType: ExpressionType.Convert, Operand: ColumnExpression { IsNullable: true } or SqlFunctionExpression { IsNullable: true } } unaryExpression) + { + Sql.Append("IIF("); + Visit(unaryExpression.Operand); + Sql.Append(" IS NULL, NULL, "); + base.VisitSqlFunction(sqlFunctionExpression); + Sql.Append(")"); + return sqlFunctionExpression; + } + } + parent.Push(sqlFunctionExpression); + var result = base.VisitSqlFunction(sqlFunctionExpression); + parent.Pop(); + return result; + } + + protected override Expression VisitCase(CaseExpression caseExpression) + { + using (Sql.Indent()) + { + parent.Push(caseExpression); + foreach (var whenClause in caseExpression.WhenClauses) + { + Sql.Append("IIF("); + + if (caseExpression.Operand != null) + { + Visit(caseExpression.Operand); + Sql.Append(" = "); + } + + Visit(whenClause.Test); + + Sql.Append(", "); + + Visit(whenClause.Result); + + Sql.Append(", "); + } + + if (caseExpression.ElseResult != null) + { + Visit(caseExpression.ElseResult); + } + else + { + Sql.Append("NULL"); + } + + Sql.Append(new string(')', caseExpression.WhenClauses.Count)); + parent.Pop(); + } + + return caseExpression; + } + + protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpression) + => throw new UnreachableException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitDelete(DeleteExpression deleteExpression) + { + var selectExpression = deleteExpression.SelectExpression; + + if (selectExpression.Offset == null + && selectExpression.Having == null + && selectExpression.Orderings.Count == 0 + && selectExpression.GroupBy.Count == 0 + && selectExpression.Projection.Count == 0) + { + Sql.Append("DELETE "); + + if (selectExpression.Tables.Count > 1) + { + Sql.Append($"{Dependencies.SqlGenerationHelper.DelimitIdentifier(deleteExpression.Table.Alias)}.*"); + Sql.AppendLine(); + } + + Sql.Append("FROM "); + VisitJetTables(selectExpression.Tables, false, out _); + + if (selectExpression.Predicate != null) + { + Sql.AppendLine().Append("WHERE "); + + Visit(selectExpression.Predicate); + } + + GenerateLimitOffset(selectExpression); + + return deleteExpression; + } + + throw new InvalidOperationException( + RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(EntityFrameworkQueryableExtensions.ExecuteDelete))); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitUpdate(UpdateExpression updateExpression) + { + var selectExpression = updateExpression.SelectExpression; + + if (selectExpression.Offset == null + && selectExpression.Having == null + && selectExpression.Orderings.Count == 0 + && selectExpression.GroupBy.Count == 0 + && selectExpression.Projection.Count == 0 + && selectExpression.Limit == null) + { + Sql.Append("UPDATE "); + + VisitJetTables(selectExpression.Tables, false, out _); + + Sql.AppendLine().Append("SET "); + Visit(updateExpression.ColumnValueSetters[0].Column); + Sql.Append(" = "); + Visit(updateExpression.ColumnValueSetters[0].Value); + + using (Sql.Indent()) + { + foreach (var columnValueSetter in updateExpression.ColumnValueSetters.Skip(1)) + { + Sql.AppendLine(","); + Visit(columnValueSetter.Column); + Sql.Append(" = "); + Visit(columnValueSetter.Value); + } + } + + if (selectExpression.Predicate != null) + { + Sql.AppendLine().Append("WHERE "); + Visit(selectExpression.Predicate); + } + + return updateExpression; + } + + throw new InvalidOperationException( + RelationalStrings.ExecuteOperationWithUnsupportedOperatorInSqlGeneration(nameof(EntityFrameworkQueryableExtensions.ExecuteUpdate))); + } + + /// + protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql) + { + base.CheckComposableSqlTrimmed(sql); + + if (sql.StartsWith("WITH", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable); + } + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGeneratorFactory.cs b/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGeneratorFactory.cs new file mode 100644 index 000000000..26c415caa --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGeneratorFactory.cs @@ -0,0 +1,27 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using EntityFrameworkCore.Jet.Query.Sql.Internal; +using EntityFrameworkCore.LibRed.Infrastructure; +using EntityFrameworkCore.LibRed.Infrastructure.Internal; + +namespace EntityFrameworkCore.LibRed.Query.Sql.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class LibRedQuerySqlGeneratorFactory( + QuerySqlGeneratorDependencies dependencies, + ITypeMappingSource typeMappingSource, + ILibRedOptions options) : IQuerySqlGeneratorFactory + { + private readonly QuerySqlGeneratorDependencies _dependencies = dependencies; + private readonly ITypeMappingSource _typeMappingSource = typeMappingSource; + private readonly ILibRedOptions _options = options; + + public virtual QuerySqlGenerator Create() + => _options.SqlMode == LibRedSqlMode.Compatible + ? new JetQuerySqlGenerator(_dependencies, _typeMappingSource) + : new LibRedQuerySqlGenerator(_dependencies, _typeMappingSource); + } +} diff --git a/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedCodeGenerator.cs b/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedCodeGenerator.cs new file mode 100644 index 000000000..5055ea027 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedCodeGenerator.cs @@ -0,0 +1,38 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using EntityFrameworkCore.LibRed.Infrastructure; + +namespace EntityFrameworkCore.LibRed.Scaffolding.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + /// + /// Initializes a new instance of the class. + /// + /// The dependencies. + public class LibRedCodeGenerator(ProviderCodeGeneratorDependencies dependencies) : ProviderCodeGenerator(dependencies) + { + private static readonly MethodInfo _useLibRedMethodInfo + = typeof(LibRedDbContextOptionsBuilderExtensions).GetRuntimeMethod( + nameof(LibRedDbContextOptionsBuilderExtensions.UseLibRed), + [typeof(DbContextOptionsBuilder), typeof(string), typeof(Action)])!; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override MethodCallCodeFragment GenerateUseProvider( + string connectionString, + MethodCallCodeFragment? providerOptions) + => new(_useLibRedMethodInfo, + providerOptions == null + ? [connectionString] + : [connectionString, new NestedClosureCodeFragment("x", providerOptions)]); + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/ILibRedRelationalConnection.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/ILibRedRelationalConnection.cs index 2ecf99cfd..d13547919 100644 --- a/src/LibRed/LibRed.EFCore/Storage/Internal/ILibRedRelationalConnection.cs +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/ILibRedRelationalConnection.cs @@ -10,6 +10,6 @@ namespace EntityFrameworkCore.LibRed.Storage.Internal; /// No members of its own today — it exists so LibRed-specific services can depend on "the /// LibRed connection" instead of the more general . /// -public interface ILibRedRelationalConnection : IJetRelationalConnection +public interface ILibRedRelationalConnection : IRelationalConnection { } diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedBoolTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedBoolTypeMapping.cs new file mode 100644 index 000000000..6ed3461f9 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedBoolTypeMapping.cs @@ -0,0 +1,31 @@ +using System.Data; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + public class LibRedBoolTypeMapping : BoolTypeMapping + { + public static new LibRedBoolTypeMapping Default { get; } = new("smallint"); + + public LibRedBoolTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Boolean) + : base(storeType, dbType) + { + } + + protected LibRedBoolTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedBoolTypeMapping(parameters); + + protected override string GenerateNonNullSqlLiteral(object value) + { + return (bool)value + ? "TRUE" + : "FALSE"; + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedByteArrayTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedByteArrayTypeMapping.cs new file mode 100644 index 000000000..e47b050c6 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedByteArrayTypeMapping.cs @@ -0,0 +1,111 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; +using System.Text; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class LibRedByteArrayTypeMapping : ByteArrayTypeMapping + { + public static new LibRedByteArrayTypeMapping Default { get; } = new(); + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public LibRedByteArrayTypeMapping( + string? storeType = null, + int? size = null, + bool fixedLength = false, + ValueComparer? comparer = null, + StoreTypePostfix? storeTypePostfix = null) + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(byte[]), null, comparer, jsonValueReaderWriter: JsonByteArrayReaderWriter.Instance), + storeType ?? (fixedLength ? "binary" : "varbinary"), + storeTypePostfix ?? StoreTypePostfix.Size, + System.Data.DbType.Binary, + size: size, + fixedLength: fixedLength)) + { + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected LibRedByteArrayTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedByteArrayTypeMapping(parameters); + + private static int CalculateSize(int? size) + => size.HasValue && size < 510 ? size.Value : 510; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override void ConfigureParameter(DbParameter parameter) + { + // For strings and byte arrays, set the max length to the size facet if specified, or + // 8000 bytes if no size facet specified, if the data will fit so as to avoid query cache + // fragmentation by setting lots of different Size values otherwise always set to + // -1 (unbounded) to avoid SQL client size inference. + + var value = parameter.Value; + var length = (value as string)?.Length ?? (value as byte[])?.Length; + int maxSpecificSize = CalculateSize(Size); + + parameter.Size = value == null || value == DBNull.Value || length != null && length <= maxSpecificSize + ? maxSpecificSize + : -1; + } + + /// + /// Generates the SQL representation of a literal value. + /// + /// The literal value. + /// + /// The generated string. + /// + protected override string GenerateNonNullSqlLiteral(object value) + { + var bytes = (byte[])value; + + // An empty array has no hex form here. SQL Server writes a bare '0x', but that is T-SQL only — + // ACE rejects it outright ("Syntax error in query expression '0x'"), so the SQL was invalid for + // this provider rather than merely unusual, and failed the same way on ACE and on LibRed. + // + // An empty STRING literal is what Access accepts, in a value position and in a DEFAULT. Verified + // against ACE: '' stores and reads back as a zero-length byte[], and stays distinct from NULL + // (IS NULL does not match it). Note 0x00 is NOT the same thing — that is a one-byte zero. + if (bytes.Length == 0) + { + return "''"; + } + + var builder = new StringBuilder(2 + bytes.Length * 2); + builder.Append("0x"); + + foreach (var @byte in bytes) + { + builder.Append(@byte.ToString("X2", CultureInfo.InvariantCulture)); + } + + return builder.ToString(); + } + + protected override string ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase) + { + return base.ProcessStoreType(parameters, storeType.Replace("(max)", ""), storeTypeNameBase); + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedByteTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedByteTypeMapping.cs new file mode 100644 index 000000000..3f1cb9cca --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedByteTypeMapping.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data; + +namespace EntityFrameworkCore.LibRed.Storage.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class LibRedByteTypeMapping : ByteTypeMapping +{ + public static new LibRedByteTypeMapping Default { get; } = new("byte", System.Data.DbType.Byte); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public LibRedByteTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Byte) + : base(storeType, dbType) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected LibRedByteTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// Creates a copy of this mapping. + /// + /// The parameters for this mapping. + /// The newly created mapping. + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedByteTypeMapping(parameters); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override string GenerateNonNullSqlLiteral(object value) + { + return $"CBYTE({base.GenerateNonNullSqlLiteral(value)})"; + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedCurrencyTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedCurrencyTypeMapping.cs new file mode 100644 index 000000000..554b63897 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedCurrencyTypeMapping.cs @@ -0,0 +1,51 @@ +using System.Data; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class LibRedCurrencyTypeMapping : DecimalTypeMapping + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public LibRedCurrencyTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Currency) + : base(storeType, dbType) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected LibRedCurrencyTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedCurrencyTypeMapping(parameters); + + protected override string ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase) + { + return base.ProcessStoreType(parameters, storeTypeNameBase, storeTypeNameBase); + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDatabaseCreator.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDatabaseCreator.cs index e15b328ae..e6cce3a6e 100644 --- a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDatabaseCreator.cs +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDatabaseCreator.cs @@ -15,39 +15,36 @@ namespace EntityFrameworkCore.LibRed.Storage.Internal; /// public class LibRedDatabaseCreator( RelationalDatabaseCreatorDependencies dependencies, - ILibRedRelationalConnection relationalConnection, - IRawSqlCommandBuilder rawSqlCommandBuilder) - : JetDatabaseCreator(dependencies, relationalConnection, rawSqlCommandBuilder) + IRelationalConnection relationalConnection) + : RelationalDatabaseCreator(dependencies) { - private readonly ILibRedRelationalConnection _connection = relationalConnection; - // Jet's Create/Delete go through relationalConnection.CreateEmptyConnection() - a "masterless" // connection used to run a CREATE/DROP DATABASE migration command. LibRedRelationalConnection // doesn't support that (there's nothing to connect to before the file exists), so these bypass // it entirely and go straight through LibRedConnection's own bootstrap (see its CreateDatabase - // remarks: it hands off to JetConnection.CreateDatabase/DAO-ADOX for now). + // remarks: DatabaseCreator.CreateEmpty synthesises the file from scratch - no DAO/ADOX). public override void Create() - => LibRedConnection.CreateDatabase(_connection.DbConnection.ConnectionString); + => LibRedConnection.CreateDatabase(relationalConnection.DbConnection.ConnectionString); public override void Delete() { - _connection.Close(); - LibRedConnection.DropDatabase(_connection.DbConnection.ConnectionString); + relationalConnection.Close(); + LibRedConnection.DropDatabase(relationalConnection.DbConnection.ConnectionString); } public override bool Exists() - => LibRedConnection.DatabaseExists(_connection.DbConnection.ConnectionString); + => LibRedConnection.DatabaseExists(relationalConnection.DbConnection.ConnectionString); public override bool HasTables() { - _connection.Open(); + relationalConnection.Open(); try { - return ((LibRedConnection)_connection.DbConnection).HasUserTables(); + return ((LibRedConnection)relationalConnection.DbConnection).HasUserTables(); } finally { - _connection.Close(); + relationalConnection.Close(); } } diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDateOnlyTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDateOnlyTypeMapping.cs new file mode 100644 index 000000000..a1dcaaaac --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDateOnlyTypeMapping.cs @@ -0,0 +1,84 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Data; +using System.Globalization; +using System.Text; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + public class LibRedDateOnlyTypeMapping : DateOnlyTypeMapping + { + + public static new LibRedDateOnlyTypeMapping Default { get; } = new LibRedDateOnlyTypeMapping("date", dbType: System.Data.DbType.Date); + public LibRedDateOnlyTypeMapping( + string storeType, + DbType? dbType = null) + : base(storeType) + { + } + + protected LibRedDateOnlyTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedDateOnlyTypeMapping(parameters); + + protected override void ConfigureParameter(DbParameter parameter) + { + base.ConfigureParameter(parameter); + if (parameter.Value is DateOnly dateOnly) + { + dateOnly.Deconstruct(out int year, out int month, out int day); + parameter.Value = new DateTime(year, month, day); + } + } + + protected override string GenerateNonNullSqlLiteral(object value) + => GenerateNonNullSqlLiteral(value, false); + + public virtual string GenerateNonNullSqlLiteral(object value, bool defaultClauseCompatible) + { + var dateTime = ConvertToDateTimeCompatibleValue(value); + + dateTime = CheckDateTimeValue(dateTime); + + var literal = new StringBuilder(); + + literal.Append( + defaultClauseCompatible + ? "'" + : "#"); + + literal.AppendFormat(CultureInfo.InvariantCulture, "{0:yyyy-MM-dd}", dateTime); + literal.Append( + defaultClauseCompatible + ? "'" + : "#"); + + return literal.ToString(); + } + + protected virtual DateTime ConvertToDateTimeCompatibleValue(object value) + { + ((DateOnly)value).Deconstruct(out int year, out int month, out int day); + return new DateTime(year, month, day); + } + + private static DateTime CheckDateTimeValue(DateTime dateTime) + { + if (dateTime != default && dateTime < new DateTime(100,1,1)) + { + throw new InvalidOperationException($"The {nameof(DateTime)} value '{dateTime}' is smaller than the minimum supported value of '{new DateTime(100, 1, 1)}'."); + } + + return dateTime; + } + + protected override string ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase) + { + return base.ProcessStoreType(parameters, storeTypeNameBase, storeTypeNameBase); + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDateTimeOffsetTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDateTimeOffsetTypeMapping.cs new file mode 100644 index 000000000..db6abe8bc --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDateTimeOffsetTypeMapping.cs @@ -0,0 +1,52 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + public class LibRedDateTimeOffsetTypeMapping : DateTimeOffsetTypeMapping + { + private const string DateTimeOffsetFormatConst = @"'{0:yyyy-MM-ddTHH:mm:ss.fffffffzzz}'"; + private const string DateTimeFormatConst = @"'{0:yyyy-MM-dd HH:mm:ss}'"; + private const string DateTimeMillisecondsFormatConst = @"'{0:yyyy-MM-dd HH:mm:ss.fff}'"; + + public static new LibRedDateTimeOffsetTypeMapping Default { get; } = new LibRedDateTimeOffsetTypeMapping("datetime"); + public LibRedDateTimeOffsetTypeMapping( + string storeType) + : base( + storeType, System.Data.DbType.DateTime) + { + } + + protected LibRedDateTimeOffsetTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedDateTimeOffsetTypeMapping(parameters); + + protected override void ConfigureParameter(DbParameter parameter) + { + // OLE DB can't handle the DateTimeOffset type. + if (parameter.Value is DateTimeOffset dateTimeOffset) + { + parameter.Value = dateTimeOffset.Ticks == 0 ? DateTime.FromOADate(0) : dateTimeOffset.UtcDateTime; + parameter.DbType = System.Data.DbType.DateTime; + } + + base.ConfigureParameter(parameter); + } + + protected override string SqlLiteralFormatString + => DateTimeOffsetFormatConst; + + protected override string GenerateNonNullSqlLiteral(object value) + { + if (value is not DateTimeOffset offset) return base.GenerateNonNullSqlLiteral(value); + var dateTime = offset.Ticks == 0 ? DateTime.FromOADate(0) : offset.UtcDateTime; + var format = dateTime.Millisecond != 0 ? DateTimeMillisecondsFormatConst : DateTimeFormatConst; + return $"CDATE({string.Format(CultureInfo.InvariantCulture, format, dateTime)})"; + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDateTimeTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDateTimeTypeMapping.cs new file mode 100644 index 000000000..be02f1e25 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDateTimeTypeMapping.cs @@ -0,0 +1,110 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Data; +using System.Globalization; +using System.Text; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + public class LibRedDateTimeTypeMapping : DateTimeTypeMapping + { + /// The lowest date Jet/ACE can represent; dates run 0100-01-01 to 9999-12-31. + private static readonly DateTime LibRedMinDate = new(100, 1, 1); + + public static new LibRedDateTimeTypeMapping Default { get; } = new LibRedDateTimeTypeMapping("datetime", dbType: System.Data.DbType.DateTime); + + public LibRedDateTimeTypeMapping( + string storeType, + DbType? dbType = null, + Type? clrType = null) + : base(storeType) + { + } + + protected LibRedDateTimeTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedDateTimeTypeMapping(parameters); + + protected override void ConfigureParameter(DbParameter parameter) + { + if (parameter.Value is DateTime { Ticks: 0 }) + { + parameter.Value = DateTime.FromOADate(0); + } + base.ConfigureParameter(parameter); + + if ((parameter.DbType == System.Data.DbType.Date || StoreTypeNameBase == "date") && parameter.Value is DateTime date) + { + parameter.Value = date.Date; + } + } + + protected override string GenerateNonNullSqlLiteral(object value) + => GenerateNonNullSqlLiteral(value, false); + + public virtual string GenerateNonNullSqlLiteral(object value, bool defaultClauseCompatible) + { + var dateTime = ConvertToDateTimeCompatibleValue(value); + if (dateTime is DateTime { Ticks: 0 }) + { + dateTime = DateTime.FromOADate(0); + } + dateTime = CheckDateTimeValue(dateTime); + + var literal = new StringBuilder(); + + literal.Append( + defaultClauseCompatible + ? "'" + : "#"); + + literal.AppendFormat(CultureInfo.InvariantCulture, "{0:yyyy-MM-dd}", dateTime); + + var time = dateTime.TimeOfDay; + if (time != TimeSpan.Zero && StoreTypeNameBase != "date") + { + literal.AppendFormat(CultureInfo.InvariantCulture, @" {0:hh\:mm\:ss}", time); + + if (time.Milliseconds != 0) + { + literal.AppendFormat(CultureInfo.InvariantCulture, @"{0:\.fff}", time); + } + } + + literal.Append( + defaultClauseCompatible + ? "'" + : "#"); + + return literal.ToString(); + } + + protected virtual DateTime ConvertToDateTimeCompatibleValue(object value) + => (DateTime)value; + + private static DateTime CheckDateTimeValue(DateTime dateTime) + { + // default(DateTime) is below Jet's floor, but every caller has already substituted the OLE epoch for + // it, so anything still under the floor here is a real value the store cannot represent. Ordering + // comparisons against default are corrected separately, in JetDateTimeRangeConverter. + if (dateTime < LibRedMinDate) + { + throw new InvalidOperationException( + $"The {nameof(DateTime)} value '{dateTime}' is smaller than the minimum supported value of '{LibRedMinDate}'."); + } + + return dateTime; + } + + // Deliberately passes storeTypeNameBase in place of storeType: Jet/ACE has no scaled datetime, so a + // precision-carrying store type such as "datetime(3)" must collapse to the bare "datetime" it understands. + protected override string ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase) + { + return base.ProcessStoreType(parameters, storeTypeNameBase, storeTypeNameBase); + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDecimalTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDecimalTypeMapping.cs new file mode 100644 index 000000000..d624359b3 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDecimalTypeMapping.cs @@ -0,0 +1,121 @@ +using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class LibRedDecimalTypeMapping : DecimalTypeMapping + { + + public static new LibRedDecimalTypeMapping Default { get; } = new("decimal(18,2)", System.Data.DbType.Decimal, precision: 18, scale: 2, StoreTypePostfix.PrecisionAndScale); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public LibRedDecimalTypeMapping( + string storeType, + DbType? dbType = null, + int? precision = null, + int? scale = null, + StoreTypePostfix storeTypePostfix = StoreTypePostfix.PrecisionAndScale) + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(decimal), jsonValueReaderWriter: JsonDecimalReaderWriter.Instance), + storeType, + storeTypePostfix, + dbType) + .WithPrecisionAndScale(precision, scale)) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected LibRedDecimalTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + { + var precision = parameters.Precision; + var scale = parameters.Scale; + if (parameters.Precision is > 28) + { + int prec_diff = parameters.Precision.Value - 28; + precision = 28; + if (parameters.Scale is > 28) + { + scale = parameters.Scale.Value - prec_diff; + } + } + + if (parameters.StoreType.Contains("bigint")) + { + var newparameters = new RelationalTypeMappingParameters(parameters.CoreParameters, "decimal", parameters.StoreTypePostfix, + parameters.DbType, parameters.Unicode, parameters.Size, parameters.FixedLength, + parameters.Precision, parameters.Scale); + return new LibRedDecimalTypeMapping(newparameters.WithPrecisionAndScale(precision, scale)); + } + return new LibRedDecimalTypeMapping(parameters.WithPrecisionAndScale(precision, scale)); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override void ConfigureParameter(DbParameter parameter) + { + base.ConfigureParameter(parameter); + + if (Size.HasValue + && Size.Value != -1) + { + parameter.Size = Size.Value; + } + + if (Precision.HasValue) + { + parameter.Precision = unchecked((byte)Precision.Value); + } + + if (Scale.HasValue) + { + parameter.Scale = unchecked((byte)Scale.Value); + } + + if (parameter.Value is decimal dec) + { + parameter.Value = decimal.Round(dec, parameter.Scale); + } + } + + protected override string GenerateNonNullSqlLiteral(object value) + { + + if (value is decimal dec && Scale.HasValue) + { + return base.GenerateNonNullSqlLiteral(decimal.Round(dec, Scale.Value)); + } + return base.GenerateNonNullSqlLiteral(value); + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDoubleTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDoubleTypeMapping.cs new file mode 100644 index 000000000..88332c1ad --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDoubleTypeMapping.cs @@ -0,0 +1,53 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + public class LibRedDoubleTypeMapping : DoubleTypeMapping + { + public static new LibRedDoubleTypeMapping Default { get; } = new("double"); + + public LibRedDoubleTypeMapping( + string storeType) + : base(storeType, System.Data.DbType.Double) + { + } + + protected LibRedDoubleTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedDoubleTypeMapping(parameters); + + protected override string ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase) + { + return base.ProcessStoreType(parameters, storeTypeNameBase, storeTypeNameBase); + } + + protected override string GenerateNonNullSqlLiteral(object value) + { + // "R" (shortest round-trippable) rather than the base's "G17" or a lossy "G15": + // - G17 always emits 17 digits, so -1.23456789 becomes -1.2345678899999999. + // - G15 is clean for ordinary values but *rounds*: 1.0/3.0 drops a digit, and double.MinValue + // renders as -1.79769313486232E+308 — a larger magnitude than double.MaxValue, i.e. not a + // representable double at all. ACE rejects that literal ("Syntax error in number"). + // - "R" is the shortest string that parses back exactly: -1.23456789 stays clean, and + // double.MinValue renders as -1.7976931348623157E+308, which ACE accepts. + // ("R" was unreliable on .NET Framework — hence the old G17 advice — but has been + // shortest-round-trippable since .NET Core 3.0.) + var doubleValue = Convert.ToDouble(value); + var literal = doubleValue.ToString("R", CultureInfo.InvariantCulture); + + return !literal.Contains('E') + && !literal.Contains('e') + && !literal.Contains('.') + && !double.IsNaN(doubleValue) + && !double.IsInfinity(doubleValue) + ? literal + ".0" + : literal; + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedFloatTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedFloatTypeMapping.cs new file mode 100644 index 000000000..96b725185 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedFloatTypeMapping.cs @@ -0,0 +1,47 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + public class LibRedFloatTypeMapping : FloatTypeMapping + { + public static new LibRedFloatTypeMapping Default { get; } = new("single"); + public LibRedFloatTypeMapping( + string storeType) + : base(storeType, System.Data.DbType.Single) + { + } + + protected LibRedFloatTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedFloatTypeMapping(parameters); + + protected override string ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase) + { + return base.ProcessStoreType(parameters, storeTypeNameBase, storeTypeNameBase); + } + + /// + /// Generates the SQL representation of a literal value. + /// + /// The literal value. + /// + /// The generated string. + /// + protected override string GenerateNonNullSqlLiteral(object value) + // "R" (shortest round-trippable) rather than G9 or a lossy G7 — see LibRedDoubleTypeMapping: + // - G9 always emits 9 digits, so 0.1f becomes 0.100000001 and 85.55f becomes 85.5500031. + // - G7 is clean for ordinary values but *rounds*, so it doesn't round-trip: -1.23456789f + // renders as -1.234568, 1f/3f as 0.3333333, and float.MaxValue as 3.402823E+38 — a + // different float, so `WHERE Float = ` silently fails to match. (Unlike double's + // G15, G7 rounds the extremes *down*, so it never yields an out-of-range literal.) + // - "R" is the shortest string that parses back exactly: 0.1f/85.55f stay clean, and + // float.MaxValue renders as 3.4028235E+38. + => Convert.ToSingle(value).ToString("R", CultureInfo.InvariantCulture); + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedGuidTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedGuidTypeMapping.cs new file mode 100644 index 000000000..d5823b77f --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedGuidTypeMapping.cs @@ -0,0 +1,31 @@ +using System.Data; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + public class LibRedGuidTypeMapping : GuidTypeMapping + { + public static new LibRedGuidTypeMapping Default { get; } = new LibRedGuidTypeMapping("uniqueidentifier"); + + public LibRedGuidTypeMapping(string storeType, DbType? dbType = System.Data.DbType.Guid) : base(storeType, dbType) + { + } + + protected LibRedGuidTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters) + { + } + + /// + /// Creates a copy of this mapping. + /// + /// The parameters for this mapping. + /// The newly created mapping. + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedGuidTypeMapping(parameters); + + /// + /// Gets the string format to be used to generate SQL literals of this type. + /// + protected override string SqlLiteralFormatString + => "{{{0}}}"; + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedIntTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedIntTypeMapping.cs new file mode 100644 index 000000000..c42905856 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedIntTypeMapping.cs @@ -0,0 +1,22 @@ +namespace EntityFrameworkCore.LibRed.Storage.Internal; + +public class LibRedIntTypeMapping : IntTypeMapping +{ + public static new LibRedIntTypeMapping Default { get; } = new LibRedIntTypeMapping("integer"); + + public LibRedIntTypeMapping(string storeType) + : base(storeType, System.Data.DbType.Int32) + { + } + + protected LibRedIntTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + // LibRedIntTypeMapping is also used for an explicit counter type, because we actually want it to be integer unless + // the value generation type is also OnAdd. + // We therefore lock the store type to its original value (which should be "integer"). + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedIntTypeMapping(parameters.WithStoreTypeAndSize(Parameters.StoreType, parameters.Size)); +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedJsonTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedJsonTypeMapping.cs new file mode 100644 index 000000000..660f98e1e --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedJsonTypeMapping.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Text.Json; + +namespace EntityFrameworkCore.LibRed.Storage.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +// EF 11 renamed the base to StructuralJsonTypeMapping and reduced JsonTypeMapping to an obsolete stub that no +// longer derives from RelationalTypeMapping — which is why the old base took GetDataReaderMethod, +// CustomizeDataReaderExpression, GenerateNonNullSqlLiteral and the nested RelationalTypeMappingParameters out of +// scope with it. The members themselves are unchanged. +public class LibRedJsonTypeMapping : StructuralJsonTypeMapping +{ + private static readonly MethodInfo GetStringMethod + = typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.GetString), [typeof(int)])!; + + private static readonly PropertyInfo UTF8Property + = typeof(Encoding).GetProperty(nameof(Encoding.UTF8))!; + + private static readonly MethodInfo EncodingGetBytesMethod + = typeof(Encoding).GetMethod(nameof(Encoding.GetBytes), [typeof(string)])!; + + private static readonly ConstructorInfo MemoryStreamConstructor + = typeof(MemoryStream).GetConstructor([typeof(byte[])])!; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static LibRedJsonTypeMapping Default { get; } = new("longchar"); + + /// + /// For a JSON column the model explicitly sized, which lands in a varchar rather than a memo. Cloned + /// outside for the requested size. + /// + public static LibRedJsonTypeMapping VarcharDefault { get; } = new("varchar"); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public LibRedJsonTypeMapping(string storeType) + : base(storeType, typeof(JsonElement), System.Data.DbType.String) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override MethodInfo GetDataReaderMethod() + => GetStringMethod; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override Expression CustomizeDataReaderExpression(Expression expression) + => Expression.New( + MemoryStreamConstructor, + Expression.Call( + Expression.Property(null, UTF8Property), + EncodingGetBytesMethod, + expression)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected LibRedJsonTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected virtual string EscapeSqlLiteral(string literal) + => literal.Replace("'", "''"); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override string GenerateNonNullSqlLiteral(object value) + => $"'{EscapeSqlLiteral(JsonSerializer.Serialize(value))}'"; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedJsonTypeMapping(parameters); +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedLongTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedLongTypeMapping.cs index e9757743f..168be1390 100644 --- a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedLongTypeMapping.cs +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedLongTypeMapping.cs @@ -1,38 +1,32 @@ -using System.Data; -using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.Json; -namespace EntityFrameworkCore.LibRed.Storage.Internal; - -/// -/// A is emulated as decimal(20,0) on disk — Jet/ACE has no native 64-bit integer -/// before ACE 16, and LibRed targets Access 2007 (ACE 12) files. EFCore.Jet's JetLongTypeMapping keeps -/// its DbType as and, at parameter-configuration time, pokes the underlying -/// OLE DB / ODBC parameter to the Numeric type via reflection to work around an x86/x64 conversion bug in those -/// drivers. LibRed is a fully managed engine that consumes the parameter's value object directly (its DbType is -/// metadata only), so it needs neither that reflection poke nor a Decimal DbType: it keeps -/// — the natural type of the CLR value — while still declaring the -/// decimal(20, 0) store type for DDL. -/// -public class LibRedLongTypeMapping : LongTypeMapping +namespace EntityFrameworkCore.LibRed.Storage.Internal { - public static new LibRedLongTypeMapping Default { get; } = new(); - - private LibRedLongTypeMapping() - : base(new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(long), jsonValueReaderWriter: JsonInt64ReaderWriter.Instance), - "decimal(20, 0)", - StoreTypePostfix.PrecisionAndScale, - System.Data.DbType.Int64) - .WithPrecisionAndScale(20, 0)) + public class LibRedLongTypeMapping : LongTypeMapping { - } + public static new LibRedLongTypeMapping Default { get; } = new("decimal(20, 0)", precision: 20, scale: 0, + StoreTypePostfix.PrecisionAndScale); - protected LibRedLongTypeMapping(RelationalTypeMappingParameters parameters) - : base(parameters) - { - } + public LibRedLongTypeMapping(string storeType, + int? precision = null, + int? scale = null, + StoreTypePostfix storeTypePostfix = StoreTypePostfix.PrecisionAndScale) + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(Int64), jsonValueReaderWriter: JsonInt64ReaderWriter.Instance), + storeType, + storeTypePostfix, + System.Data.DbType.Int64) + .WithPrecisionAndScale(precision, scale)) + { + } + + protected LibRedLongTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } - protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new LibRedLongTypeMapping(parameters); + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedLongTypeMapping(parameters); + } } diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedRelationalConnection.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedRelationalConnection.cs index c06512324..d00473978 100644 --- a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedRelationalConnection.cs +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedRelationalConnection.cs @@ -11,12 +11,8 @@ namespace EntityFrameworkCore.LibRed.Storage.Internal; /// the EFCore.Jet provider on its own managed engine. /// public class LibRedRelationalConnection(RelationalConnectionDependencies dependencies) - : JetRelationalConnection(dependencies), ILibRedRelationalConnection + : RelationalConnection(dependencies), ILibRedRelationalConnection { protected override DbConnection CreateDbConnection() => new LibRedConnection { ConnectionString = ConnectionString }; - - public override IJetRelationalConnection CreateEmptyConnection() - => throw new NotSupportedException( - "LibRed does not support masterless (empty) connections yet — database creation/drop is not implemented."); } diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedShortTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedShortTypeMapping.cs new file mode 100644 index 000000000..71c70af91 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedShortTypeMapping.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data; + +namespace EntityFrameworkCore.LibRed.Storage.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class LibRedShortTypeMapping : ShortTypeMapping +{ + public static new LibRedShortTypeMapping Default { get; } = new("smallint", System.Data.DbType.Int16); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public LibRedShortTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Int16) + : base(storeType, dbType) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected LibRedShortTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// Creates a copy of this mapping. + /// + /// The parameters for this mapping. + /// The newly created mapping. + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedShortTypeMapping(parameters); + + /// + /// A bare integer literal is Long/Integer to Jet, so a short (smallint) constant must be wrapped in + /// CINT (VBA CInt → 16-bit Integer) — mirroring 's CBYTE — otherwise the + /// reader gets an Int32 and GetInt16 throws. + /// + protected override string GenerateNonNullSqlLiteral(object value) + { + return $"CINT({base.GenerateNonNullSqlLiteral(value)})"; + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedStringTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedStringTypeMapping.cs new file mode 100644 index 000000000..083e3029e --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedStringTypeMapping.cs @@ -0,0 +1,120 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class LibRedStringTypeMapping : StringTypeMapping + { + private readonly bool _keepLineBreakCharacters; + private readonly int _maxSpecificSize; + + public static new LibRedStringTypeMapping Default { get; } = new(); + + private static readonly CaseInsensitiveValueComparer CaseInsensitiveValueComparer = new(); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public LibRedStringTypeMapping( + string? storeType = null, + bool unicode = true , + int? size = null, + bool fixedLength = false, + StoreTypePostfix? storeTypePostfix = null, + bool keepLineBreakCharacters = false, + bool useKeyComparison = false) + : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(string), comparer: useKeyComparison ? CaseInsensitiveValueComparer : null, keyComparer:useKeyComparison ? CaseInsensitiveValueComparer : null, jsonValueReaderWriter: JsonStringReaderWriter.Instance), + storeType ?? GetStoreName(fixedLength), + storeTypePostfix ?? StoreTypePostfix.Size, + (fixedLength + ? System.Data.DbType.StringFixedLength + : (DbType?)System.Data.DbType.String), + unicode, + size, + fixedLength), + keepLineBreakCharacters) + { + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected LibRedStringTypeMapping(RelationalTypeMappingParameters parameters, bool keepLineBreakCharacters) + : base(parameters) + { + _keepLineBreakCharacters = keepLineBreakCharacters; + _maxSpecificSize = CalculateSize(parameters.Size); + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedStringTypeMapping(parameters, _keepLineBreakCharacters); + + private static string GetStoreName(bool fixedLength) + => fixedLength + ? "char" + : "varchar"; + + private static int CalculateSize(int? size) + { + return size.HasValue && size < 255 + ? size.Value + : 255; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override void ConfigureParameter(DbParameter parameter) + { + // For strings and byte arrays, set the max length to the size facet if specified, or + // 255 characters if no size facet specified, if the data will fit so as to avoid query cache + // fragmentation by setting lots of different Size values otherwise always set to + // -1 (unbounded) to avoid SQL client size inference. + + var value = parameter.Value; + var length = (value as string)?.Length ?? (value as byte[])?.Length; + + parameter.Size = value == null || + value == DBNull.Value || + length != null && length <= _maxSpecificSize + ? _maxSpecificSize + : -1; + } + + /// + /// Generates the SQL representation of a literal value. + /// + /// The literal value. + /// + /// The generated string. + /// + protected override string GenerateNonNullSqlLiteral(object value) + { + var escaped = $"'{EscapeSqlLiteral((string)value)}'"; + + // BUG: EF Core indents idempotent scripts, which can lead to unexpected values for strings + // that contain line breaks. + // Tracked by: https://github.com/aspnet/EntityFrameworkCore/issues/15256 + // + // Convert line break characters to their CHR() representation as a workaround. + + return _keepLineBreakCharacters + ? escaped + : escaped + .Replace("\r\n", "' & CHR(13) & CHR(10) & '") + .Replace("\r", "' & CHR(13) & '") + .Replace("\n", "' & CHR(10) & '"); + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedTimeOnlyTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedTimeOnlyTypeMapping.cs new file mode 100644 index 000000000..ebe0993e9 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedTimeOnlyTypeMapping.cs @@ -0,0 +1,44 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + public class LibRedTimeOnlyTypeMapping : TimeOnlyTypeMapping + { + public static new LibRedTimeOnlyTypeMapping Default { get; } = new LibRedTimeOnlyTypeMapping("time"); + public LibRedTimeOnlyTypeMapping( + string storeType) + : base(storeType) + { + } + + protected LibRedTimeOnlyTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override void ConfigureParameter(DbParameter parameter) + { + base.ConfigureParameter(parameter); + if (parameter.Value is TimeOnly timeOnly) + { + timeOnly.Deconstruct(out int hour, out int min, out int sec, out int msec); + parameter.Value = new TimeSpan(0, hour, min, sec, msec); + } + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedTimeOnlyTypeMapping(parameters); + + protected override string GenerateNonNullSqlLiteral(object value) + { + return ((TimeOnly)value).Millisecond != 0 + ? FormattableString.Invariant($@"TIMEVALUE('{value:HH\:mm\:ss\.fff}')") + : FormattableString.Invariant($@"TIMEVALUE('{value:HH\:mm\:ss}')"); + } + + protected override string ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase) + { + return base.ProcessStoreType(parameters, storeTypeNameBase, storeTypeNameBase); + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedTimeSpanTypeMapping.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedTimeSpanTypeMapping.cs new file mode 100644 index 000000000..cea6d50b4 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedTimeSpanTypeMapping.cs @@ -0,0 +1,35 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace EntityFrameworkCore.LibRed.Storage.Internal +{ + public class LibRedTimeSpanTypeMapping : TimeSpanTypeMapping + { + public static new LibRedTimeSpanTypeMapping Default { get; } = new LibRedTimeSpanTypeMapping("datetime"); + + public LibRedTimeSpanTypeMapping( + string storeType) + : base(storeType) + { + } + + protected LibRedTimeSpanTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new LibRedTimeSpanTypeMapping(parameters); + + protected override string ProcessStoreType(RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase) + { + return base.ProcessStoreType(parameters, storeTypeNameBase, storeTypeNameBase); + } + + protected override string GenerateNonNullSqlLiteral(object value) + { + return ((TimeSpan)value).Milliseconds != 0 + ? FormattableString.Invariant($@"TIMEVALUE('{value:hh\:mm\:ss\.fff}')") + : FormattableString.Invariant($@"TIMEVALUE('{value:hh\:mm\:ss}')"); + } + } +} diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedTypeMappingSource.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedTypeMappingSource.cs index 9d4fa0f26..33f194974 100644 --- a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedTypeMappingSource.cs +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedTypeMappingSource.cs @@ -1,28 +1,441 @@ -using EntityFrameworkCore.Jet.Infrastructure.Internal; -using EntityFrameworkCore.Jet.Storage.Internal; -using Microsoft.EntityFrameworkCore.Storage; - -namespace EntityFrameworkCore.LibRed.Storage.Internal; - -/// -/// LibRed's relational type-mapping source. It reuses every EFCore.Jet mapping unchanged, but substitutes a -/// driver-free mapping () whose DbType reflects the type -/// the value is actually stored as (decimal(20,0)) rather than Int64 — EFCore.Jet only reports -/// Decimal via an OLE DB / ODBC reflection poke that a native engine has no reason to reproduce. -/// -public class LibRedTypeMappingSource : JetTypeMappingSource +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Data; +using System.Text.Json; +using EntityFrameworkCore.Jet.Internal; +using EntityFrameworkCore.LibRed.Infrastructure.Internal; + +namespace EntityFrameworkCore.LibRed.Storage.Internal { - public LibRedTypeMappingSource( - TypeMappingSourceDependencies dependencies, - RelationalTypeMappingSourceDependencies relationalDependencies, - IJetOptions options) - : base(dependencies, relationalDependencies, options) + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class LibRedTypeMappingSource : RelationalTypeMappingSource { - } + private readonly LibRedByteArrayTypeMapping _fixedLengthBinary = new("binary"); + private readonly LibRedByteArrayTypeMapping _variableLengthBinary = new("varbinary"); + private readonly LibRedByteArrayTypeMapping _variableLengthMaxBinary = new("varbinary", storeTypePostfix: StoreTypePostfix.None); + private readonly LibRedByteArrayTypeMapping _unboundedBinary = new("longbinary", storeTypePostfix: StoreTypePostfix.None); - protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) - { - RelationalTypeMapping? mapping = base.FindMapping(mappingInfo); - return mapping is JetLongTypeMapping ? LibRedLongTypeMapping.Default : mapping; + private readonly LibRedBoolTypeMapping _bit = new("bit"); // JET bits are not nullable + private readonly LibRedBoolTypeMapping _bool = LibRedBoolTypeMapping.Default; + + // We just map counter etc. to integer. Whether an integer property/column is actually a counter + // is determined by the value generation type. + private readonly LibRedIntTypeMapping _counter = LibRedIntTypeMapping.Default; + + private readonly LibRedByteTypeMapping _byte = LibRedByteTypeMapping.Default; // unsigned, there is no signed byte in Jet + private readonly LibRedShortTypeMapping _smallint = LibRedShortTypeMapping.Default; + private readonly LibRedIntTypeMapping _integer = LibRedIntTypeMapping.Default; + private readonly LibRedLongTypeMapping _bigint = LibRedLongTypeMapping.Default; + + private readonly LibRedFloatTypeMapping _single = LibRedFloatTypeMapping.Default; + private readonly LibRedDoubleTypeMapping _double = LibRedDoubleTypeMapping.Default; + + private readonly LibRedDecimalTypeMapping _decimal = new("decimal(18,2)", DbType.Decimal, precision: 18, scale: 2, StoreTypePostfix.PrecisionAndScale); + private readonly LibRedDecimalTypeMapping _decimal18_0 = new("decimal", DbType.Decimal, precision: 18, scale: 0); + private readonly LibRedDecimalTypeMapping _currency = new("currency", DbType.Currency, storeTypePostfix:StoreTypePostfix.None); + + private readonly LibRedDateTimeTypeMapping _datetime; + private readonly LibRedDateTimeTypeMapping _dateasdatetime; + private readonly LibRedDateTimeOffsetTypeMapping _datetimeoffset; + private readonly LibRedDateOnlyTypeMapping _dateonly; + private readonly LibRedTimeSpanTypeMapping _timespan; + private readonly LibRedTimeOnlyTypeMapping _timeonly; + + private readonly LibRedStringTypeMapping _fixedLengthUnicodeString = new("char", unicode: true); + private readonly LibRedStringTypeMapping _variableLengthUnicodeString = new("varchar", unicode: true); + private readonly LibRedStringTypeMapping _variableLengthMaxUnicodeString = new("varchar", unicode: true, size: 255, storeTypePostfix: StoreTypePostfix.Size); + private readonly LibRedStringTypeMapping _unboundedUnicodeString = new("longchar", unicode: true, storeTypePostfix: StoreTypePostfix.None); + private readonly LibRedGuidTypeMapping _guid; + private readonly LibRedByteArrayTypeMapping _rowversion = new("varbinary", size: 8, + comparer: new ValueComparer( + (v1, v2) => StructuralComparisons.StructuralEqualityComparer.Equals(v1, v2), + v => StructuralComparisons.StructuralEqualityComparer.GetHashCode(v), + v => v.ToArray())); + + private readonly Dictionary _storeTypeMappings; + private readonly Dictionary _clrTypeMappings; + private readonly HashSet _disallowedMappings; + + private readonly ILibRedOptions _options; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public LibRedTypeMappingSource( + TypeMappingSourceDependencies dependencies, + RelationalTypeMappingSourceDependencies relationalDependencies, + ILibRedOptions options) + : base(dependencies, relationalDependencies) + { + // References: + // https://docs.microsoft.com/en-us/previous-versions/office/developer/office2000/aa140015(v=office.10) + // https://docs.microsoft.com/en-us/office/vba/access/concepts/error-codes/comparison-of-data-types + // https://support.office.com/en-us/article/equivalent-ansi-sql-data-types-7a0a6bef-ef25-45f9-8a9a-3c5f21b5c65d + // https://sourcedaddy.com/ms-access/sql-data-types.html + + // LibRed has a single native engine rather than an ODBC/OLE DB driver choice, so there is only + // one GUID mapping - EFCore.Jet's Odbc variant exists solely for that driver difference. + _guid = LibRedGuidTypeMapping.Default; + + // TODO: Check the types and their mappings against + // https://docs.microsoft.com/en-us/previous-versions/office/developer/office2000/aa140015(v=office.10) + + _datetime = LibRedDateTimeTypeMapping.Default; + _dateasdatetime = new LibRedDateTimeTypeMapping("date", dbType: DbType.Date); + _datetimeoffset = LibRedDateTimeOffsetTypeMapping.Default; + _dateonly = LibRedDateOnlyTypeMapping.Default; + _timeonly = LibRedTimeOnlyTypeMapping.Default; + _timespan = LibRedTimeSpanTypeMapping.Default; + + _storeTypeMappings + = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + {"binary", [_fixedLengthBinary] }, + + {"varbinary", [_variableLengthBinary] }, + {"binary varying", [_variableLengthBinary] }, + {"bit varying", [_variableLengthBinary] }, + + {"longbinary", [_unboundedBinary] }, + {"general", [_unboundedBinary] }, + {"image", [_unboundedBinary] }, + {"oleobject", [_unboundedBinary] }, + + {"bit", [_bit] }, + {"boolean", [_bit] }, + {"logical", [_bit] }, + {"logical1", [_bit] }, + {"yesno", [_bit] }, + + {"counter", [_counter] }, + {"identity", [_counter] }, + {"autoincrement", [_counter] }, + + {"byte", [_byte] }, + {"tinyint", [_byte] }, + {"integer1", [_byte] }, + + {"smallint", [_smallint] }, + {"short", [_smallint] }, + {"integer2", [_smallint] }, + + {"integer", [_integer] }, + {"long", [_integer] }, + {"int", [_integer] }, + {"integer4", [_integer] }, + //{"bigint", [_bigint] }, + + {"single", [_single] }, + {"real", [_single] }, + {"float4", [_single] }, + {"ieeesingle", [_single] }, + + {"double", [_double] }, + {"double precision", [_double] }, + {"float", [_double] }, + {"float8", [_double] }, + {"ieeedouble", [_double] }, + {"number", [_double] }, + + {"decimal", [_decimal18_0] }, + {"numeric", [_decimal18_0] }, + {"dec", [_decimal18_0] }, + + {"currency", [_currency] }, + {"money", [_currency] }, + + {"datetime", [_datetime] }, + {"date", [_dateonly, _dateasdatetime] }, + {"time", [_timeonly, _timespan] }, + + {"char", [_fixedLengthUnicodeString] }, + {"alphanumeric", [_fixedLengthUnicodeString] }, + {"character", [_fixedLengthUnicodeString] }, + {"nchar", [_fixedLengthUnicodeString] }, + {"national char", [_fixedLengthUnicodeString] }, + {"national character", [_fixedLengthUnicodeString] }, + + {"varchar", [_variableLengthUnicodeString] }, + {"string", [_variableLengthUnicodeString] }, + {"char varying", [_variableLengthUnicodeString] }, + {"character varying", [_variableLengthUnicodeString] }, + {"national char varying", [_variableLengthUnicodeString] }, + {"national character varying", [_variableLengthUnicodeString] }, + + {"longchar", [_unboundedUnicodeString] }, + {"longtext", [_unboundedUnicodeString] }, + {"memo", [_unboundedUnicodeString] }, + {"note", [_unboundedUnicodeString] }, + {"ntext", [_unboundedUnicodeString] }, + + {"guid", [_guid] }, + {"uniqueidentifier", [_guid] }, + + {"timestamp", [_rowversion] }, + }; + + _clrTypeMappings + = new Dictionary + { + {typeof(bool), _bool}, + {typeof(byte), _byte}, + {typeof(short), _smallint}, + {typeof(int), _integer}, + {typeof(long), _bigint}, + {typeof(float), _single}, + {typeof(double), _double}, + {typeof(decimal), _decimal}, // CHECK: Is this supported or do we need to use CURRENCY? + {typeof(DateTime), _datetime}, + {typeof(DateOnly), _dateonly}, + {typeof(DateTimeOffset), _datetimeoffset}, + {typeof(TimeSpan), _timespan}, + {typeof(TimeOnly), _timeonly}, + {typeof(Guid), _guid}, + { typeof(JsonTypePlaceholder), LibRedJsonTypeMapping.Default } + }; + + // These are disallowed only if specified without any kind of length specified in parenthesis. + // This is because we don't try to make a new type from this string and any max length value + // specified in the model, which means use of these strings is almost certainly an error, and + // if it is not an error, then using, for example, varbinary(1) will work instead. + _disallowedMappings + = new HashSet(StringComparer.OrdinalIgnoreCase) + { + }; + + _options = options; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override void ValidateMapping(CoreTypeMapping? mapping, IProperty? property) + { + if (mapping is RelationalTypeMapping relationalMapping) + { + if (_disallowedMappings.Contains(relationalMapping.StoreType)) + { + if (property == null) + { + throw new ArgumentException(JetStrings.UnqualifiedDataType(relationalMapping.StoreType)); + } + + throw new ArgumentException( + JetStrings.UnqualifiedDataTypeOnProperty(relationalMapping.StoreType, property.Name)); + } + } + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) + => base.FindMapping(mappingInfo) + ?? FindRawMapping(mappingInfo)?.WithTypeMappingInfo(mappingInfo); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + private RelationalTypeMapping? FindRawMapping(RelationalTypeMappingInfo mappingInfo) + { + var clrType = mappingInfo.ClrType; + var storeTypeName = mappingInfo.StoreTypeName; + + // A structural type (complex type or owned entity) mapped to JSON. EF asks for JsonTypePlaceholder + // *together with* the column's store type, and reports the column as unsupported if the answer is + // null. Jet stores JSON in a memo, so that store type is longchar - which is also the ordinary + // unbounded string mapping, whose CLR type is string. The store-type branch below would therefore + // find longchar, fail to match the placeholder, and answer null, so the question has to be settled + // before the store type gets a chance to shadow it. + if (clrType == typeof(JsonTypePlaceholder)) + { + return mappingInfo.StoreTypeNameBase switch + { + "longchar" => LibRedJsonTypeMapping.Default, + + // TEXT without a size is a synonym for LONGCHAR, as above. + "text" when !mappingInfo.Size.HasValue => LibRedJsonTypeMapping.Default, + + // Sized, so it gets cloned outside for the right size. + "varchar" or "text" => LibRedJsonTypeMapping.VarcharDefault, + + null => LibRedJsonTypeMapping.Default, + + // Anything else genuinely cannot hold JSON, and null is what tells EF to say so. + _ => null + }; + } + + if (storeTypeName != null) + { + var storeTypeNameBase = mappingInfo.StoreTypeNameBase; + // If the TEXT store type is used with a size argument like `TEXT(n)`, it is handled as a synonym + // for `VARCHAR(n)`. + // If the TEXT store type is used without a size argument like `TEXT`, it is handled as a synonym + // for `LONGCHAR`. + // See "Notes" in: https://support.office.com/en-us/article/equivalent-ansi-sql-data-types-7a0a6bef-ef25-45f9-8a9a-3c5f21b5c65d + if (storeTypeNameBase!.Equals("text", StringComparison.OrdinalIgnoreCase) && + !mappingInfo.IsFixedLength.GetValueOrDefault()) + { + if (mappingInfo.Size.GetValueOrDefault() > 0) + { + return clrType == null + || _variableLengthUnicodeString.ClrType == clrType + ? _variableLengthUnicodeString + : null; + } + else + { + return clrType == null + || _unboundedUnicodeString.ClrType == clrType + ? _unboundedUnicodeString + : null; + } + } + + if (_storeTypeMappings.TryGetValue(storeTypeName, out var mappings) + || _storeTypeMappings.TryGetValue(storeTypeNameBase, out mappings)) + { + // We found the user-specified store type. No CLR type was provided - we're probably scaffolding from an existing database, + // take the first mapping as the default. + if (clrType is null) + { + return mappings[0]; + } + + // A CLR type was provided - look for a mapping between the store and CLR types. If not found, fail + // immediately. + foreach (var m in mappings) + { + if (m.ClrType == clrType) + { + return m; + } + } + + return null; + } + + // A store type name was provided, but is unknown. This could be a domain (alias) type, in which case + // we proceed with a CLR type lookup (if the type doesn't exist at all the failure will come later). + } + + if (clrType != null) + { + if (_clrTypeMappings.TryGetValue(clrType, out var mapping)) + { + return mapping; + } + + if (clrType == typeof(string)) + { + var isFixedLength = mappingInfo.IsFixedLength == true; + + const int maxCharColumnSize = 255; + const int maxIndexedCharColumnSize = 255; + + var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? maxIndexedCharColumnSize : null); + bool extendtolongchar = false; + if (size > maxCharColumnSize) + { + size = isFixedLength ? maxCharColumnSize : null; + extendtolongchar = true; + } + + if (_options.UseShortTextForSystemString && size == null && !extendtolongchar) + + { + return new LibRedStringTypeMapping("varchar", unicode: true, size: 255); + } + + return size == null + ? _unboundedUnicodeString + : new LibRedStringTypeMapping( + storeType: isFixedLength + ? _fixedLengthUnicodeString.StoreTypeNameBase + : _variableLengthUnicodeString.StoreTypeNameBase, + size: size, + unicode: true, + useKeyComparison: mappingInfo.IsKeyOrIndex); + } + + if (clrType == typeof(byte[])) + { + if (mappingInfo.IsRowVersion == true) + { + return _rowversion; + } + + if (mappingInfo.ElementTypeMapping == null) + { + var isFixedLength = mappingInfo.IsFixedLength == true; + + const int maxBinaryColumnSize = 510; + + var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? maxBinaryColumnSize : null); + if (size > maxBinaryColumnSize) + { + size = isFixedLength ? maxBinaryColumnSize : null; + } + + return size == null + ? _unboundedBinary + : new LibRedByteArrayTypeMapping( + size: size, + storeType: isFixedLength + ? _fixedLengthBinary.StoreTypeNameBase + : _variableLengthBinary.StoreTypeNameBase); + } + } + } + + return null; + } + + private static readonly List NameBasesUsingPrecision = + [ + "decimal", + "dec", + "numeric" + ]; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override string? ParseStoreTypeName( + string? storeTypeName, + ref bool? unicode, + ref int? size, + ref int? precision, + ref int? scale) + { + if (storeTypeName == null) + { + return null; + } + + var originalSize = size; + var parsedName = base.ParseStoreTypeName(storeTypeName, ref unicode, ref size, ref precision, ref scale); + + if (size.HasValue + && NameBasesUsingPrecision.Any(n => storeTypeName.StartsWith(n, StringComparison.OrdinalIgnoreCase))) + { + precision = size; + size = originalSize; + } + + return parsedName; + } } } diff --git a/src/LibRed/LibRed.EFCore/Update/Internal/LibRedUpdateSqlGenerator.cs b/src/LibRed/LibRed.EFCore/Update/Internal/LibRedUpdateSqlGenerator.cs new file mode 100644 index 000000000..dff24edd3 --- /dev/null +++ b/src/LibRed/LibRed.EFCore/Update/Internal/LibRedUpdateSqlGenerator.cs @@ -0,0 +1,300 @@ +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Data; +using System.Globalization; +using System.Text; +using EntityFrameworkCore.Jet.Metadata; +using EntityFrameworkCore.Jet.Update.Internal; +using EntityFrameworkCore.Jet.Utilities; + +namespace EntityFrameworkCore.LibRed.Update.Internal +{ + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + /// + /// The service lifetime is . This means a single instance + /// is used by many instances. The implementation must be thread-safe. + /// This service cannot depend on services registered as . + /// + /// + public class LibRedUpdateSqlGenerator(UpdateSqlGeneratorDependencies dependencies) + : UpdateAndSelectSqlGenerator(dependencies), IJetUpdateSqlGenerator + { + public override ResultSetMapping AppendInsertOperation(StringBuilder commandStringBuilder, IReadOnlyModificationCommand command, + int commandPosition, out bool requiresTransaction) + { + //No database columns need to be read back + if (command.ColumnModifications.All(o => !o.IsRead)) + { + return AppendInsertReturningOperation(commandStringBuilder, command, commandPosition, out requiresTransaction); + } + return base.AppendInsertOperation(commandStringBuilder, command, commandPosition, out requiresTransaction); + } + + public ResultSetMapping AppendBulkInsertOperation(StringBuilder commandStringBuilder, IReadOnlyList modificationCommands, int commandPosition, out bool requiresTransaction) + { + var firstCommand = modificationCommands[0]; + var table = StoreObjectIdentifier.Table(firstCommand.TableName, modificationCommands[0].Schema); + + var readOperations = firstCommand.ColumnModifications.Where(o => o.IsRead).ToList(); + var writeOperations = firstCommand.ColumnModifications.Where(o => o.IsWrite).ToList(); + var keyOperations = firstCommand.ColumnModifications.Where(o => o.IsKey).ToList(); + + var writableOperations = modificationCommands[0].ColumnModifications + .Where( + o => + o.Property?.GetValueGenerationStrategy(table) != JetValueGenerationStrategy.IdentityColumn + && o.Property?.GetComputedColumnSql() is null + && o.Property?.GetColumnType() is not "rowversion" and not "timestamp") + .ToList(); + + requiresTransaction = modificationCommands.Count > 1; + foreach (var modification in modificationCommands) + { + AppendInsertOperation(commandStringBuilder, modification, commandPosition++, out var localRequiresTransaction); + requiresTransaction = requiresTransaction || localRequiresTransaction; + } + + // Each command above was appended as its own INSERT + SELECT, so the group produces one result set + // PER COMMAND, not one for the group. Report "not last": JetModificationCommandBatch stamps this + // single value onto every command in the group and then patches only the final entry to + // LastInResultSet. Returning LastInResultSet here instead left commands 1..N-1 each claiming to be the + // last result set, so the reader could stop advancing early. (For a single-command group the outcome is + // unchanged — the patch turns the one entry into LastInResultSet either way.) + // + // Deliberately NOT IsPositionalResultMappingEnabled: that says the rows carry a position column for + // correlating them back to commands, which exists for a set-based MERGE ... OUTPUT. Jet has no MERGE — + // these are ordered, one-row-each result sets, so there is nothing to correlate positionally. + return readOperations.Count == 0 + ? ResultSetMapping.NoResults + : ResultSetMapping.NotLastInResultSet; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override void AppendIdentityWhereCondition(StringBuilder commandStringBuilder, IColumnModification columnModification) + { + SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, columnModification.ColumnName); + commandStringBuilder.Append(" = "); + + commandStringBuilder.Append("@@identity"); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override void AppendRowsAffectedWhereCondition(StringBuilder commandStringBuilder, int expectedRowsAffected) + { + commandStringBuilder + .Append("@@ROWCOUNT = ") + .Append(expectedRowsAffected.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// rom your code. This API may change or be removed in future releases. + protected override ResultSetMapping AppendSelectAffectedCountCommand(StringBuilder commandStringBuilder, string name, string? schema, int commandPosition) + { + commandStringBuilder + .Append("SELECT @@ROWCOUNT") + .Append(SqlGenerationHelper.StatementTerminator).AppendLine() + .AppendLine(); + + return ResultSetMapping.LastInResultSet | ResultSetMapping.ResultSetWithRowsAffectedOnly; + } + + //If multiple columns were output, the SQL Server behavior is to produce a INSERT INTO ... OUTPUT statement + //Jet does not support OUTPUT, so we need to use a SELECT statement instead + //@@identity is available to get the identity value of the last inserted row. + //Most tables will only have one identity column, so the AppendIdentityWhereColumn only gets called once + //However if there is a complex identity so that you have more than one identity column, the rest of those get added + //Given @@identity only gets the value of the first identity column, we must only use that and not any others + + protected override void AppendWhereAffectedClause(StringBuilder commandStringBuilder, IReadOnlyList operations) + { + commandStringBuilder + .AppendLine() + .Append("WHERE "); + + AppendRowsAffectedWhereCondition(commandStringBuilder, 1); + + // Choose the conditions before writing any of them: appending " AND " up front leaves a dangling + // "AND" — invalid SQL — whenever no operation ends up qualifying. + var conditions = new List<(IColumnModification Column, bool ByIdentity)>(); + var identityUsed = false; + foreach (var v in operations) + { + if (v is { IsKey: true, IsRead: false }) + { + conditions.Add((v, false)); + } + else if (IsIdentityOperation(v) && !identityUsed && IsGeneratedByAutoNumber(v)) + { + // @@identity only ever holds the value of the FIRST AutoNumber, so at most one column + // can be matched this way. + conditions.Add((v, true)); + identityUsed = true; + } + } + + if (conditions.Count == 0) + { + return; + } + + commandStringBuilder + .Append(" AND ") + .AppendJoin( + conditions, (sb, c) => + { + if (c.ByIdentity) + { + AppendIdentityWhereCondition(sb, c.Column); + } + else + { + AppendWhereCondition(sb, c.Column, c.Column.UseOriginalValueParameter); + } + + return true; + }, " AND "); + } + + /// + /// Whether a store-generated read-back key column gets its value from a genuine AutoNumber, and so can + /// be located again with = @@identity. + /// + /// + /// @@identity reflects only an AutoNumber. A key column store-generated some other way — by a + /// DEFAULT or as a computed column — is not reflected in it, so col = @@identity would compare + /// against the previous insert's AutoNumber (possibly from another table entirely) and match no row. + /// Those columns get no condition at all: @@ROWCOUNT = 1 plus the other provided (non-read) key + /// columns identify the just-inserted row, and the generated value is what we read back. + /// + /// Tested negatively, against the ways a value can be generated *other* than by AutoNumber, rather + /// than positively against GetValueGenerationStrategy() == IdentityColumn: that also reports + /// when the model-level strategy or the property + /// itself is unavailable, which would wrongly drop the condition for a real AutoNumber. + /// + /// + private static bool IsGeneratedByAutoNumber(IColumnModification columnModification) + => columnModification.Property is not { } property + || (!property.TryGetDefaultValue(out _) + && property.GetDefaultValueSql() is null + && property.GetComputedColumnSql() is null); + + public override ResultSetMapping AppendStoredProcedureCall( + StringBuilder commandStringBuilder, + IReadOnlyModificationCommand command, + int commandPosition, + out bool requiresTransaction) + { + Check.DebugAssert(command.StoreStoredProcedure is not null, "command.StoredProcedure is not null"); + + var storedProcedure = command.StoreStoredProcedure; + + var resultSetMapping = ResultSetMapping.NoResults; + + foreach (var resultColumn in storedProcedure.ResultColumns) + { + resultSetMapping = ResultSetMapping.LastInResultSet; + + if (resultColumn == command.RowsAffectedColumn) + { + resultSetMapping |= ResultSetMapping.ResultSetWithRowsAffectedOnly; + } + else + { + resultSetMapping = ResultSetMapping.LastInResultSet; + break; + } + } + + Check.DebugAssert( + storedProcedure.Parameters.Any() || storedProcedure.ResultColumns.Any(), + "Stored procedure call with neither parameters nor result columns"); + + commandStringBuilder.Append("EXEC "); + + if (storedProcedure.ReturnValue is not null) + { + var returnValueModification = command.ColumnModifications.First(c => c.Column is IStoreStoredProcedureReturnValue); + + Check.DebugAssert(returnValueModification.UseCurrentValueParameter, "returnValueModification.UseCurrentValueParameter"); + Check.DebugAssert(!returnValueModification.UseOriginalValueParameter, "!returnValueModification.UseOriginalValueParameter"); + + SqlGenerationHelper.GenerateParameterNamePlaceholder(commandStringBuilder, returnValueModification.ParameterName!); + + commandStringBuilder.Append(" = "); + + resultSetMapping |= ResultSetMapping.HasOutputParameters; + } + + SqlGenerationHelper.DelimitIdentifier(commandStringBuilder, storedProcedure.Name, storedProcedure.Schema); + + if (storedProcedure.Parameters.Any()) + { + commandStringBuilder.Append(' '); + + var first = true; + + // Only positional parameter style supported for now, see #28439 + + // Note: the column modifications are already ordered according to the sproc parameter ordering + // (see ModificationCommand.GenerateColumnModifications) + for (var i = 0; i < command.ColumnModifications.Count; i++) + { + var columnModification = command.ColumnModifications[i]; + + if (columnModification.Column is not IStoreStoredProcedureParameter parameter) + { + continue; + } + + if (first) + { + first = false; + } + else + { + commandStringBuilder.Append(", "); + } + + Check.DebugAssert(columnModification.UseParameter, "Column modification matched a parameter, but UseParameter is false"); + + commandStringBuilder.Append(columnModification.UseOriginalValueParameter + ? columnModification.OriginalParameterName! + : columnModification.ParameterName!); + + // Note that in/out parameters also get suffixed with OUTPUT in SQL Server + if (parameter.Direction.HasFlag(ParameterDirection.Output)) + { + commandStringBuilder.Append(" OUTPUT"); + resultSetMapping |= ResultSetMapping.HasOutputParameters; + } + } + } + + commandStringBuilder.AppendLine(SqlGenerationHelper.StatementTerminator); + + requiresTransaction = true; + + return resultSetMapping; + } + } +} diff --git a/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs b/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs index 9f6c4e613..06cdbf5db 100644 --- a/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs +++ b/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs @@ -16,23 +16,36 @@ internal static class AccessTypeMapper public static ColumnSpec ToColumnSpec(ColumnDefinition column, JetVersion version) => MapType(column, version) with { IsNullable = !column.NotNull }; - private static ColumnSpec MapType(ColumnDefinition column, JetVersion version) - { - // Collapse any internal whitespace so two-word aliases ("character varying") match. - string t = string.Join(' ', column.TypeName.ToUpperInvariant().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)); - - // BIGINT and DATETIME2 were added in DIFFERENT format versions — verified against files authored with - // each feature enabled: BIGINT (Large Number) forces the ACE 16 / Access 2016 format (version byte - // 0x05), while DATETIME2 (Date/Time Extended) forces the ACE 17 / 2019+ format (0x06). On any older - // format the engine can't represent the type, so refuse to create/alter a column to one rather than - // silently producing a file the real Access version couldn't open. This is the single DDL choke point. - (JetVersion Min, string Label)? gate = t switch + /// + /// The minimum file format a declared type needs, or null for the types every format can hold. + /// + /// + /// BIGINT and DATETIME2 arrived in DIFFERENT format versions — verified against files authored with each + /// feature enabled: BIGINT (Large Number) forces the ACE 16 / Access 2016 format (version byte 0x05), + /// DATETIME2 (Date/Time Extended) the ACE 17 / 2019+ format (0x06). Below those the engine cannot + /// represent the type at all. + /// Callers that are about to write storage raise the file to meet this (see + /// StatementExecutor.MapColumn), which is what Access itself does. still + /// refuses a type the open file is too old for, so a caller that skips the upgrade — or cannot perform it, + /// on a read-only database — fails loudly instead of writing a column Access could not read. + /// + public static (JetVersion Min, string Label)? RequiredVersion(string typeName) => + Normalize(typeName) switch { "BIGINT" => (JetVersion.Version16_2016, "Access 2016 (ACE 16)"), "DATETIME2" => (JetVersion.Version17_2019, "Access 2019+ (ACE 17)"), _ => null, }; - if (gate is { } g && version < g.Min) + + /// Collapses internal whitespace so two-word aliases ("character varying") match. + private static string Normalize(string typeName) => + string.Join(' ', typeName.ToUpperInvariant().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)); + + private static ColumnSpec MapType(ColumnDefinition column, JetVersion version) + { + string t = Normalize(column.TypeName); + + if (RequiredVersion(t) is { } g && version < g.Min) throw new NotSupportedException( $"Column type '{column.TypeName}' requires {g.Label} or later; this database is {version}."); @@ -50,8 +63,12 @@ private static ColumnSpec MapType(ColumnDefinition column, JetVersion version) => Fixed(column, JetDataType.Int16, 2), "BYTE" or "TINYINT" or "INTEGER1" => Fixed(column, JetDataType.Byte, 1), + // Large Number. Always 8 bytes, yet ACE stores it in the row's VARIABLE region, not the fixed one + // — a descriptor carrying length 8 with the fixed flag clear (verified: a column ACE created reads + // back `length=8 fixed=False`, and the row lays it out behind the variable offset table). Declaring + // it fixed would put it somewhere ACE does not look for it. "BIGINT" - => Fixed(column, JetDataType.Int64, 8), + => new ColumnSpec(column.Name, JetDataType.Int64, 8, IsFixedLength: false), "REAL" or "SINGLE" or "IEEESINGLE" or "FLOAT4" => Fixed(column, JetDataType.Single, 4), "FLOAT" or "DOUBLE" or "DOUBLE PRECISION" or "IEEEDOUBLE" or "FLOAT8" or "NUMBER" @@ -64,6 +81,12 @@ private static ColumnSpec MapType(ColumnDefinition column, JetVersion version) // no file-format upgrade (verified vs ACE: SMALLDATETIME → DateTime, version byte unchanged). "DATETIME" or "DATE" or "TIME" or "TIMESTAMP" or "SMALLDATETIME" => Fixed(column, JetDataType.DateTime, 8), + // Date/Time Extended: a fixed 42-byte ASCII triple, not the 8-byte OA double (see JetTypeCodec). + // Version-gated to ACE 17 above. ACE's own DDL accepts only this bare spelling - DATETIME2(7), + // DATETIMEEXTENDED and DATE/TIME EXTENDED are all syntax errors - so there are no aliases to fold + // in, and the declared precision is always 7. + "DATETIME2" + => Fixed(column, JetDataType.DateTimeExtended, 42), "BIT" or "YESNO" or "BOOLEAN" or "LOGICAL" or "LOGICAL1" => Fixed(column, JetDataType.Boolean, 1), "GUID" or "UNIQUEIDENTIFIER" diff --git a/src/LibRed/LibRed.Engine/Execution/DecorrelationGate.cs b/src/LibRed/LibRed.Engine/Execution/DecorrelationGate.cs index 39850f780..3148078c2 100644 --- a/src/LibRed/LibRed.Engine/Execution/DecorrelationGate.cs +++ b/src/LibRed/LibRed.Engine/Execution/DecorrelationGate.cs @@ -41,13 +41,26 @@ namespace LibRed.Engine.Execution; internal sealed class DecorrelationGate { /// 25 ms of per-row work before the decorrelated form takes over. - private static readonly long Budget = Stopwatch.Frequency / 40; + private static readonly long DefaultBudget = Stopwatch.Frequency / 40; + private readonly long _budget; + private readonly Func _timestamp; private long _spent; + internal DecorrelationGate(long? budget = null, Func? timestamp = null) + { + _budget = budget ?? DefaultBudget; + if (_budget <= 0) throw new ArgumentOutOfRangeException(nameof(budget)); + _timestamp = timestamp ?? Stopwatch.GetTimestamp; + } + /// Whether enough per-row time has been spent to justify one pass over the whole body. - internal bool Ready => _spent >= Budget; + internal bool Ready => _spent >= _budget; /// Charges one per-row evaluation, given the timestamp taken just before it started. - internal void Charge(long startTimestamp) => _spent += Stopwatch.GetTimestamp() - startTimestamp; + internal void Charge(long startTimestamp) + { + long elapsed = _timestamp() - startTimestamp; + if (elapsed > 0) _spent += elapsed; + } } diff --git a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs index bac40d8ef..19828fa9f 100644 --- a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs +++ b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs @@ -135,12 +135,15 @@ private static bool TryNiladicFunction(ColumnReference c, out object? value) // value in the Jet expression service — so a trailing "$" is stripped and dispatched to the base name. string name = f.Name.ToUpperInvariant(); if (name.Length > 1 && name[^1] == '$') name = name[..^1]; + ValidateArity(name, f.Arguments.Count); return name switch { - "IIF" => IsTrue(f.Arguments[0]) ? Evaluate(f.Arguments[1]) : Evaluate(f.Arguments[2]), + "IIF" => IsTrue(f.Arguments[0]) ? Evaluate(f.Arguments[1]) + : f.Arguments.Count == 3 ? Evaluate(f.Arguments[2]) : null, "CHOOSE" => Choose(f), "SWITCH" => Switch(f), + "NULLIF" => NullIf(f), "DATEPART" => DatePart(Evaluate(f.Arguments[0]), Evaluate(f.Arguments[1])), "ROUND" => Round(f), "FIX" => Numeric1(f, Math.Truncate, Math.Truncate), // toward zero @@ -168,12 +171,15 @@ private static bool TryNiladicFunction(ColumnReference c, out object? value) // VBA/Access string functions. All propagate NULL; positions are 1-based. Comparisons default to // case-insensitive (Access "Option Compare Database" = Text), overridable by a compare argument. - "LEN" => Convert1(f, v => v.ToString()!.Length), - "LCASE" => Convert1(f, v => v.ToString()!.ToLowerInvariant()), - "UCASE" => Convert1(f, v => v.ToString()!.ToUpperInvariant()), - "TRIM" => Convert1(f, v => v.ToString()!.Trim(' ')), - "LTRIM" => Convert1(f, v => v.ToString()!.TrimStart(' ')), - "RTRIM" => Convert1(f, v => v.ToString()!.TrimEnd(' ')), + // ToText, not ToString: a binary column's value is a UTF-16 STRING to every text function, so + // Len(0x4100) is 1 (one character) where LenB is 2. Calling ToString() on a byte[] yields the + // literal "System.Byte[]", which silently produced nonsense — Len returned 13 for every value. + "LEN" => Convert1(f, v => ToText(v).Length), + "LCASE" => Convert1(f, v => ToText(v).ToLowerInvariant()), + "UCASE" => Convert1(f, v => ToText(v).ToUpperInvariant()), + "TRIM" => Convert1(f, v => ToText(v).Trim(' ')), + "LTRIM" => Convert1(f, v => ToText(v).TrimStart(' ')), + "RTRIM" => Convert1(f, v => ToText(v).TrimEnd(' ')), "LEFT" => StringInt(f, static (s, n) => n <= 0 ? "" : n >= s.Length ? s : s[..n]), "RIGHT" => StringInt(f, static (s, n) => n <= 0 ? "" : n >= s.Length ? s : s[^n..]), "MID" => Mid(f), @@ -290,6 +296,71 @@ private static bool TryNiladicFunction(ColumnReference c, out object? value) }; } + /// Rejects argument counts verified against ACE. Keep this table evidence-driven: add a function + /// only after its minimum/maximum have been exercised through ACE, since Jet includes quirks such as IIf's + /// accepted two-argument form (the omitted false branch is Null). + internal static void ValidateArity(string name, int count) + { + (int Min, int Max)? range = name switch + { + // Conversion, unary numeric/string/date/inspection functions and single-argument aliases. + "CBOOL" or "CBYTE" or "CINT" or "CLNG" or "CSNG" or "CDBL" or "CCUR" or "CDEC" + or "CSTR" or "CDATE" or "CVAR" + or "ABS" or "SGN" or "INT" or "FIX" or "SQR" or "EXP" or "LOG" or "SIN" or "COS" + or "TAN" or "ATN" + or "LEN" or "LCASE" or "UCASE" or "TRIM" or "LTRIM" or "RTRIM" or "SPACE" + or "STRREVERSE" or "STR" or "VAL" or "CHR" or "ASC" or "HEX" or "OCT" + or "DATEVALUE" or "TIMEVALUE" or "YEAR" or "MONTH" or "DAY" or "HOUR" or "MINUTE" + or "SECOND" or "ISDATE" or "ISNULL" or "ISNUMERIC" or "ISERROR" or "TYPENAME" or "VARTYPE" + or "QBCOLOR" or "ASCW" or "CHRW" or "ASCB" or "LENB" => (1, 1), + + "LEFT" or "RIGHT" or "STRING" or "LEFTB" or "RIGHTB" => (2, 2), + "MID" or "MIDB" => (2, 3), + "INSTR" or "INSTRREV" or "INSTRB" => (2, 4), + "STRCOMP" => (2, 3), + "STRCONV" => (2, 3), + "IIF" => (2, 3), + "NULLIF" => (2, 2), + "CHOOSE" => (2, int.MaxValue), + "SWITCH" => (2, int.MaxValue), + + "NOW" or "DATE" or "TIME" or "TIMER" or "GENUNIQUEID" or "GENGUID" => (0, 0), + "DATEADD" => (3, 3), + "DATEDIFF" => (3, 5), + "DATEPART" => (2, 4), + "DATESERIAL" or "TIMESERIAL" => (3, 3), + "WEEKDAY" or "MONTHNAME" => (1, 2), + "WEEKDAYNAME" => (1, 3), + + "RGB" => (3, 3), + "ROUND" => (1, 2), + "RND" => (0, 1), + "REPLACE" => (3, 6), + "FORMAT" => (1, 4), + "FORMATCURRENCY" or "FORMATNUMBER" or "FORMATPERCENT" => (1, 5), + "FORMATDATETIME" => (1, 2), + "PARTITION" => (4, 4), + + "PMT" or "FV" or "PV" or "NPER" => (3, 5), + "IPMT" or "PPMT" => (4, 6), + "RATE" => (3, 6), + "SLN" => (3, 3), + "SYD" => (4, 4), + "DDB" => (4, 5), + + "COUNT" or "SUM" or "AVG" or "MIN" or "MAX" or "FIRST" or "LAST" or "STDEV" or "VAR" + or "STDEVP" or "VARP" or "STDDEV" or "STDDEVP" => (1, 1), + _ => null, + }; + bool invalidPairs = name == "SWITCH" && count % 2 != 0; + if (range is { } valid && (count < valid.Min || count > valid.Max || invalidPairs)) + throw new InvalidOperationException( + $"Wrong number of arguments used with function {name} (expected " + + (name == "SWITCH" ? "condition/value pairs" : valid.Min == valid.Max + ? valid.Min.ToString(CultureInfo.InvariantCulture) + : valid.Max == int.MaxValue ? $"at least {valid.Min}" : $"{valid.Min} to {valid.Max}") + ")."); + } + /// Access Choose(index, choice-1, choice-2, …): returns the 1-based choice at /// 's index, or NULL when the index is out of range (verified vs ACE: Choose(0,…) /// and Choose(5,…) on three choices both return Null). A NULL index is an error in ACE ("Data type @@ -304,6 +375,31 @@ private static bool TryNiladicFunction(ColumnReference c, out object? value) return index < 1 || index > choiceCount ? null : Evaluate(f.Arguments[index]); } + /// + /// NULLIF(a, b): NULL when the two are equal, otherwise a. + /// + /// + /// A deliberate divergence from ACE, which has no such function — it answers "Undefined function 'NULLIF' + /// in expression" (verified). Access's own spelling of this is IIF(a = b, NULL, a), and that is what + /// this evaluates to; the difference is only that LibRed also accepts the name EF Core emits, so a query + /// using it runs here rather than failing at the engine. + /// + /// Equality follows the same comparison as the = operator, which makes the NULL cases fall out + /// correctly without special-casing: comparing with a NULL is unknown rather than equal, so + /// NULLIF(x, NULL) is x and NULLIF(NULL, y) is NULL — matching the IIF form, where an + /// unknown condition takes the false branch. + /// + /// Unlike the IIF spelling, a is evaluated once. + /// + private object? NullIf(FunctionCall f) + { + object? left = Evaluate(f.Arguments[0]); + if (left is null) return null; + + object? right = Evaluate(f.Arguments[1]); + return right is not null && Compare(left, right) == 0 ? null : left; + } + /// Access Switch(cond-1, value-1, cond-2, value-2, …): evaluates the conditions left to /// right and returns the value paired with the first true one, or NULL if none is true (verified vs ACE). /// The argument count must be even (condition/value pairs) — an odd count is an error in ACE ("Wrong number @@ -435,6 +531,9 @@ private static object VbaVal(string s) decimal => "Currency", DateTime => "Date", string => "String", + // A binary column reports as String, not Byte[] — ACE's expression service sees the value as a + // UTF-16 string (VarType 8 = VT_BSTR), so TypeName must say so even though LibRed holds a byte[]. + byte[] => "String", _ => v.GetType().Name, }; @@ -1114,6 +1213,7 @@ private static object BitwiseOp(object a, object b, Func op) = "h" => d.AddHours(n), "n" => d.AddMinutes(n), "s" => d.AddSeconds(n), + "ms" => d.AddMilliseconds(n), _ => throw new NotSupportedException($"DATEADD interval '{intervalV}' is not supported."), }; } @@ -1126,7 +1226,22 @@ private static object BitwiseOp(object a, object b, Func op) = if (d1V is null || d2V is null) return null; var d1 = Convert.ToDateTime(d1V, CultureInfo.InvariantCulture); var d2 = Convert.ToDateTime(d2V, CultureInfo.InvariantCulture); - return (intervalV?.ToString() ?? "").ToLowerInvariant() switch + string interval = (intervalV?.ToString() ?? "").ToLowerInvariant(); + + // "ms" is a LibRed extension — ACE's interval list stops at "s". It is available because LibRed stores + // the full OA double rather than truncating to whole seconds as ACE does, and it is exact: .NET's OA + // conversion quantises to whole milliseconds, so nothing below a millisecond survived storage anyway + // (measured: 12:34:56.123 round-trips with zero tick loss, .1234560 comes back as .123). + // + // Handled before the switch, and as Int64 rather than the Long Integer every other interval returns: a + // millisecond difference overflows Int32 after 25 days, and ToUnixTimeMilliseconds spans decades. A + // long arm inside the switch would widen every other interval's result type along with it. + if (interval == "ms") + { + return (long)(d2 - d1).TotalMilliseconds; + } + + return interval switch { "yyyy" => d2.Year - d1.Year, "q" => (d2.Year - d1.Year) * 4 + (d2.Month - 1) / 3 - (d1.Month - 1) / 3, @@ -1136,6 +1251,7 @@ private static object BitwiseOp(object a, object b, Func op) = "h" => (int)(d2 - d1).TotalHours, "n" => (int)(d2 - d1).TotalMinutes, "s" => (int)(d2 - d1).TotalSeconds, + // "ms" is handled above, as Int64. _ => throw new NotSupportedException($"DATEDIFF interval '{intervalV}' is not supported."), }; } @@ -1186,7 +1302,7 @@ private static object BitwiseOp(object a, object b, Func op) = object? right = Evaluate(b.Right); if (b.Operator == BinaryOperator.Concat) - return (left?.ToString() ?? "") + (right?.ToString() ?? ""); + return (left is null ? "" : ToText(left)) + (right is null ? "" : ToText(right)); if (left is null || right is null) return null; @@ -1199,9 +1315,12 @@ private static object BitwiseOp(object a, object b, Func op) = BinaryOperator.LessThanOrEqual => Compare(left, right) <= 0, BinaryOperator.GreaterThan => Compare(left, right) > 0, BinaryOperator.GreaterThanOrEqual => Compare(left, right) >= 0, - BinaryOperator.Like => Like(left.ToString()!, right.ToString()!), + // LIKE reads a binary value as text, so it is CASE-INSENSITIVE over a binary column even though + // '=' on the same column is byte-wise and case-sensitive. Verified vs ACE: `B LIKE 'A%'` matches + // both 0x4100 ('A') and 0x6100 ('a'), while `B = 0x4100` matches only the first. + BinaryOperator.Like => Like(ToText(left), ToText(right)), // Access '+' concatenates when either operand is text (but, unlike '&', null already propagated above). - BinaryOperator.Add => left is string || right is string ? left.ToString() + right.ToString() : Arithmetic(left, right, '+'), + BinaryOperator.Add => left is string || right is string ? ToText(left) + ToText(right) : Arithmetic(left, right, '+'), BinaryOperator.Subtract => Arithmetic(left, right, '-'), BinaryOperator.Multiply => Arithmetic(left, right, '*'), BinaryOperator.Divide => Divide(left, right), // Access '/' is floating division diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs index 2eea50063..517be0eb8 100644 --- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs @@ -791,7 +791,11 @@ private static HashSet ContributingQualifiers( { var (leftColumns, leftRows) = Execute(join.Left, outer); Expression? on = join.On; // null for a CROSS join (cartesian product) - bool leftOuter = join.Kind == JoinKind.Left; + // Which sides are preserved. FULL preserves both, so it is left-outer and right-outer at once. The + // right-preserving half costs more: a right row's fate is not settled until every left row has been + // tried, so those rows are tracked and emitted after the loop rather than inside it. + bool leftOuter = join.Kind is JoinKind.Left or JoinKind.Full; + bool rightOuter = join.Kind is JoinKind.Right or JoinKind.Full; // Index-nested-loop: the right side is a *correlated* index seek (keyed off the outer row). Re-execute // it per left row — seeking the inner index — instead of materialising and scanning the whole inner @@ -855,16 +859,18 @@ private static HashSet ContributingQualifiers( { var onScope = new EvalScope(columns, [], outer); // one scope/evaluator, rebound per combined row var onEval = new ExpressionEvaluator(onScope, this, parameters: _parameters, session: _session); + bool[]? rightMatched = rightOuter ? new bool[rightRows.Count] : null; foreach (object?[] left in leftRows) { bool matched = false; - foreach (object?[] right in rightRows) + for (int r = 0; r < rightRows.Count; r++) { - object?[] combined = [.. left, .. right]; + object?[] combined = [.. left, .. rightRows[r]]; if (on is null || onEval.Rebind(combined).IsTrue(on)) { matched = true; + if (rightMatched is not null) rightMatched[r] = true; yield return combined; } } @@ -872,6 +878,11 @@ private static HashSet ContributingQualifiers( if (leftOuter && !matched) yield return [.. left, .. new object?[rightColumns.Count]]; } + + // Right-preserving tail: every right row no left row matched, null-padded on the left. + for (int r = 0; rightMatched is not null && r < rightMatched.Length; r++) + if (!rightMatched[r]) + yield return [.. new object?[leftColumns.Count], .. rightRows[r]]; } return (columns, Rows()); @@ -888,8 +899,11 @@ private static HashSet ContributingQualifiers( // INNER/LEFT build the right side and probe with the left; RIGHT builds the left and probes with the // right (so the preserved — outer — side is always the probe side). The emitted row is [left.., right..] // regardless of which side was built. + // FULL builds the right and probes with the left, like INNER/LEFT - but it preserves the build side too, + // which the others never do, so it additionally tracks which build rows were hit. bool buildRight = join.Kind != JoinKind.Right; - bool preserveProbe = join.Kind is JoinKind.Left or JoinKind.Right; // probe side is the outer/preserved side + bool preserveProbe = join.Kind is JoinKind.Left or JoinKind.Right or JoinKind.Full; // probe side is outer + bool preserveBuild = join.Kind is JoinKind.Full; var buildColumns = buildRight ? rightColumns : leftColumns; var buildKeys = buildRight ? join.RightKeys : join.LeftKeys; var buildRowsEnum = buildRight ? rightRowsEnum : leftRows; @@ -904,11 +918,19 @@ private static HashSet ContributingQualifiers( var table = new Dictionary>(HashKeyComparer.Instance); var buildScope = new EvalScope(buildColumns, [], outer); var buildEval = new ExpressionEvaluator(buildScope, this, parameters: _parameters, session: _session); + // A null-key build row can never match, so it is normally dropped outright. Under FULL the build + // side is preserved, which makes "never matches" a reason to emit it, not to discard it - so those + // rows are set aside instead and joined to the unmatched tail below. + List? unhashableBuild = preserveBuild ? [] : null; foreach (object?[] b in buildRowsEnum) { buildScope.Rebind(b); var key = new object?[buildKeys.Count]; - if (!EvalKey(buildEval, buildKeys, key)) continue; // null key → unmatchable + if (!EvalKey(buildEval, buildKeys, key)) // null key → unmatchable + { + unhashableBuild?.Add(b); + continue; + } if (!table.TryGetValue(key, out List? bucket)) table[key] = bucket = []; bucket.Add(b); @@ -921,6 +943,7 @@ private static HashSet ContributingQualifiers( var onScope = new EvalScope(joinColumns, [], outer); var onEval = new ExpressionEvaluator(onScope, this, parameters: _parameters, session: _session); var probe = new object?[probeKeys.Count]; // reused; only used to look up, never stored + var matchedBuild = preserveBuild ? new HashSet(RowIdentityComparer.Instance) : null; foreach (object?[] p in probeRowsEnum) { @@ -936,6 +959,7 @@ private static HashSet ContributingQualifiers( if (onEval.Rebind(combined).IsTrue(on)) { matched = true; + matchedBuild?.Add(b); yield return combined; } } @@ -945,6 +969,24 @@ private static HashSet ContributingQualifiers( ? [.. p, .. new object?[rightWidth]] // probe is the left side; right is null : [.. new object?[leftWidth], .. p]; // probe is the right side; left is null } + + // FULL only: the build side is preserved as well, so every build row the probe never matched is + // emitted null-padded on the other side. This has to trail the whole probe pass - a build row is + // only unmatched once every probe row has failed to hit it. + if (preserveBuild) + { + object?[] Unmatched(object?[] b) => buildRight + ? [.. new object?[leftWidth], .. b] // build is the right side; left is null + : [.. b, .. new object?[rightWidth]]; // build is the left side; right is null + + foreach (List bucket in table.Values) + foreach (object?[] b in bucket) + if (!matchedBuild!.Contains(b)) + yield return Unmatched(b); + + foreach (object?[] b in unhashableBuild!) // null-key rows: unmatchable by construction + yield return Unmatched(b); + } } return (joinColumns, Rows()); @@ -960,6 +1002,17 @@ private static bool EvalKey(ExpressionEvaluator eval, IReadOnlyList return true; } + /// Identity, not value, over a row array: a FULL join's "was this build row ever matched?" set has to + /// distinguish two rows that happen to hold equal values, so it keys on the reference itself. + private sealed class RowIdentityComparer : IEqualityComparer + { + public static readonly RowIdentityComparer Instance = new(); + + public bool Equals(object?[]? x, object?[]? y) => ReferenceEquals(x, y); + + public int GetHashCode(object?[] obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } + /// Hash/equality over a composite join key that mirrors the evaluator's = within a type kind /// (the planner only builds a hash join over same-kind key columns). Key elements are never null. private sealed class HashKeyComparer : IEqualityComparer @@ -1215,6 +1268,7 @@ private ExpressionEvaluator Eval(IReadOnlyList columns, object?[] private object? ComputeAggregate(FunctionCall call, List group, IReadOnlyList columns, EvalScope? outer) { string name = call.Name.ToUpperInvariant(); + ExpressionEvaluator.ValidateArity(name, call.Arguments.Count); Expression? arg = call.Arguments.Count > 0 ? call.Arguments[0] : null; // COUNT is an Access Long Integer (32-bit) — EF reads it with GetInt32, so return int, not long. diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index b30b40f6e..bb637270d 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -30,15 +30,38 @@ internal sealed class StatementExecutor(JetDatabase database, IReadOnlyDictionar DropTableStatement dropTable => DropTable(dropTable.Table), DropViewStatement dropView => DropQueryObject(dropView.View, "view"), DropProcedureStatement dropProc => DropQueryObject(dropProc.Procedure, "procedure"), + SelectStatement { Into: not null } makeTable => ExecuteSelectInto(makeTable), InsertStatement insert => ExecuteInsert(insert), UpdateStatement update => ExecuteUpdate(update), DeleteStatement delete => ExecuteDelete(delete), _ => throw new NotSupportedException($"{statement.GetType().Name} cannot be executed as a non-query."), }; + /// + /// Maps a declared column type to its storage spec, first raising the database's format version if the + /// type needs a newer one than the file currently is. + /// + /// + /// Access does the upgrade itself rather than refusing the DDL — adding a Date/Time Extended column to an + /// ACE 12 database moves its version byte to 0x06, and for DATETIME2 that byte is the entire upgrade + /// (docs/format/page-00-database.md). Refusing instead would leave LibRed unable to do something the + /// engine it mirrors does routinely. + /// The raise joins this statement's transaction, so a failed CREATE/ALTER takes its format bump + /// back down with it. It is still one-way in the sense that matters: once committed, an Access older than + /// the new format cannot open the file — unavoidable, since the column it would find is one it cannot + /// read either. + /// + private ColumnSpec MapColumn(ColumnDefinition column) + { + if (AccessTypeMapper.RequiredVersion(column.TypeName) is { } required) + _database.EnsureFormatAtLeast(required.Min); + + return AccessTypeMapper.ToColumnSpec(column, _database.Format.Version); + } + private int ExecuteCreateTable(CreateTableStatement statement) { - var columns = statement.Columns.Select(c => AccessTypeMapper.ToColumnSpec(c, _database.Format.Version)).ToList(); + var columns = statement.Columns.Select(MapColumn).ToList(); foreach (var (spec, def) in columns.Zip(statement.Columns)) ValidateColumnDefault(spec, def.Default); IReadOnlyList? primaryKey = statement.PrimaryKey.Count > 0 ? statement.PrimaryKey : null; @@ -374,6 +397,11 @@ private int ExecuteCreateProcedure(CreateProcedureStatement statement) { // A procedure is a parameterized stored query: a view spec plus a parameter row per declared // parameter (name + Jet type code, resolved from the declared Access type name). + // + // Deliberately NOT MapColumn: this declares no storage, so there is no column forcing the file's + // hand, and a BIGINT/DATETIME2 parameter on an older format is left to fail. Upgrading a whole + // database for a saved query's parameter type is a bigger claim than anything measured — what ACE + // does with a new-type parameter in MSysQueries has not been probed, unlike the column case. var parameters = statement.Parameters .Select(p => new ViewParameterSpec( p.Name, @@ -481,7 +509,7 @@ private int DropConstraint(string table, string name) private int AddColumn(string table, ColumnDefinition column) { // NOT NULL and DEFAULT are written to the column's LvProp properties (Required / DefaultValue). - ColumnSpec spec = AccessTypeMapper.ToColumnSpec(column, _database.Format.Version); + ColumnSpec spec = MapColumn(column); ValidateColumnDefault(spec, column.Default); if (!_database.AddColumn(table, spec, column.Default)) throw new InvalidOperationException($"ALTER TABLE '{table}' ADD COLUMN '{column.Name}': the column already exists."); @@ -559,7 +587,7 @@ private int AddCheck(string table, AddCheckAction chk) private int AlterColumn(string table, AlterColumnAction alter) { var colDef = new ColumnDefinition(alter.Field, alter.TypeName, alter.Size, alter.Scale, NotNull: false, PrimaryKey: false); - _database.AlterColumn(table, alter.Field, AccessTypeMapper.ToColumnSpec(colDef, _database.Format.Version)); + _database.AlterColumn(table, alter.Field, MapColumn(colDef)); // Apply DEFAULT before Required so the column's property map keeps ACE's order (DefaultValue, then Required). if (alter.Default is not null) // ALTER COLUMN … DEFAULT: set the column's default after the type change _database.SetColumnDefault(table, alter.Field, alter.Default); @@ -618,6 +646,110 @@ private int ExecuteCreateActionProcedure(CreateActionProcedureStatement statemen OrderBy: d.OrderBy.Select(o => new ViewOrderBySpec(o.Expression, o.Descending)).ToList(), Top: d.Top); + /// + /// A make-table query: SELECT … INTO newtable FROM source. + /// + /// + /// The new table takes the result's column names and types and NOTHING else. Measured against ACE + /// (SelectIntoShapeProbeTest): a source's PRIMARY KEY and indexes are not copied, so archiving a + /// keyed table gives an unkeyed copy. An expression column is typed from the expression rather than from + /// any source column — Qty * 2 gives Int32, a concatenation gives Text at the 255-character + /// maximum, and SUM widens to Double. An empty result still creates the table. An existing name is + /// an error, which the docs call "a trappable error" and ACE reports as "Table 'X' already exists". + /// + private int ExecuteSelectInto(SelectStatement statement) + { + string target = statement.Into!; + if (_database.Catalog.Tables.Any(t => string.Equals(t.Name, target, StringComparison.OrdinalIgnoreCase))) + throw new SchemaObjectExistsException($"Table '{target}' already exists.", target); + + // Run the query first — with INTO stripped, or planning would recurse back into this method — and + // materialise it. The rows have to exist before the table does: the source may read a table this + // statement is about to change, and the row count is not known until the read completes. + ResultSet source = _scalarRunner.ExecuteQuery(Planning.IndexSelection.Apply( + Planning.QueryPlanner.PlanSelect(statement with { Into = null }), _database.Catalog)); + var rows = source.Rows.ToList(); + + // A result column that IS a source column keeps that column's DEFINITION — its type and its declared + // width. Only a computed column is typed from its value. Measured: a source Text(30) arrives as + // Text(60) bytes, not the Text(510) maximum, while a concatenation does get the maximum because + // there is no declared width to copy. + var sourceColumns = SourceColumnsFor(statement); + var specs = source.ColumnNames + .Select((name, i) => sourceColumns.TryGetValue(name, out ColumnDef? column) + ? new ColumnSpec(name, column.Type, column.Length, column.IsFixedLength, + Precision: column.Precision, Scale: column.Scale) + : ColumnSpecFor(name, source.ColumnTypes[i])) + .ToList(); + _database.CreateTable(target, specs); + + Table table = _database.OpenTable(target); + foreach (object?[] row in rows) + { + var values = new object?[specs.Count]; + Array.Copy(row, values, Math.Min(row.Length, values.Length)); + table.Insert(values); + } + + if (_session is not null) _session.RowCount = rows.Count; + return rows.Count; + } + + /// + /// The source columns a make-table's output names can be copied from, by output name. + /// + /// + /// Only projections that ARE a column carry a definition to copy: SELECT Label and + /// SELECT Label AS L do, SELECT Label & '!' does not. SELECT * takes every + /// column of every table in the FROM. Anything not found here falls back to typing from the value. + /// + private Dictionary SourceColumnsFor(SelectStatement statement) + { + var available = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string table in TablesIn(statement.From)) + if (_database.Catalog.Tables.FirstOrDefault( + t => string.Equals(t.Name, table, StringComparison.OrdinalIgnoreCase)) is { } def) + foreach (ColumnDef column in def.Columns) + available.TryAdd(column.Name, column); + + if (statement.IsSelectStar) return available; + + var byOutputName = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (SelectItem item in statement.Projection) + if (item.Value is ColumnReference reference && + available.TryGetValue(reference.Column, out ColumnDef? column)) + byOutputName.TryAdd(item.Alias ?? reference.Column, column); + return byOutputName; + } + + /// The table names a FROM clause reaches, so their column definitions can be found. + private static IEnumerable TablesIn(TableReference? from) => from switch + { + NamedTable named => [named.Name], + JoinTable join => [.. TablesIn(join.Left), .. TablesIn(join.Right)], + // A derived table has no stored column definitions to copy, so its columns fall back to being typed + // from their values — as a computed column does. + _ => [], + }; + + /// The column a COMPUTED result column becomes. Text takes the 255-character maximum, because an + /// expression carries no declared width — ACE gives a concatenation Text(255) rather than measuring the + /// values it produced. + private static ColumnSpec ColumnSpecFor(string name, Type clrType) => Type.GetTypeCode(clrType) switch + { + TypeCode.Boolean => new ColumnSpec(name, JetDataType.Boolean, 1, IsFixedLength: true), + TypeCode.Byte => new ColumnSpec(name, JetDataType.Byte, 1, IsFixedLength: true), + TypeCode.Int16 => new ColumnSpec(name, JetDataType.Int16, 2, IsFixedLength: true), + TypeCode.Int32 => new ColumnSpec(name, JetDataType.Int32, 4, IsFixedLength: true), + TypeCode.Single => new ColumnSpec(name, JetDataType.Single, 4, IsFixedLength: true), + TypeCode.Double => new ColumnSpec(name, JetDataType.Double, 8, IsFixedLength: true), + TypeCode.Decimal => new ColumnSpec(name, JetDataType.Currency, 8, IsFixedLength: true), + TypeCode.DateTime => new ColumnSpec(name, JetDataType.DateTime, 8, IsFixedLength: true), + _ when clrType == typeof(Guid) => new ColumnSpec(name, JetDataType.Guid, 16, IsFixedLength: true), + _ when clrType == typeof(byte[]) => new ColumnSpec(name, JetDataType.Binary, 255, IsFixedLength: false), + _ => new ColumnSpec(name, JetDataType.Text, 255 * 2, IsFixedLength: false), + }; + private int ExecuteInsert(InsertStatement statement) { Table table = _database.OpenTable(statement.Table); @@ -649,11 +781,14 @@ private int ExecuteInsert(InsertStatement statement) int affected = 0; object? lastIdentity = null; - foreach (IReadOnlyList rowExprs in statement.Rows) + + // One row, given the values already in target order — the two append forms differ only in where + // those come from: evaluated VALUES expressions, or a row of the source query's output. + void InsertRow(ReadOnlySpan supplied) { - if (rowExprs.Count != targets.Count) + if (supplied.Length != targets.Count) throw new InvalidOperationException( - $"INSERT has {rowExprs.Count} values but {targets.Count} target columns."); + $"INSERT has {supplied.Length} values but {targets.Count} target columns."); var values = new object?[columns.Count]; var provided = new HashSet(); @@ -661,7 +796,7 @@ private int ExecuteInsert(InsertStatement statement) { ColumnDef column = table.Definition.FindColumn(targets[i]) ?? throw new InvalidOperationException($"Column '{targets[i]}' does not exist in '{statement.Table}'."); - values[column.Index] = evaluator.Evaluate(rowExprs[i]); + values[column.Index] = supplied[i]; provided.Add(column.Index); } @@ -679,6 +814,40 @@ private int ExecuteInsert(InsertStatement statement) affected++; } + if (statement.Source is not null) + { + // The multiple-record form. The source is MATERIALISED before a single row is written: appending + // a table to itself otherwise feeds its own output back into the scan and never terminates. + // Access's INSERT INTO t SELECT * FROM t doubles the table and stops, so the read completes + // before the write begins. + ResultSet source = _scalarRunner.ExecuteQuery( + Planning.IndexSelection.Apply(Planning.QueryPlanner.PlanStatement(statement.Source), _database.Catalog)); + var rows = source.Rows.ToList(); + + // With no column list the source's output NAMES choose the target columns — ACE resolves by name, + // not by position. Measured, because the two only disagree when they disagree silently: + // INSERT INTO PDst SELECT B AS Name, A AS Id FROM PSrc + // stores Id=7, Name='seven' — the values routed by their aliases, not by the order they appear + // in. Positionally that would have put 'seven' in Id. ACE rejects a name the target lacks + // ("unknown field name: 'A'"), including through SELECT *, and FindColumn below does the same. + // + // An explicit column list is the other rule entirely: it names the targets and the source's + // values map positionally onto IT, whatever the source calls them. + if (statement.Columns.Count == 0) + targets = source.ColumnNames; + + foreach (object?[] row in rows) InsertRow(row); + } + else + { + foreach (IReadOnlyList rowExprs in statement.Rows) + { + var supplied = new object?[rowExprs.Count]; + for (int i = 0; i < rowExprs.Count; i++) supplied[i] = evaluator.Evaluate(rowExprs[i]); + InsertRow(supplied); + } + } + // Publish @@ROWCOUNT (rows this insert affected) and @@IDENTITY (the last AutoNumber generated) so a // following SELECT in the same batch can read the store-generated key back — the shape EF Core emits. // @@IDENTITY is connection-scoped and only overwritten by an insert that actually generates an id, diff --git a/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs b/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs index 08e28234a..61014dea7 100644 --- a/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs +++ b/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs @@ -169,8 +169,9 @@ private static PlanNode RewriteJoin(JoinNode j, JetCatalog catalog, HashSet statement switch + /// Plans a SELECT or a set operation over SELECTs. Public because an append query's source is + /// either — INSERT INTO t SELECT …, or a UNION feeding one. + public static PlanNode PlanStatement(SqlStatement statement) => statement switch { SelectStatement select => PlanSelect(select), SetOperationStatement set => new SetOperationNode( diff --git a/src/LibRed/LibRed.Engine/QueryEngine.cs b/src/LibRed/LibRed.Engine/QueryEngine.cs index beb3461e4..92784e59f 100644 --- a/src/LibRed/LibRed.Engine/QueryEngine.cs +++ b/src/LibRed/LibRed.Engine/QueryEngine.cs @@ -37,8 +37,33 @@ public QueryEngine(JetDatabase database, ISqlParser? parser = null) public ResultSet ExecuteQuery(string sql, IReadOnlyDictionary? parameters = null) { + // Text with no statement in it (blank, or only comments) has nothing to run and nothing to return. + // It takes no page scope at all — there is no work to isolate. + if (_parser.IsStatementless(sql)) return ResultSet.Empty; + + // Parse outside the gate — it touches no pages, and holding a file-wide scope across it would + // serialize parsing for no isolation benefit. The parsed shape then picks the scope. SqlStatement parsed = _parser.ParseStatement(sql); + return Scoped(parsed, () => ExecuteQueryCore(parsed, parameters)); + } + + /// Runs in the page scope needs: shared + /// (concurrent with other readers) for a statement that cannot write, exclusive otherwise so its pages + /// publish as one unit. Anything not provably read-only takes the exclusive scope — a shared scope that + /// then writes is rejected, not silently upgraded. + private T Scoped(SqlStatement statement, Func action) => + // A SELECT with INTO is a make-table query: it looks like a read and writes a table, so it takes the + // exclusive scope with the other writers. The guard above caught this rather than letting it through. + statement is SelectStatement { Into: null } or SetOperationStatement or SystemVariableSelectStatement + ? _database.ReadConsistent(action) + : _database.WriteExclusive(action); + + private ResultSet ExecuteQueryCore(SqlStatement parsed, IReadOnlyDictionary? parameters) + { if (parsed is ExecuteStatement exec) return ExecuteProcedure(exec, parameters).Rows; + // A make-table query is an action query however it was invoked: run it and return nothing, rather + // than handing the caller the rows it just wrote into a table. + if (parsed is SelectStatement { Into: not null }) return Route(parsed, parameters).Rows; SqlStatement ast = ViewExpander.Expand(parsed, _database.Catalog.Views, _parser); BoundStatement bound = _binder.Bind(ast); if (bound.Statement is TransactionControlStatement txnControl) @@ -55,6 +80,13 @@ public ResultSet ExecuteQuery(string sql, IReadOnlyDictionary? public int ExecuteNonQuery(string sql, IReadOnlyDictionary? parameters = null) => Execute(sql, parameters).RecordsAffected; + /// + /// True when holds no statement — blank, or nothing but comments. The ADO batch + /// splitter uses this to drop such a fragment instead of running it, so a trailing -- comment in a + /// batch cannot become the batch's "last statement" and mask the real one's rows-affected. + /// + public bool IsStatementless(string sql) => _parser.IsStatementless(sql); + /// Executes a stored action query (a CREATE PROCEDURE body that is not a SELECT) by name — the /// read-back counterpart of . The query is reconstructed from /// its catalog rows and run; a kind LibRed cannot execute (e.g. INSERT … SELECT) throws @@ -77,7 +109,16 @@ public int ExecuteStoredActionQuery(string name) /// public CommandResult Execute(string sql, IReadOnlyDictionary? parameters = null) { + // As in ExecuteQuery: nothing to parse, nothing to run. Reported as zero rows affected rather than + // the -1 a query returns, since a comment is an action that did nothing, not a result set. + if (_parser.IsStatementless(sql)) return new CommandResult(ResultSet.Empty, RecordsAffected: 0); + SqlStatement parsed = _parser.ParseStatement(sql); + return Scoped(parsed, () => ExecuteCore(parsed, parameters)); + } + + private CommandResult ExecuteCore(SqlStatement parsed, IReadOnlyDictionary? parameters) + { if (parsed is ExecuteStatement exec) return ExecuteProcedure(exec, parameters); SqlStatement ast = ViewExpander.Expand(parsed, _database.Catalog.Views, _parser); BoundStatement bound = _binder.Bind(ast); @@ -107,6 +148,11 @@ private CommandResult Route(SqlStatement statement, IReadOnlyDictionary or a (e.g. a UNION). public sealed record SubqueryTable(SqlStatement Query, string? Alias) : TableReference; -public enum JoinKind { Inner, Left, Right, Cross } +/// is a LibRed extension - ACE has no full outer join. +public enum JoinKind { Inner, Left, Right, Cross, Full } /// A join between two table references with an ON condition. public sealed record JoinTable( diff --git a/src/LibRed/LibRed.Sql/Ast/Statements.cs b/src/LibRed/LibRed.Sql/Ast/Statements.cs index 081ec501d..9a1f7ee32 100644 --- a/src/LibRed/LibRed.Sql/Ast/Statements.cs +++ b/src/LibRed/LibRed.Sql/Ast/Statements.cs @@ -27,7 +27,18 @@ public sealed record SelectStatement( // DISTINCTROW: dedupe on the underlying rows of the output-contributing tables (Access-specific). bool DistinctRow = false, // TOP n PERCENT: Top is a percentage (0–100) of the row count rather than an absolute count. - bool TopPercent = false) : SqlStatement; + bool TopPercent = false, + /// + /// The INTO newtable of a MAKE-TABLE query: the rows are written to a new table of that name + /// rather than returned. Null for an ordinary SELECT. + /// + /// + /// The new table takes the result's column names and types and nothing else. Measured against ACE: the + /// source's PRIMARY KEY and indexes are NOT copied, an expression column is typed from the expression + /// (Qty * 2 gives Int32, a concatenation gives Text(255)), SUM widens to Double, an empty + /// result still creates the table, and an existing name is an error ("Table 'X' already exists"). + /// + string? Into = null) : SqlStatement; /// EXECUTE|EXEC procedure [arg, …] — invokes a stored procedure/query by name, passing /// positional argument values that bind to its declared parameters (in declaration order). @@ -55,14 +66,28 @@ public sealed record SetOperationStatement( SetOperator Operator, SqlStatement Right) : SqlStatement; -/// An INSERT. is the DEFAULT VALUES form (no column or -/// value list): a single row where every column takes its default / AutoNumber; -/// is empty and holds one empty row. +/// +/// An INSERT — Access's two append-query forms. +/// +/// +/// The single-record form's values, INSERT INTO t (…) VALUES (…). Empty when +/// is set. +/// +/// +/// The multiple-record form's query, INSERT INTO t (…) SELECT … FROM …. Null for the +/// single-record form. Access documents the source as a SELECT; a full query expression is accepted here so +/// a UNION can feed an append, which is the shape EF emits from a Concat. +/// +/// +/// The DEFAULT VALUES form (no column or value list): a single row where every column takes its +/// default / AutoNumber. is empty and holds one empty row. +/// public sealed record InsertStatement( string Table, IReadOnlyList Columns, IReadOnlyList> Rows, - bool DefaultValues = false) : SqlStatement; + bool DefaultValues = false, + SqlStatement? Source = null) : SqlStatement; /// A column in a CREATE TABLE: its declared SQL type, optional size/scale, constraints, and the /// raw text of an optional DEFAULT value expression (stored as the column's DefaultValue property). diff --git a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 index 4c5d4fe3b..0e779dc15 100644 --- a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 +++ b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 @@ -1,6 +1,6 @@ // ANTLR4 grammar for the Jet/ACE (Microsoft Access) SQL dialect. // -// Scope: SELECT with projection/aliases, multi-table FROM with INNER/LEFT/RIGHT JOIN and +// Scope: SELECT with projection/aliases, multi-table FROM with INNER/LEFT/RIGHT/FULL JOIN and // derived-table subqueries, WHERE, ORDER BY, TOP. The parse tree is lowered into // LibRed.Sql.Ast by AstBuilder, so the rest of the engine never sees these generated types. // @@ -184,12 +184,22 @@ referentialAction | SET DEFAULT # SetDefaultAction ; -// INSERT … VALUES (…), or `INSERT INTO t DEFAULT VALUES` — the latter (EF Core emits it for an all-store- -// -generated/all-default row) inserts one row taking every column's default / AutoNumber. +// Access's two append-query forms: +// single-record INSERT INTO target [(field, …)] VALUES (value, …) +// multiple-record INSERT INTO target [(field, …)] SELECT [source.]field, … FROM tableexpression +// plus `INSERT INTO t DEFAULT VALUES` — EF Core emits that for an all-store-generated/all-default row, and +// it inserts one row taking every column's default / AutoNumber. +// +// The IN externaldatabase clause both forms allow is deliberately absent: appending into another file is +// part of the linked/external-database subsystem, which LibRed neither reads nor writes. +// +// The multiple-record source is a queryExpression rather than a bare selectStatement, so a UNION can feed an +// append — the shape EF emits from a Concat — which is a superset of what Access documents. insertStatement : INSERT INTO table=identifier ( (LPAREN columns+=identifier (COMMA columns+=identifier)* RPAREN)? - VALUES LPAREN expression (COMMA expression)* RPAREN + ( VALUES LPAREN expression (COMMA expression)* RPAREN + | source=queryExpression ) | DEFAULT VALUES ) ; @@ -205,8 +215,13 @@ queryTerm setOperator : UNION ALL? | INTERSECT | EXCEPT ; // The FROM clause is optional: ACE accepts a bare `SELECT 2` (verified) — a FROM-less SELECT yields one row. +// +// INTO makes it a MAKE-TABLE query: the rows go into a new table rather than back to the caller. +// SELECT field1[, field2[, …]] INTO newtable [IN externaldatabase] FROM source +// The IN externaldatabase clause is deliberately absent, as it is on INSERT — creating a table in another +// file is part of the linked-database subsystem LibRed does not have. selectStatement - : SELECT predicate=selectPredicate? topClause? selectList fromClause? whereClause? groupByClause? havingClause? orderByClause? + : SELECT predicate=selectPredicate? topClause? selectList (INTO into=identifier)? fromClause? whereClause? groupByClause? havingClause? orderByClause? ; // The optional row predicate. ALL is the default (return every row); DISTINCT dedupes on the output @@ -247,10 +262,15 @@ tablePrimary joinClause : joinType JOIN tablePrimary ON expression ; +// FULL [OUTER] JOIN is a LibRed extension: ACE has no full outer join at all, and no way to express one +// (its query designer offers only the three above). FULL is therefore a keyword here that is not reserved in +// Access, so a column actually named "Full" has to be bracketed or backticked - the same tax LEFT, RIGHT, +// ORDER and every other keyword already charge. joinType : INNER? # InnerJoin | LEFT OUTER? # LeftJoin | RIGHT OUTER? # RightJoin + | FULL OUTER? # FullJoin ; whereClause : WHERE expression ; @@ -345,6 +365,7 @@ MOD : [Mm][Oo][Dd] ; INNER : [Ii][Nn][Nn][Ee][Rr] ; LEFT : [Ll][Ee][Ff][Tt] ; RIGHT : [Rr][Ii][Gg][Hh][Tt] ; +FULL : [Ff][Uu][Ll][Ll] ; OUTER : [Oo][Uu][Tt][Ee][Rr] ; JOIN : [Jj][Oo][Ii][Nn] ; IN : [Ii][Nn] ; diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs index 7d895218f..23220f0ce 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs @@ -912,6 +912,17 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result. public virtual Result VisitRightJoin([NotNull] AccessSqlParser.RightJoinContext context) { return VisitChildren(context); } /// + /// Visit a parse tree produced by the FullJoin + /// labeled alternative in . + /// + /// The default implementation returns the result of calling + /// on . + /// + /// + /// The parse tree. + /// The visitor result. + public virtual Result VisitFullJoin([NotNull] AccessSqlParser.FullJoinContext context) { return VisitChildren(context); } + /// /// Visit a parse tree produced by . /// /// The default implementation returns the result of calling diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs index 9615c9fdc..5d4015cdd 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs @@ -35,22 +35,22 @@ public partial class AccessSqlLexer : Lexer { protected static PredictionContextCache sharedContextCache = new PredictionContextCache(); public const int SELECT=1, FROM=2, WHERE=3, TOP=4, AS=5, AND=6, OR=7, NOT=8, BAND=9, BOR=10, - BXOR=11, BNOT=12, LIKE=13, MOD=14, INNER=15, LEFT=16, RIGHT=17, OUTER=18, - JOIN=19, IN=20, ON=21, ORDER=22, GROUP=23, IS=24, BY=25, HAVING=26, EXISTS=27, - IF=28, THEN=29, DISTINCTROW=30, DISTINCT=31, PERCENT=32, BETWEEN=33, UNION=34, - ALL=35, INTERSECT=36, EXCEPT=37, CREATE=38, TABLE=39, BEGIN=40, COMMIT=41, - ROLLBACK=42, TRANSACTION=43, WORK=44, ALTER=45, RENAME=46, TO=47, ADD=48, - DROP=49, COLUMN=50, INSERT=51, INTO=52, VALUES=53, PRIMARY=54, KEY=55, - CONSTRAINT=56, FOREIGN=57, REFERENCES=58, DELETE=59, UPDATE=60, CASCADE=61, - RESTRICT=62, ACTION=63, SET=64, DEFAULT=65, NO=66, UNIQUE=67, INDEX=68, - TEMPORARY=69, WITH=70, COMPRESSION=71, COMP=72, DISALLOW=73, IGNORE=74, - CHECK=75, VIEW=76, PROCEDURE=77, PARAMETERS=78, EXECUTE=79, EXEC=80, ASC=81, - DESC=82, TRUE=83, FALSE=84, NULL=85, STAR=86, SLASH=87, BACKSLASH=88, - CARET=89, PLUS=90, MINUS=91, AMP=92, EQ=93, NEQ=94, LTE=95, GTE=96, LT=97, - GT=98, LPAREN=99, RPAREN=100, COMMA=101, DOT=102, SEMI=103, SYSVAR=104, - PARAM=105, HEX_LITERAL=106, INTEGER_LITERAL=107, NUMBER_LITERAL=108, STRING_LITERAL=109, - DATE_LITERAL=110, GUID_LITERAL=111, BRACKET_ID=112, BACKTICK_ID=113, IDENTIFIER=114, - WS=115, LINE_COMMENT=116, BLOCK_COMMENT=117; + BXOR=11, BNOT=12, LIKE=13, MOD=14, INNER=15, LEFT=16, RIGHT=17, FULL=18, + OUTER=19, JOIN=20, IN=21, ON=22, ORDER=23, GROUP=24, IS=25, BY=26, HAVING=27, + EXISTS=28, IF=29, THEN=30, DISTINCTROW=31, DISTINCT=32, PERCENT=33, BETWEEN=34, + UNION=35, ALL=36, INTERSECT=37, EXCEPT=38, CREATE=39, TABLE=40, BEGIN=41, + COMMIT=42, ROLLBACK=43, TRANSACTION=44, WORK=45, ALTER=46, RENAME=47, + TO=48, ADD=49, DROP=50, COLUMN=51, INSERT=52, INTO=53, VALUES=54, PRIMARY=55, + KEY=56, CONSTRAINT=57, FOREIGN=58, REFERENCES=59, DELETE=60, UPDATE=61, + CASCADE=62, RESTRICT=63, ACTION=64, SET=65, DEFAULT=66, NO=67, UNIQUE=68, + INDEX=69, TEMPORARY=70, WITH=71, COMPRESSION=72, COMP=73, DISALLOW=74, + IGNORE=75, CHECK=76, VIEW=77, PROCEDURE=78, PARAMETERS=79, EXECUTE=80, + EXEC=81, ASC=82, DESC=83, TRUE=84, FALSE=85, NULL=86, STAR=87, SLASH=88, + BACKSLASH=89, CARET=90, PLUS=91, MINUS=92, AMP=93, EQ=94, NEQ=95, LTE=96, + GTE=97, LT=98, GT=99, LPAREN=100, RPAREN=101, COMMA=102, DOT=103, SEMI=104, + SYSVAR=105, PARAM=106, HEX_LITERAL=107, INTEGER_LITERAL=108, NUMBER_LITERAL=109, + STRING_LITERAL=110, DATE_LITERAL=111, GUID_LITERAL=112, BRACKET_ID=113, + BACKTICK_ID=114, IDENTIFIER=115, WS=116, LINE_COMMENT=117, BLOCK_COMMENT=118; public static string[] channelNames = { "DEFAULT_TOKEN_CHANNEL", "HIDDEN" }; @@ -61,21 +61,21 @@ public const int public static readonly string[] ruleNames = { "SELECT", "FROM", "WHERE", "TOP", "AS", "AND", "OR", "NOT", "BAND", "BOR", - "BXOR", "BNOT", "LIKE", "MOD", "INNER", "LEFT", "RIGHT", "OUTER", "JOIN", - "IN", "ON", "ORDER", "GROUP", "IS", "BY", "HAVING", "EXISTS", "IF", "THEN", - "DISTINCTROW", "DISTINCT", "PERCENT", "BETWEEN", "UNION", "ALL", "INTERSECT", - "EXCEPT", "CREATE", "TABLE", "BEGIN", "COMMIT", "ROLLBACK", "TRANSACTION", - "WORK", "ALTER", "RENAME", "TO", "ADD", "DROP", "COLUMN", "INSERT", "INTO", - "VALUES", "PRIMARY", "KEY", "CONSTRAINT", "FOREIGN", "REFERENCES", "DELETE", - "UPDATE", "CASCADE", "RESTRICT", "ACTION", "SET", "DEFAULT", "NO", "UNIQUE", - "INDEX", "TEMPORARY", "WITH", "COMPRESSION", "COMP", "DISALLOW", "IGNORE", - "CHECK", "VIEW", "PROCEDURE", "PARAMETERS", "EXECUTE", "EXEC", "ASC", - "DESC", "TRUE", "FALSE", "NULL", "STAR", "SLASH", "BACKSLASH", "CARET", - "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", "LT", "GT", "LPAREN", - "RPAREN", "COMMA", "DOT", "SEMI", "SYSVAR", "PARAM", "HEX_LITERAL", "INTEGER_LITERAL", - "NUMBER_LITERAL", "EXPONENT", "STRING_LITERAL", "DATE_LITERAL", "GUID_LITERAL", - "HEXDIGIT", "BRACKET_ID", "BACKTICK_ID", "IDENTIFIER", "WS", "LINE_COMMENT", - "BLOCK_COMMENT" + "BXOR", "BNOT", "LIKE", "MOD", "INNER", "LEFT", "RIGHT", "FULL", "OUTER", + "JOIN", "IN", "ON", "ORDER", "GROUP", "IS", "BY", "HAVING", "EXISTS", + "IF", "THEN", "DISTINCTROW", "DISTINCT", "PERCENT", "BETWEEN", "UNION", + "ALL", "INTERSECT", "EXCEPT", "CREATE", "TABLE", "BEGIN", "COMMIT", "ROLLBACK", + "TRANSACTION", "WORK", "ALTER", "RENAME", "TO", "ADD", "DROP", "COLUMN", + "INSERT", "INTO", "VALUES", "PRIMARY", "KEY", "CONSTRAINT", "FOREIGN", + "REFERENCES", "DELETE", "UPDATE", "CASCADE", "RESTRICT", "ACTION", "SET", + "DEFAULT", "NO", "UNIQUE", "INDEX", "TEMPORARY", "WITH", "COMPRESSION", + "COMP", "DISALLOW", "IGNORE", "CHECK", "VIEW", "PROCEDURE", "PARAMETERS", + "EXECUTE", "EXEC", "ASC", "DESC", "TRUE", "FALSE", "NULL", "STAR", "SLASH", + "BACKSLASH", "CARET", "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", + "LT", "GT", "LPAREN", "RPAREN", "COMMA", "DOT", "SEMI", "SYSVAR", "PARAM", + "HEX_LITERAL", "INTEGER_LITERAL", "NUMBER_LITERAL", "EXPONENT", "STRING_LITERAL", + "DATE_LITERAL", "GUID_LITERAL", "HEXDIGIT", "BRACKET_ID", "BACKTICK_ID", + "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT" }; @@ -96,13 +96,13 @@ public AccessSqlLexer(ICharStream input, TextWriter output, TextWriter errorOutp null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, "'*'", "'/'", "'\\'", "'^'", "'+'", "'-'", "'&'", "'='", null, - "'<='", "'>='", "'<'", "'>'", "'('", "')'", "','", "'.'", "';'" + null, null, null, "'*'", "'/'", "'\\'", "'^'", "'+'", "'-'", "'&'", "'='", + null, "'<='", "'>='", "'<'", "'>'", "'('", "')'", "','", "'.'", "';'" }; private static readonly string[] _SymbolicNames = { null, "SELECT", "FROM", "WHERE", "TOP", "AS", "AND", "OR", "NOT", "BAND", - "BOR", "BXOR", "BNOT", "LIKE", "MOD", "INNER", "LEFT", "RIGHT", "OUTER", - "JOIN", "IN", "ON", "ORDER", "GROUP", "IS", "BY", "HAVING", "EXISTS", + "BOR", "BXOR", "BNOT", "LIKE", "MOD", "INNER", "LEFT", "RIGHT", "FULL", + "OUTER", "JOIN", "IN", "ON", "ORDER", "GROUP", "IS", "BY", "HAVING", "EXISTS", "IF", "THEN", "DISTINCTROW", "DISTINCT", "PERCENT", "BETWEEN", "UNION", "ALL", "INTERSECT", "EXCEPT", "CREATE", "TABLE", "BEGIN", "COMMIT", "ROLLBACK", "TRANSACTION", "WORK", "ALTER", "RENAME", "TO", "ADD", "DROP", "COLUMN", @@ -145,7 +145,7 @@ static AccessSqlLexer() { } } private static int[] _serializedATN = { - 4,0,117,1009,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, + 4,0,118,1016,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, 7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2, 14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2, 21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2, @@ -162,324 +162,326 @@ static AccessSqlLexer() { 98,7,98,2,99,7,99,2,100,7,100,2,101,7,101,2,102,7,102,2,103,7,103,2,104, 7,104,2,105,7,105,2,106,7,106,2,107,7,107,2,108,7,108,2,109,7,109,2,110, 7,110,2,111,7,111,2,112,7,112,2,113,7,113,2,114,7,114,2,115,7,115,2,116, - 7,116,2,117,7,117,2,118,7,118,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1, - 1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,3,1,3,1,3,1,3,1,4,1,4,1,4,1,5,1,5,1, - 5,1,5,1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,9, - 1,10,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12, - 1,12,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,15,1,15,1,15, - 1,15,1,15,1,16,1,16,1,16,1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,17, - 1,18,1,18,1,18,1,18,1,18,1,19,1,19,1,19,1,20,1,20,1,20,1,21,1,21,1,21, - 1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,24,1,24, - 1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,26,1,26, - 1,26,1,27,1,27,1,27,1,28,1,28,1,28,1,28,1,28,1,29,1,29,1,29,1,29,1,29, - 1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,30,1,30,1,30, - 1,30,1,30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1,32,1,32, - 1,32,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34, - 1,35,1,35,1,35,1,35,1,35,1,35,1,35,1,35,1,35,1,35,1,36,1,36,1,36,1,36, + 7,116,2,117,7,117,2,118,7,118,2,119,7,119,1,0,1,0,1,0,1,0,1,0,1,0,1,0, + 1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,3,1,3,1,3,1,3,1,4,1,4,1, + 4,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,9, + 1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,12,1,12, + 1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,15, + 1,15,1,15,1,15,1,15,1,16,1,16,1,16,1,16,1,16,1,16,1,17,1,17,1,17,1,17, + 1,17,1,18,1,18,1,18,1,18,1,18,1,18,1,19,1,19,1,19,1,19,1,19,1,20,1,20, + 1,20,1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23, + 1,23,1,23,1,24,1,24,1,24,1,25,1,25,1,25,1,26,1,26,1,26,1,26,1,26,1,26, + 1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,28,1,28,1,28,1,29,1,29,1,29, + 1,29,1,29,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30, + 1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32, + 1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,34,1,34,1,34, + 1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,36,1,36,1,36,1,36,1,36,1,36,1,36, 1,36,1,36,1,36,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,38,1,38,1,38,1,38, - 1,38,1,38,1,39,1,39,1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40,1,40, - 1,40,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,42,1,42,1,42,1,42, - 1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,43,1,44, - 1,44,1,44,1,44,1,44,1,44,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,46,1,46, - 1,46,1,47,1,47,1,47,1,47,1,48,1,48,1,48,1,48,1,48,1,49,1,49,1,49,1,49, + 1,38,1,38,1,38,1,39,1,39,1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40, + 1,40,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,42,1,42,1,42,1,42,1,42,1,42, + 1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43, + 1,43,1,44,1,44,1,44,1,44,1,44,1,45,1,45,1,45,1,45,1,45,1,45,1,46,1,46, + 1,46,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1,48,1,48,1,48,1,48,1,49,1,49, 1,49,1,49,1,49,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,51,1,51,1,51,1,51, - 1,51,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,54,1,54,1,54,1,54,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55, - 1,55,1,55,1,55,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,57,1,57,1,57, - 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,58,1,58,1,58,1,58,1,58,1,58, + 1,51,1,51,1,51,1,52,1,52,1,52,1,52,1,52,1,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,55,1,55,1,55,1,55,1,56, + 1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,57,1,57,1,57,1,57, + 1,57,1,57,1,57,1,57,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58, 1,58,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60,1,60,1,60,1,60, - 1,60,1,60,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,62,1,62,1,62, - 1,62,1,62,1,62,1,62,1,63,1,63,1,63,1,63,1,64,1,64,1,64,1,64,1,64,1,64, - 1,64,1,64,1,65,1,65,1,65,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,67,1,67, - 1,67,1,67,1,67,1,67,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68, - 1,69,1,69,1,69,1,69,1,69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,70, - 1,70,1,70,1,70,1,71,1,71,1,71,1,71,1,71,1,72,1,72,1,72,1,72,1,72,1,72, - 1,72,1,72,1,72,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,74,1,74,1,74,1,74, - 1,74,1,74,1,75,1,75,1,75,1,75,1,75,1,76,1,76,1,76,1,76,1,76,1,76,1,76, - 1,76,1,76,1,76,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77, - 1,78,1,78,1,78,1,78,1,78,1,78,1,78,1,78,1,79,1,79,1,79,1,79,1,79,1,80, - 1,80,1,80,1,80,1,81,1,81,1,81,1,81,1,81,1,82,1,82,1,82,1,82,1,82,1,83, - 1,83,1,83,1,83,1,83,1,83,1,84,1,84,1,84,1,84,1,84,1,85,1,85,1,86,1,86, - 1,87,1,87,1,88,1,88,1,89,1,89,1,90,1,90,1,91,1,91,1,92,1,92,1,93,1,93, - 1,93,1,93,3,93,791,8,93,1,94,1,94,1,94,1,95,1,95,1,95,1,96,1,96,1,97,1, - 97,1,98,1,98,1,99,1,99,1,100,1,100,1,101,1,101,1,102,1,102,1,103,1,103, - 1,103,1,103,1,103,5,103,818,8,103,10,103,12,103,821,9,103,1,104,1,104, - 1,104,1,104,5,104,827,8,104,10,104,12,104,830,9,104,3,104,832,8,104,1, - 105,1,105,1,105,4,105,837,8,105,11,105,12,105,838,1,106,4,106,842,8,106, - 11,106,12,106,843,1,107,4,107,847,8,107,11,107,12,107,848,1,107,1,107, - 5,107,853,8,107,10,107,12,107,856,9,107,1,107,3,107,859,8,107,1,107,1, - 107,4,107,863,8,107,11,107,12,107,864,1,107,3,107,868,8,107,1,107,4,107, - 871,8,107,11,107,12,107,872,1,107,3,107,876,8,107,1,108,1,108,3,108,880, - 8,108,1,108,4,108,883,8,108,11,108,12,108,884,1,109,1,109,1,109,1,109, - 5,109,891,8,109,10,109,12,109,894,9,109,1,109,1,109,1,109,1,109,1,109, - 5,109,901,8,109,10,109,12,109,904,9,109,1,109,3,109,907,8,109,1,110,1, - 110,5,110,911,8,110,10,110,12,110,914,9,110,1,110,1,110,1,111,1,111,4, - 111,920,8,111,11,111,12,111,921,1,111,1,111,4,111,926,8,111,11,111,12, - 111,927,1,111,1,111,4,111,932,8,111,11,111,12,111,933,1,111,1,111,4,111, - 938,8,111,11,111,12,111,939,1,111,1,111,4,111,944,8,111,11,111,12,111, - 945,1,111,1,111,1,112,1,112,1,113,1,113,4,113,954,8,113,11,113,12,113, - 955,1,113,1,113,1,114,1,114,4,114,962,8,114,11,114,12,114,963,1,114,1, - 114,1,115,1,115,5,115,970,8,115,10,115,12,115,973,9,115,1,115,3,115,976, - 8,115,1,116,4,116,979,8,116,11,116,12,116,980,1,116,1,116,1,117,1,117, - 1,117,1,117,5,117,989,8,117,10,117,12,117,992,9,117,1,117,1,117,1,118, - 1,118,1,118,1,118,5,118,1000,8,118,10,118,12,118,1003,9,118,1,118,1,118, - 1,118,1,118,1,118,1,1001,0,119,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9, - 19,10,21,11,23,12,25,13,27,14,29,15,31,16,33,17,35,18,37,19,39,20,41,21, - 43,22,45,23,47,24,49,25,51,26,53,27,55,28,57,29,59,30,61,31,63,32,65,33, - 67,34,69,35,71,36,73,37,75,38,77,39,79,40,81,41,83,42,85,43,87,44,89,45, - 91,46,93,47,95,48,97,49,99,50,101,51,103,52,105,53,107,54,109,55,111,56, - 113,57,115,58,117,59,119,60,121,61,123,62,125,63,127,64,129,65,131,66, - 133,67,135,68,137,69,139,70,141,71,143,72,145,73,147,74,149,75,151,76, - 153,77,155,78,157,79,159,80,161,81,163,82,165,83,167,84,169,85,171,86, - 173,87,175,88,177,89,179,90,181,91,183,92,185,93,187,94,189,95,191,96, - 193,97,195,98,197,99,199,100,201,101,203,102,205,103,207,104,209,105,211, - 106,213,107,215,108,217,0,219,109,221,110,223,111,225,0,227,112,229,113, - 231,114,233,115,235,116,237,117,1,0,37,2,0,83,83,115,115,2,0,69,69,101, - 101,2,0,76,76,108,108,2,0,67,67,99,99,2,0,84,84,116,116,2,0,70,70,102, - 102,2,0,82,82,114,114,2,0,79,79,111,111,2,0,77,77,109,109,2,0,87,87,119, - 119,2,0,72,72,104,104,2,0,80,80,112,112,2,0,65,65,97,97,2,0,78,78,110, - 110,2,0,68,68,100,100,2,0,66,66,98,98,2,0,88,88,120,120,2,0,73,73,105, - 105,2,0,75,75,107,107,2,0,71,71,103,103,2,0,85,85,117,117,2,0,74,74,106, - 106,2,0,89,89,121,121,2,0,86,86,118,118,2,0,81,81,113,113,3,0,65,90,95, - 95,97,122,4,0,48,57,65,90,95,95,97,122,3,0,48,57,65,70,97,102,1,0,48,57, - 2,0,43,43,45,45,1,0,34,34,1,0,39,39,1,0,35,35,1,0,93,93,1,0,96,96,3,0, - 9,10,13,13,32,32,2,0,10,10,13,13,1040,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0, - 0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17, - 1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0, - 0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39, - 1,0,0,0,0,41,1,0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0, - 0,0,51,1,0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0,0,59,1,0,0,0,0,61, - 1,0,0,0,0,63,1,0,0,0,0,65,1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,71,1,0,0, - 0,0,73,1,0,0,0,0,75,1,0,0,0,0,77,1,0,0,0,0,79,1,0,0,0,0,81,1,0,0,0,0,83, - 1,0,0,0,0,85,1,0,0,0,0,87,1,0,0,0,0,89,1,0,0,0,0,91,1,0,0,0,0,93,1,0,0, - 0,0,95,1,0,0,0,0,97,1,0,0,0,0,99,1,0,0,0,0,101,1,0,0,0,0,103,1,0,0,0,0, - 105,1,0,0,0,0,107,1,0,0,0,0,109,1,0,0,0,0,111,1,0,0,0,0,113,1,0,0,0,0, - 115,1,0,0,0,0,117,1,0,0,0,0,119,1,0,0,0,0,121,1,0,0,0,0,123,1,0,0,0,0, - 125,1,0,0,0,0,127,1,0,0,0,0,129,1,0,0,0,0,131,1,0,0,0,0,133,1,0,0,0,0, - 135,1,0,0,0,0,137,1,0,0,0,0,139,1,0,0,0,0,141,1,0,0,0,0,143,1,0,0,0,0, - 145,1,0,0,0,0,147,1,0,0,0,0,149,1,0,0,0,0,151,1,0,0,0,0,153,1,0,0,0,0, - 155,1,0,0,0,0,157,1,0,0,0,0,159,1,0,0,0,0,161,1,0,0,0,0,163,1,0,0,0,0, - 165,1,0,0,0,0,167,1,0,0,0,0,169,1,0,0,0,0,171,1,0,0,0,0,173,1,0,0,0,0, - 175,1,0,0,0,0,177,1,0,0,0,0,179,1,0,0,0,0,181,1,0,0,0,0,183,1,0,0,0,0, - 185,1,0,0,0,0,187,1,0,0,0,0,189,1,0,0,0,0,191,1,0,0,0,0,193,1,0,0,0,0, - 195,1,0,0,0,0,197,1,0,0,0,0,199,1,0,0,0,0,201,1,0,0,0,0,203,1,0,0,0,0, - 205,1,0,0,0,0,207,1,0,0,0,0,209,1,0,0,0,0,211,1,0,0,0,0,213,1,0,0,0,0, - 215,1,0,0,0,0,219,1,0,0,0,0,221,1,0,0,0,0,223,1,0,0,0,0,227,1,0,0,0,0, - 229,1,0,0,0,0,231,1,0,0,0,0,233,1,0,0,0,0,235,1,0,0,0,0,237,1,0,0,0,1, - 239,1,0,0,0,3,246,1,0,0,0,5,251,1,0,0,0,7,257,1,0,0,0,9,261,1,0,0,0,11, - 264,1,0,0,0,13,268,1,0,0,0,15,271,1,0,0,0,17,275,1,0,0,0,19,280,1,0,0, - 0,21,284,1,0,0,0,23,289,1,0,0,0,25,294,1,0,0,0,27,299,1,0,0,0,29,303,1, - 0,0,0,31,309,1,0,0,0,33,314,1,0,0,0,35,320,1,0,0,0,37,326,1,0,0,0,39,331, - 1,0,0,0,41,334,1,0,0,0,43,337,1,0,0,0,45,343,1,0,0,0,47,349,1,0,0,0,49, - 352,1,0,0,0,51,355,1,0,0,0,53,362,1,0,0,0,55,369,1,0,0,0,57,372,1,0,0, - 0,59,377,1,0,0,0,61,389,1,0,0,0,63,398,1,0,0,0,65,406,1,0,0,0,67,414,1, - 0,0,0,69,420,1,0,0,0,71,424,1,0,0,0,73,434,1,0,0,0,75,441,1,0,0,0,77,448, - 1,0,0,0,79,454,1,0,0,0,81,460,1,0,0,0,83,467,1,0,0,0,85,476,1,0,0,0,87, - 488,1,0,0,0,89,493,1,0,0,0,91,499,1,0,0,0,93,506,1,0,0,0,95,509,1,0,0, - 0,97,513,1,0,0,0,99,518,1,0,0,0,101,525,1,0,0,0,103,532,1,0,0,0,105,537, - 1,0,0,0,107,544,1,0,0,0,109,552,1,0,0,0,111,556,1,0,0,0,113,567,1,0,0, - 0,115,575,1,0,0,0,117,586,1,0,0,0,119,593,1,0,0,0,121,600,1,0,0,0,123, - 608,1,0,0,0,125,617,1,0,0,0,127,624,1,0,0,0,129,628,1,0,0,0,131,636,1, - 0,0,0,133,639,1,0,0,0,135,646,1,0,0,0,137,652,1,0,0,0,139,662,1,0,0,0, - 141,667,1,0,0,0,143,679,1,0,0,0,145,684,1,0,0,0,147,693,1,0,0,0,149,700, - 1,0,0,0,151,706,1,0,0,0,153,711,1,0,0,0,155,721,1,0,0,0,157,732,1,0,0, - 0,159,740,1,0,0,0,161,745,1,0,0,0,163,749,1,0,0,0,165,754,1,0,0,0,167, - 759,1,0,0,0,169,765,1,0,0,0,171,770,1,0,0,0,173,772,1,0,0,0,175,774,1, - 0,0,0,177,776,1,0,0,0,179,778,1,0,0,0,181,780,1,0,0,0,183,782,1,0,0,0, - 185,784,1,0,0,0,187,790,1,0,0,0,189,792,1,0,0,0,191,795,1,0,0,0,193,798, - 1,0,0,0,195,800,1,0,0,0,197,802,1,0,0,0,199,804,1,0,0,0,201,806,1,0,0, - 0,203,808,1,0,0,0,205,810,1,0,0,0,207,812,1,0,0,0,209,831,1,0,0,0,211, - 833,1,0,0,0,213,841,1,0,0,0,215,875,1,0,0,0,217,877,1,0,0,0,219,906,1, - 0,0,0,221,908,1,0,0,0,223,917,1,0,0,0,225,949,1,0,0,0,227,951,1,0,0,0, - 229,959,1,0,0,0,231,967,1,0,0,0,233,978,1,0,0,0,235,984,1,0,0,0,237,995, - 1,0,0,0,239,240,7,0,0,0,240,241,7,1,0,0,241,242,7,2,0,0,242,243,7,1,0, - 0,243,244,7,3,0,0,244,245,7,4,0,0,245,2,1,0,0,0,246,247,7,5,0,0,247,248, - 7,6,0,0,248,249,7,7,0,0,249,250,7,8,0,0,250,4,1,0,0,0,251,252,7,9,0,0, - 252,253,7,10,0,0,253,254,7,1,0,0,254,255,7,6,0,0,255,256,7,1,0,0,256,6, - 1,0,0,0,257,258,7,4,0,0,258,259,7,7,0,0,259,260,7,11,0,0,260,8,1,0,0,0, - 261,262,7,12,0,0,262,263,7,0,0,0,263,10,1,0,0,0,264,265,7,12,0,0,265,266, - 7,13,0,0,266,267,7,14,0,0,267,12,1,0,0,0,268,269,7,7,0,0,269,270,7,6,0, - 0,270,14,1,0,0,0,271,272,7,13,0,0,272,273,7,7,0,0,273,274,7,4,0,0,274, - 16,1,0,0,0,275,276,7,15,0,0,276,277,7,12,0,0,277,278,7,13,0,0,278,279, - 7,14,0,0,279,18,1,0,0,0,280,281,7,15,0,0,281,282,7,7,0,0,282,283,7,6,0, - 0,283,20,1,0,0,0,284,285,7,15,0,0,285,286,7,16,0,0,286,287,7,7,0,0,287, - 288,7,6,0,0,288,22,1,0,0,0,289,290,7,15,0,0,290,291,7,13,0,0,291,292,7, - 7,0,0,292,293,7,4,0,0,293,24,1,0,0,0,294,295,7,2,0,0,295,296,7,17,0,0, - 296,297,7,18,0,0,297,298,7,1,0,0,298,26,1,0,0,0,299,300,7,8,0,0,300,301, - 7,7,0,0,301,302,7,14,0,0,302,28,1,0,0,0,303,304,7,17,0,0,304,305,7,13, - 0,0,305,306,7,13,0,0,306,307,7,1,0,0,307,308,7,6,0,0,308,30,1,0,0,0,309, - 310,7,2,0,0,310,311,7,1,0,0,311,312,7,5,0,0,312,313,7,4,0,0,313,32,1,0, - 0,0,314,315,7,6,0,0,315,316,7,17,0,0,316,317,7,19,0,0,317,318,7,10,0,0, - 318,319,7,4,0,0,319,34,1,0,0,0,320,321,7,7,0,0,321,322,7,20,0,0,322,323, - 7,4,0,0,323,324,7,1,0,0,324,325,7,6,0,0,325,36,1,0,0,0,326,327,7,21,0, - 0,327,328,7,7,0,0,328,329,7,17,0,0,329,330,7,13,0,0,330,38,1,0,0,0,331, - 332,7,17,0,0,332,333,7,13,0,0,333,40,1,0,0,0,334,335,7,7,0,0,335,336,7, - 13,0,0,336,42,1,0,0,0,337,338,7,7,0,0,338,339,7,6,0,0,339,340,7,14,0,0, - 340,341,7,1,0,0,341,342,7,6,0,0,342,44,1,0,0,0,343,344,7,19,0,0,344,345, - 7,6,0,0,345,346,7,7,0,0,346,347,7,20,0,0,347,348,7,11,0,0,348,46,1,0,0, - 0,349,350,7,17,0,0,350,351,7,0,0,0,351,48,1,0,0,0,352,353,7,15,0,0,353, - 354,7,22,0,0,354,50,1,0,0,0,355,356,7,10,0,0,356,357,7,12,0,0,357,358, - 7,23,0,0,358,359,7,17,0,0,359,360,7,13,0,0,360,361,7,19,0,0,361,52,1,0, - 0,0,362,363,7,1,0,0,363,364,7,16,0,0,364,365,7,17,0,0,365,366,7,0,0,0, - 366,367,7,4,0,0,367,368,7,0,0,0,368,54,1,0,0,0,369,370,7,17,0,0,370,371, - 7,5,0,0,371,56,1,0,0,0,372,373,7,4,0,0,373,374,7,10,0,0,374,375,7,1,0, - 0,375,376,7,13,0,0,376,58,1,0,0,0,377,378,7,14,0,0,378,379,7,17,0,0,379, - 380,7,0,0,0,380,381,7,4,0,0,381,382,7,17,0,0,382,383,7,13,0,0,383,384, - 7,3,0,0,384,385,7,4,0,0,385,386,7,6,0,0,386,387,7,7,0,0,387,388,7,9,0, - 0,388,60,1,0,0,0,389,390,7,14,0,0,390,391,7,17,0,0,391,392,7,0,0,0,392, - 393,7,4,0,0,393,394,7,17,0,0,394,395,7,13,0,0,395,396,7,3,0,0,396,397, - 7,4,0,0,397,62,1,0,0,0,398,399,7,11,0,0,399,400,7,1,0,0,400,401,7,6,0, - 0,401,402,7,3,0,0,402,403,7,1,0,0,403,404,7,13,0,0,404,405,7,4,0,0,405, - 64,1,0,0,0,406,407,7,15,0,0,407,408,7,1,0,0,408,409,7,4,0,0,409,410,7, - 9,0,0,410,411,7,1,0,0,411,412,7,1,0,0,412,413,7,13,0,0,413,66,1,0,0,0, - 414,415,7,20,0,0,415,416,7,13,0,0,416,417,7,17,0,0,417,418,7,7,0,0,418, - 419,7,13,0,0,419,68,1,0,0,0,420,421,7,12,0,0,421,422,7,2,0,0,422,423,7, - 2,0,0,423,70,1,0,0,0,424,425,7,17,0,0,425,426,7,13,0,0,426,427,7,4,0,0, - 427,428,7,1,0,0,428,429,7,6,0,0,429,430,7,0,0,0,430,431,7,1,0,0,431,432, - 7,3,0,0,432,433,7,4,0,0,433,72,1,0,0,0,434,435,7,1,0,0,435,436,7,16,0, - 0,436,437,7,3,0,0,437,438,7,1,0,0,438,439,7,11,0,0,439,440,7,4,0,0,440, - 74,1,0,0,0,441,442,7,3,0,0,442,443,7,6,0,0,443,444,7,1,0,0,444,445,7,12, - 0,0,445,446,7,4,0,0,446,447,7,1,0,0,447,76,1,0,0,0,448,449,7,4,0,0,449, - 450,7,12,0,0,450,451,7,15,0,0,451,452,7,2,0,0,452,453,7,1,0,0,453,78,1, - 0,0,0,454,455,7,15,0,0,455,456,7,1,0,0,456,457,7,19,0,0,457,458,7,17,0, - 0,458,459,7,13,0,0,459,80,1,0,0,0,460,461,7,3,0,0,461,462,7,7,0,0,462, - 463,7,8,0,0,463,464,7,8,0,0,464,465,7,17,0,0,465,466,7,4,0,0,466,82,1, - 0,0,0,467,468,7,6,0,0,468,469,7,7,0,0,469,470,7,2,0,0,470,471,7,2,0,0, - 471,472,7,15,0,0,472,473,7,12,0,0,473,474,7,3,0,0,474,475,7,18,0,0,475, - 84,1,0,0,0,476,477,7,4,0,0,477,478,7,6,0,0,478,479,7,12,0,0,479,480,7, - 13,0,0,480,481,7,0,0,0,481,482,7,12,0,0,482,483,7,3,0,0,483,484,7,4,0, - 0,484,485,7,17,0,0,485,486,7,7,0,0,486,487,7,13,0,0,487,86,1,0,0,0,488, - 489,7,9,0,0,489,490,7,7,0,0,490,491,7,6,0,0,491,492,7,18,0,0,492,88,1, - 0,0,0,493,494,7,12,0,0,494,495,7,2,0,0,495,496,7,4,0,0,496,497,7,1,0,0, - 497,498,7,6,0,0,498,90,1,0,0,0,499,500,7,6,0,0,500,501,7,1,0,0,501,502, - 7,13,0,0,502,503,7,12,0,0,503,504,7,8,0,0,504,505,7,1,0,0,505,92,1,0,0, - 0,506,507,7,4,0,0,507,508,7,7,0,0,508,94,1,0,0,0,509,510,7,12,0,0,510, - 511,7,14,0,0,511,512,7,14,0,0,512,96,1,0,0,0,513,514,7,14,0,0,514,515, - 7,6,0,0,515,516,7,7,0,0,516,517,7,11,0,0,517,98,1,0,0,0,518,519,7,3,0, - 0,519,520,7,7,0,0,520,521,7,2,0,0,521,522,7,20,0,0,522,523,7,8,0,0,523, - 524,7,13,0,0,524,100,1,0,0,0,525,526,7,17,0,0,526,527,7,13,0,0,527,528, - 7,0,0,0,528,529,7,1,0,0,529,530,7,6,0,0,530,531,7,4,0,0,531,102,1,0,0, - 0,532,533,7,17,0,0,533,534,7,13,0,0,534,535,7,4,0,0,535,536,7,7,0,0,536, - 104,1,0,0,0,537,538,7,23,0,0,538,539,7,12,0,0,539,540,7,2,0,0,540,541, - 7,20,0,0,541,542,7,1,0,0,542,543,7,0,0,0,543,106,1,0,0,0,544,545,7,11, - 0,0,545,546,7,6,0,0,546,547,7,17,0,0,547,548,7,8,0,0,548,549,7,12,0,0, - 549,550,7,6,0,0,550,551,7,22,0,0,551,108,1,0,0,0,552,553,7,18,0,0,553, - 554,7,1,0,0,554,555,7,22,0,0,555,110,1,0,0,0,556,557,7,3,0,0,557,558,7, - 7,0,0,558,559,7,13,0,0,559,560,7,0,0,0,560,561,7,4,0,0,561,562,7,6,0,0, - 562,563,7,12,0,0,563,564,7,17,0,0,564,565,7,13,0,0,565,566,7,4,0,0,566, - 112,1,0,0,0,567,568,7,5,0,0,568,569,7,7,0,0,569,570,7,6,0,0,570,571,7, - 1,0,0,571,572,7,17,0,0,572,573,7,19,0,0,573,574,7,13,0,0,574,114,1,0,0, - 0,575,576,7,6,0,0,576,577,7,1,0,0,577,578,7,5,0,0,578,579,7,1,0,0,579, - 580,7,6,0,0,580,581,7,1,0,0,581,582,7,13,0,0,582,583,7,3,0,0,583,584,7, - 1,0,0,584,585,7,0,0,0,585,116,1,0,0,0,586,587,7,14,0,0,587,588,7,1,0,0, - 588,589,7,2,0,0,589,590,7,1,0,0,590,591,7,4,0,0,591,592,7,1,0,0,592,118, - 1,0,0,0,593,594,7,20,0,0,594,595,7,11,0,0,595,596,7,14,0,0,596,597,7,12, - 0,0,597,598,7,4,0,0,598,599,7,1,0,0,599,120,1,0,0,0,600,601,7,3,0,0,601, - 602,7,12,0,0,602,603,7,0,0,0,603,604,7,3,0,0,604,605,7,12,0,0,605,606, - 7,14,0,0,606,607,7,1,0,0,607,122,1,0,0,0,608,609,7,6,0,0,609,610,7,1,0, - 0,610,611,7,0,0,0,611,612,7,4,0,0,612,613,7,6,0,0,613,614,7,17,0,0,614, - 615,7,3,0,0,615,616,7,4,0,0,616,124,1,0,0,0,617,618,7,12,0,0,618,619,7, - 3,0,0,619,620,7,4,0,0,620,621,7,17,0,0,621,622,7,7,0,0,622,623,7,13,0, - 0,623,126,1,0,0,0,624,625,7,0,0,0,625,626,7,1,0,0,626,627,7,4,0,0,627, - 128,1,0,0,0,628,629,7,14,0,0,629,630,7,1,0,0,630,631,7,5,0,0,631,632,7, - 12,0,0,632,633,7,20,0,0,633,634,7,2,0,0,634,635,7,4,0,0,635,130,1,0,0, - 0,636,637,7,13,0,0,637,638,7,7,0,0,638,132,1,0,0,0,639,640,7,20,0,0,640, - 641,7,13,0,0,641,642,7,17,0,0,642,643,7,24,0,0,643,644,7,20,0,0,644,645, - 7,1,0,0,645,134,1,0,0,0,646,647,7,17,0,0,647,648,7,13,0,0,648,649,7,14, - 0,0,649,650,7,1,0,0,650,651,7,16,0,0,651,136,1,0,0,0,652,653,7,4,0,0,653, - 654,7,1,0,0,654,655,7,8,0,0,655,656,7,11,0,0,656,657,7,7,0,0,657,658,7, - 6,0,0,658,659,7,12,0,0,659,660,7,6,0,0,660,661,7,22,0,0,661,138,1,0,0, - 0,662,663,7,9,0,0,663,664,7,17,0,0,664,665,7,4,0,0,665,666,7,10,0,0,666, - 140,1,0,0,0,667,668,7,3,0,0,668,669,7,7,0,0,669,670,7,8,0,0,670,671,7, - 11,0,0,671,672,7,6,0,0,672,673,7,1,0,0,673,674,7,0,0,0,674,675,7,0,0,0, - 675,676,7,17,0,0,676,677,7,7,0,0,677,678,7,13,0,0,678,142,1,0,0,0,679, - 680,7,3,0,0,680,681,7,7,0,0,681,682,7,8,0,0,682,683,7,11,0,0,683,144,1, - 0,0,0,684,685,7,14,0,0,685,686,7,17,0,0,686,687,7,0,0,0,687,688,7,12,0, - 0,688,689,7,2,0,0,689,690,7,2,0,0,690,691,7,7,0,0,691,692,7,9,0,0,692, - 146,1,0,0,0,693,694,7,17,0,0,694,695,7,19,0,0,695,696,7,13,0,0,696,697, - 7,7,0,0,697,698,7,6,0,0,698,699,7,1,0,0,699,148,1,0,0,0,700,701,7,3,0, - 0,701,702,7,10,0,0,702,703,7,1,0,0,703,704,7,3,0,0,704,705,7,18,0,0,705, - 150,1,0,0,0,706,707,7,23,0,0,707,708,7,17,0,0,708,709,7,1,0,0,709,710, - 7,9,0,0,710,152,1,0,0,0,711,712,7,11,0,0,712,713,7,6,0,0,713,714,7,7,0, - 0,714,715,7,3,0,0,715,716,7,1,0,0,716,717,7,14,0,0,717,718,7,20,0,0,718, - 719,7,6,0,0,719,720,7,1,0,0,720,154,1,0,0,0,721,722,7,11,0,0,722,723,7, - 12,0,0,723,724,7,6,0,0,724,725,7,12,0,0,725,726,7,8,0,0,726,727,7,1,0, - 0,727,728,7,4,0,0,728,729,7,1,0,0,729,730,7,6,0,0,730,731,7,0,0,0,731, - 156,1,0,0,0,732,733,7,1,0,0,733,734,7,16,0,0,734,735,7,1,0,0,735,736,7, - 3,0,0,736,737,7,20,0,0,737,738,7,4,0,0,738,739,7,1,0,0,739,158,1,0,0,0, - 740,741,7,1,0,0,741,742,7,16,0,0,742,743,7,1,0,0,743,744,7,3,0,0,744,160, - 1,0,0,0,745,746,7,12,0,0,746,747,7,0,0,0,747,748,7,3,0,0,748,162,1,0,0, - 0,749,750,7,14,0,0,750,751,7,1,0,0,751,752,7,0,0,0,752,753,7,3,0,0,753, - 164,1,0,0,0,754,755,7,4,0,0,755,756,7,6,0,0,756,757,7,20,0,0,757,758,7, - 1,0,0,758,166,1,0,0,0,759,760,7,5,0,0,760,761,7,12,0,0,761,762,7,2,0,0, - 762,763,7,0,0,0,763,764,7,1,0,0,764,168,1,0,0,0,765,766,7,13,0,0,766,767, - 7,20,0,0,767,768,7,2,0,0,768,769,7,2,0,0,769,170,1,0,0,0,770,771,5,42, - 0,0,771,172,1,0,0,0,772,773,5,47,0,0,773,174,1,0,0,0,774,775,5,92,0,0, - 775,176,1,0,0,0,776,777,5,94,0,0,777,178,1,0,0,0,778,779,5,43,0,0,779, - 180,1,0,0,0,780,781,5,45,0,0,781,182,1,0,0,0,782,783,5,38,0,0,783,184, - 1,0,0,0,784,785,5,61,0,0,785,186,1,0,0,0,786,787,5,60,0,0,787,791,5,62, - 0,0,788,789,5,33,0,0,789,791,5,61,0,0,790,786,1,0,0,0,790,788,1,0,0,0, - 791,188,1,0,0,0,792,793,5,60,0,0,793,794,5,61,0,0,794,190,1,0,0,0,795, - 796,5,62,0,0,796,797,5,61,0,0,797,192,1,0,0,0,798,799,5,60,0,0,799,194, - 1,0,0,0,800,801,5,62,0,0,801,196,1,0,0,0,802,803,5,40,0,0,803,198,1,0, - 0,0,804,805,5,41,0,0,805,200,1,0,0,0,806,807,5,44,0,0,807,202,1,0,0,0, - 808,809,5,46,0,0,809,204,1,0,0,0,810,811,5,59,0,0,811,206,1,0,0,0,812, - 813,5,64,0,0,813,814,5,64,0,0,814,815,1,0,0,0,815,819,7,25,0,0,816,818, - 7,26,0,0,817,816,1,0,0,0,818,821,1,0,0,0,819,817,1,0,0,0,819,820,1,0,0, - 0,820,208,1,0,0,0,821,819,1,0,0,0,822,832,5,63,0,0,823,824,5,64,0,0,824, - 828,7,25,0,0,825,827,7,26,0,0,826,825,1,0,0,0,827,830,1,0,0,0,828,826, - 1,0,0,0,828,829,1,0,0,0,829,832,1,0,0,0,830,828,1,0,0,0,831,822,1,0,0, - 0,831,823,1,0,0,0,832,210,1,0,0,0,833,834,5,48,0,0,834,836,7,16,0,0,835, - 837,7,27,0,0,836,835,1,0,0,0,837,838,1,0,0,0,838,836,1,0,0,0,838,839,1, - 0,0,0,839,212,1,0,0,0,840,842,7,28,0,0,841,840,1,0,0,0,842,843,1,0,0,0, - 843,841,1,0,0,0,843,844,1,0,0,0,844,214,1,0,0,0,845,847,7,28,0,0,846,845, - 1,0,0,0,847,848,1,0,0,0,848,846,1,0,0,0,848,849,1,0,0,0,849,850,1,0,0, - 0,850,854,5,46,0,0,851,853,7,28,0,0,852,851,1,0,0,0,853,856,1,0,0,0,854, - 852,1,0,0,0,854,855,1,0,0,0,855,858,1,0,0,0,856,854,1,0,0,0,857,859,3, - 217,108,0,858,857,1,0,0,0,858,859,1,0,0,0,859,876,1,0,0,0,860,862,5,46, - 0,0,861,863,7,28,0,0,862,861,1,0,0,0,863,864,1,0,0,0,864,862,1,0,0,0,864, - 865,1,0,0,0,865,867,1,0,0,0,866,868,3,217,108,0,867,866,1,0,0,0,867,868, - 1,0,0,0,868,876,1,0,0,0,869,871,7,28,0,0,870,869,1,0,0,0,871,872,1,0,0, - 0,872,870,1,0,0,0,872,873,1,0,0,0,873,874,1,0,0,0,874,876,3,217,108,0, - 875,846,1,0,0,0,875,860,1,0,0,0,875,870,1,0,0,0,876,216,1,0,0,0,877,879, - 7,1,0,0,878,880,7,29,0,0,879,878,1,0,0,0,879,880,1,0,0,0,880,882,1,0,0, - 0,881,883,7,28,0,0,882,881,1,0,0,0,883,884,1,0,0,0,884,882,1,0,0,0,884, - 885,1,0,0,0,885,218,1,0,0,0,886,892,5,34,0,0,887,891,8,30,0,0,888,889, - 5,34,0,0,889,891,5,34,0,0,890,887,1,0,0,0,890,888,1,0,0,0,891,894,1,0, - 0,0,892,890,1,0,0,0,892,893,1,0,0,0,893,895,1,0,0,0,894,892,1,0,0,0,895, - 907,5,34,0,0,896,902,5,39,0,0,897,901,8,31,0,0,898,899,5,39,0,0,899,901, - 5,39,0,0,900,897,1,0,0,0,900,898,1,0,0,0,901,904,1,0,0,0,902,900,1,0,0, - 0,902,903,1,0,0,0,903,905,1,0,0,0,904,902,1,0,0,0,905,907,5,39,0,0,906, - 886,1,0,0,0,906,896,1,0,0,0,907,220,1,0,0,0,908,912,5,35,0,0,909,911,8, - 32,0,0,910,909,1,0,0,0,911,914,1,0,0,0,912,910,1,0,0,0,912,913,1,0,0,0, - 913,915,1,0,0,0,914,912,1,0,0,0,915,916,5,35,0,0,916,222,1,0,0,0,917,919, - 5,123,0,0,918,920,3,225,112,0,919,918,1,0,0,0,920,921,1,0,0,0,921,919, - 1,0,0,0,921,922,1,0,0,0,922,923,1,0,0,0,923,925,5,45,0,0,924,926,3,225, - 112,0,925,924,1,0,0,0,926,927,1,0,0,0,927,925,1,0,0,0,927,928,1,0,0,0, - 928,929,1,0,0,0,929,931,5,45,0,0,930,932,3,225,112,0,931,930,1,0,0,0,932, - 933,1,0,0,0,933,931,1,0,0,0,933,934,1,0,0,0,934,935,1,0,0,0,935,937,5, - 45,0,0,936,938,3,225,112,0,937,936,1,0,0,0,938,939,1,0,0,0,939,937,1,0, - 0,0,939,940,1,0,0,0,940,941,1,0,0,0,941,943,5,45,0,0,942,944,3,225,112, - 0,943,942,1,0,0,0,944,945,1,0,0,0,945,943,1,0,0,0,945,946,1,0,0,0,946, - 947,1,0,0,0,947,948,5,125,0,0,948,224,1,0,0,0,949,950,7,27,0,0,950,226, - 1,0,0,0,951,953,5,91,0,0,952,954,8,33,0,0,953,952,1,0,0,0,954,955,1,0, - 0,0,955,953,1,0,0,0,955,956,1,0,0,0,956,957,1,0,0,0,957,958,5,93,0,0,958, - 228,1,0,0,0,959,961,5,96,0,0,960,962,8,34,0,0,961,960,1,0,0,0,962,963, - 1,0,0,0,963,961,1,0,0,0,963,964,1,0,0,0,964,965,1,0,0,0,965,966,5,96,0, - 0,966,230,1,0,0,0,967,971,7,25,0,0,968,970,7,26,0,0,969,968,1,0,0,0,970, - 973,1,0,0,0,971,969,1,0,0,0,971,972,1,0,0,0,972,975,1,0,0,0,973,971,1, - 0,0,0,974,976,5,36,0,0,975,974,1,0,0,0,975,976,1,0,0,0,976,232,1,0,0,0, - 977,979,7,35,0,0,978,977,1,0,0,0,979,980,1,0,0,0,980,978,1,0,0,0,980,981, - 1,0,0,0,981,982,1,0,0,0,982,983,6,116,0,0,983,234,1,0,0,0,984,985,5,45, - 0,0,985,986,5,45,0,0,986,990,1,0,0,0,987,989,8,36,0,0,988,987,1,0,0,0, - 989,992,1,0,0,0,990,988,1,0,0,0,990,991,1,0,0,0,991,993,1,0,0,0,992,990, - 1,0,0,0,993,994,6,117,0,0,994,236,1,0,0,0,995,996,5,47,0,0,996,997,5,42, - 0,0,997,1001,1,0,0,0,998,1000,9,0,0,0,999,998,1,0,0,0,1000,1003,1,0,0, - 0,1001,1002,1,0,0,0,1001,999,1,0,0,0,1002,1004,1,0,0,0,1003,1001,1,0,0, - 0,1004,1005,5,42,0,0,1005,1006,5,47,0,0,1006,1007,1,0,0,0,1007,1008,6, - 118,0,0,1008,238,1,0,0,0,34,0,790,819,828,831,838,843,848,854,858,864, - 867,872,875,879,884,890,892,900,902,906,912,921,927,933,939,945,955,963, - 971,975,980,990,1001,1,6,0,0 + 1,60,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,62,1,62,1,62,1,62,1,62, + 1,62,1,62,1,62,1,62,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,64,1,64,1,64, + 1,64,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,66,1,66,1,66,1,67,1,67, + 1,67,1,67,1,67,1,67,1,67,1,68,1,68,1,68,1,68,1,68,1,68,1,69,1,69,1,69, + 1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,70,1,70,1,70,1,70,1,70,1,71,1,71, + 1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,72,1,72,1,72,1,72, + 1,72,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,74,1,74,1,74,1,74, + 1,74,1,74,1,74,1,75,1,75,1,75,1,75,1,75,1,75,1,76,1,76,1,76,1,76,1,76, + 1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,78,1,78,1,78,1,78, + 1,78,1,78,1,78,1,78,1,78,1,78,1,78,1,79,1,79,1,79,1,79,1,79,1,79,1,79, + 1,79,1,80,1,80,1,80,1,80,1,80,1,81,1,81,1,81,1,81,1,82,1,82,1,82,1,82, + 1,82,1,83,1,83,1,83,1,83,1,83,1,84,1,84,1,84,1,84,1,84,1,84,1,85,1,85, + 1,85,1,85,1,85,1,86,1,86,1,87,1,87,1,88,1,88,1,89,1,89,1,90,1,90,1,91, + 1,91,1,92,1,92,1,93,1,93,1,94,1,94,1,94,1,94,3,94,798,8,94,1,95,1,95,1, + 95,1,96,1,96,1,96,1,97,1,97,1,98,1,98,1,99,1,99,1,100,1,100,1,101,1,101, + 1,102,1,102,1,103,1,103,1,104,1,104,1,104,1,104,1,104,5,104,825,8,104, + 10,104,12,104,828,9,104,1,105,1,105,1,105,1,105,5,105,834,8,105,10,105, + 12,105,837,9,105,3,105,839,8,105,1,106,1,106,1,106,4,106,844,8,106,11, + 106,12,106,845,1,107,4,107,849,8,107,11,107,12,107,850,1,108,4,108,854, + 8,108,11,108,12,108,855,1,108,1,108,5,108,860,8,108,10,108,12,108,863, + 9,108,1,108,3,108,866,8,108,1,108,1,108,4,108,870,8,108,11,108,12,108, + 871,1,108,3,108,875,8,108,1,108,4,108,878,8,108,11,108,12,108,879,1,108, + 3,108,883,8,108,1,109,1,109,3,109,887,8,109,1,109,4,109,890,8,109,11,109, + 12,109,891,1,110,1,110,1,110,1,110,5,110,898,8,110,10,110,12,110,901,9, + 110,1,110,1,110,1,110,1,110,1,110,5,110,908,8,110,10,110,12,110,911,9, + 110,1,110,3,110,914,8,110,1,111,1,111,5,111,918,8,111,10,111,12,111,921, + 9,111,1,111,1,111,1,112,1,112,4,112,927,8,112,11,112,12,112,928,1,112, + 1,112,4,112,933,8,112,11,112,12,112,934,1,112,1,112,4,112,939,8,112,11, + 112,12,112,940,1,112,1,112,4,112,945,8,112,11,112,12,112,946,1,112,1,112, + 4,112,951,8,112,11,112,12,112,952,1,112,1,112,1,113,1,113,1,114,1,114, + 4,114,961,8,114,11,114,12,114,962,1,114,1,114,1,115,1,115,4,115,969,8, + 115,11,115,12,115,970,1,115,1,115,1,116,1,116,5,116,977,8,116,10,116,12, + 116,980,9,116,1,116,3,116,983,8,116,1,117,4,117,986,8,117,11,117,12,117, + 987,1,117,1,117,1,118,1,118,1,118,1,118,5,118,996,8,118,10,118,12,118, + 999,9,118,1,118,1,118,1,119,1,119,1,119,1,119,5,119,1007,8,119,10,119, + 12,119,1010,9,119,1,119,1,119,1,119,1,119,1,119,1,1008,0,120,1,1,3,2,5, + 3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,16, + 33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26,53,27,55,28, + 57,29,59,30,61,31,63,32,65,33,67,34,69,35,71,36,73,37,75,38,77,39,79,40, + 81,41,83,42,85,43,87,44,89,45,91,46,93,47,95,48,97,49,99,50,101,51,103, + 52,105,53,107,54,109,55,111,56,113,57,115,58,117,59,119,60,121,61,123, + 62,125,63,127,64,129,65,131,66,133,67,135,68,137,69,139,70,141,71,143, + 72,145,73,147,74,149,75,151,76,153,77,155,78,157,79,159,80,161,81,163, + 82,165,83,167,84,169,85,171,86,173,87,175,88,177,89,179,90,181,91,183, + 92,185,93,187,94,189,95,191,96,193,97,195,98,197,99,199,100,201,101,203, + 102,205,103,207,104,209,105,211,106,213,107,215,108,217,109,219,0,221, + 110,223,111,225,112,227,0,229,113,231,114,233,115,235,116,237,117,239, + 118,1,0,37,2,0,83,83,115,115,2,0,69,69,101,101,2,0,76,76,108,108,2,0,67, + 67,99,99,2,0,84,84,116,116,2,0,70,70,102,102,2,0,82,82,114,114,2,0,79, + 79,111,111,2,0,77,77,109,109,2,0,87,87,119,119,2,0,72,72,104,104,2,0,80, + 80,112,112,2,0,65,65,97,97,2,0,78,78,110,110,2,0,68,68,100,100,2,0,66, + 66,98,98,2,0,88,88,120,120,2,0,73,73,105,105,2,0,75,75,107,107,2,0,71, + 71,103,103,2,0,85,85,117,117,2,0,74,74,106,106,2,0,89,89,121,121,2,0,86, + 86,118,118,2,0,81,81,113,113,3,0,65,90,95,95,97,122,4,0,48,57,65,90,95, + 95,97,122,3,0,48,57,65,70,97,102,1,0,48,57,2,0,43,43,45,45,1,0,34,34,1, + 0,39,39,1,0,35,35,1,0,93,93,1,0,96,96,3,0,9,10,13,13,32,32,2,0,10,10,13, + 13,1047,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0, + 11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1, + 0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0, + 0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0,0,0,0,43, + 1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0,0,0,0,53,1,0,0, + 0,0,55,1,0,0,0,0,57,1,0,0,0,0,59,1,0,0,0,0,61,1,0,0,0,0,63,1,0,0,0,0,65, + 1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,71,1,0,0,0,0,73,1,0,0,0,0,75,1,0,0, + 0,0,77,1,0,0,0,0,79,1,0,0,0,0,81,1,0,0,0,0,83,1,0,0,0,0,85,1,0,0,0,0,87, + 1,0,0,0,0,89,1,0,0,0,0,91,1,0,0,0,0,93,1,0,0,0,0,95,1,0,0,0,0,97,1,0,0, + 0,0,99,1,0,0,0,0,101,1,0,0,0,0,103,1,0,0,0,0,105,1,0,0,0,0,107,1,0,0,0, + 0,109,1,0,0,0,0,111,1,0,0,0,0,113,1,0,0,0,0,115,1,0,0,0,0,117,1,0,0,0, + 0,119,1,0,0,0,0,121,1,0,0,0,0,123,1,0,0,0,0,125,1,0,0,0,0,127,1,0,0,0, + 0,129,1,0,0,0,0,131,1,0,0,0,0,133,1,0,0,0,0,135,1,0,0,0,0,137,1,0,0,0, + 0,139,1,0,0,0,0,141,1,0,0,0,0,143,1,0,0,0,0,145,1,0,0,0,0,147,1,0,0,0, + 0,149,1,0,0,0,0,151,1,0,0,0,0,153,1,0,0,0,0,155,1,0,0,0,0,157,1,0,0,0, + 0,159,1,0,0,0,0,161,1,0,0,0,0,163,1,0,0,0,0,165,1,0,0,0,0,167,1,0,0,0, + 0,169,1,0,0,0,0,171,1,0,0,0,0,173,1,0,0,0,0,175,1,0,0,0,0,177,1,0,0,0, + 0,179,1,0,0,0,0,181,1,0,0,0,0,183,1,0,0,0,0,185,1,0,0,0,0,187,1,0,0,0, + 0,189,1,0,0,0,0,191,1,0,0,0,0,193,1,0,0,0,0,195,1,0,0,0,0,197,1,0,0,0, + 0,199,1,0,0,0,0,201,1,0,0,0,0,203,1,0,0,0,0,205,1,0,0,0,0,207,1,0,0,0, + 0,209,1,0,0,0,0,211,1,0,0,0,0,213,1,0,0,0,0,215,1,0,0,0,0,217,1,0,0,0, + 0,221,1,0,0,0,0,223,1,0,0,0,0,225,1,0,0,0,0,229,1,0,0,0,0,231,1,0,0,0, + 0,233,1,0,0,0,0,235,1,0,0,0,0,237,1,0,0,0,0,239,1,0,0,0,1,241,1,0,0,0, + 3,248,1,0,0,0,5,253,1,0,0,0,7,259,1,0,0,0,9,263,1,0,0,0,11,266,1,0,0,0, + 13,270,1,0,0,0,15,273,1,0,0,0,17,277,1,0,0,0,19,282,1,0,0,0,21,286,1,0, + 0,0,23,291,1,0,0,0,25,296,1,0,0,0,27,301,1,0,0,0,29,305,1,0,0,0,31,311, + 1,0,0,0,33,316,1,0,0,0,35,322,1,0,0,0,37,327,1,0,0,0,39,333,1,0,0,0,41, + 338,1,0,0,0,43,341,1,0,0,0,45,344,1,0,0,0,47,350,1,0,0,0,49,356,1,0,0, + 0,51,359,1,0,0,0,53,362,1,0,0,0,55,369,1,0,0,0,57,376,1,0,0,0,59,379,1, + 0,0,0,61,384,1,0,0,0,63,396,1,0,0,0,65,405,1,0,0,0,67,413,1,0,0,0,69,421, + 1,0,0,0,71,427,1,0,0,0,73,431,1,0,0,0,75,441,1,0,0,0,77,448,1,0,0,0,79, + 455,1,0,0,0,81,461,1,0,0,0,83,467,1,0,0,0,85,474,1,0,0,0,87,483,1,0,0, + 0,89,495,1,0,0,0,91,500,1,0,0,0,93,506,1,0,0,0,95,513,1,0,0,0,97,516,1, + 0,0,0,99,520,1,0,0,0,101,525,1,0,0,0,103,532,1,0,0,0,105,539,1,0,0,0,107, + 544,1,0,0,0,109,551,1,0,0,0,111,559,1,0,0,0,113,563,1,0,0,0,115,574,1, + 0,0,0,117,582,1,0,0,0,119,593,1,0,0,0,121,600,1,0,0,0,123,607,1,0,0,0, + 125,615,1,0,0,0,127,624,1,0,0,0,129,631,1,0,0,0,131,635,1,0,0,0,133,643, + 1,0,0,0,135,646,1,0,0,0,137,653,1,0,0,0,139,659,1,0,0,0,141,669,1,0,0, + 0,143,674,1,0,0,0,145,686,1,0,0,0,147,691,1,0,0,0,149,700,1,0,0,0,151, + 707,1,0,0,0,153,713,1,0,0,0,155,718,1,0,0,0,157,728,1,0,0,0,159,739,1, + 0,0,0,161,747,1,0,0,0,163,752,1,0,0,0,165,756,1,0,0,0,167,761,1,0,0,0, + 169,766,1,0,0,0,171,772,1,0,0,0,173,777,1,0,0,0,175,779,1,0,0,0,177,781, + 1,0,0,0,179,783,1,0,0,0,181,785,1,0,0,0,183,787,1,0,0,0,185,789,1,0,0, + 0,187,791,1,0,0,0,189,797,1,0,0,0,191,799,1,0,0,0,193,802,1,0,0,0,195, + 805,1,0,0,0,197,807,1,0,0,0,199,809,1,0,0,0,201,811,1,0,0,0,203,813,1, + 0,0,0,205,815,1,0,0,0,207,817,1,0,0,0,209,819,1,0,0,0,211,838,1,0,0,0, + 213,840,1,0,0,0,215,848,1,0,0,0,217,882,1,0,0,0,219,884,1,0,0,0,221,913, + 1,0,0,0,223,915,1,0,0,0,225,924,1,0,0,0,227,956,1,0,0,0,229,958,1,0,0, + 0,231,966,1,0,0,0,233,974,1,0,0,0,235,985,1,0,0,0,237,991,1,0,0,0,239, + 1002,1,0,0,0,241,242,7,0,0,0,242,243,7,1,0,0,243,244,7,2,0,0,244,245,7, + 1,0,0,245,246,7,3,0,0,246,247,7,4,0,0,247,2,1,0,0,0,248,249,7,5,0,0,249, + 250,7,6,0,0,250,251,7,7,0,0,251,252,7,8,0,0,252,4,1,0,0,0,253,254,7,9, + 0,0,254,255,7,10,0,0,255,256,7,1,0,0,256,257,7,6,0,0,257,258,7,1,0,0,258, + 6,1,0,0,0,259,260,7,4,0,0,260,261,7,7,0,0,261,262,7,11,0,0,262,8,1,0,0, + 0,263,264,7,12,0,0,264,265,7,0,0,0,265,10,1,0,0,0,266,267,7,12,0,0,267, + 268,7,13,0,0,268,269,7,14,0,0,269,12,1,0,0,0,270,271,7,7,0,0,271,272,7, + 6,0,0,272,14,1,0,0,0,273,274,7,13,0,0,274,275,7,7,0,0,275,276,7,4,0,0, + 276,16,1,0,0,0,277,278,7,15,0,0,278,279,7,12,0,0,279,280,7,13,0,0,280, + 281,7,14,0,0,281,18,1,0,0,0,282,283,7,15,0,0,283,284,7,7,0,0,284,285,7, + 6,0,0,285,20,1,0,0,0,286,287,7,15,0,0,287,288,7,16,0,0,288,289,7,7,0,0, + 289,290,7,6,0,0,290,22,1,0,0,0,291,292,7,15,0,0,292,293,7,13,0,0,293,294, + 7,7,0,0,294,295,7,4,0,0,295,24,1,0,0,0,296,297,7,2,0,0,297,298,7,17,0, + 0,298,299,7,18,0,0,299,300,7,1,0,0,300,26,1,0,0,0,301,302,7,8,0,0,302, + 303,7,7,0,0,303,304,7,14,0,0,304,28,1,0,0,0,305,306,7,17,0,0,306,307,7, + 13,0,0,307,308,7,13,0,0,308,309,7,1,0,0,309,310,7,6,0,0,310,30,1,0,0,0, + 311,312,7,2,0,0,312,313,7,1,0,0,313,314,7,5,0,0,314,315,7,4,0,0,315,32, + 1,0,0,0,316,317,7,6,0,0,317,318,7,17,0,0,318,319,7,19,0,0,319,320,7,10, + 0,0,320,321,7,4,0,0,321,34,1,0,0,0,322,323,7,5,0,0,323,324,7,20,0,0,324, + 325,7,2,0,0,325,326,7,2,0,0,326,36,1,0,0,0,327,328,7,7,0,0,328,329,7,20, + 0,0,329,330,7,4,0,0,330,331,7,1,0,0,331,332,7,6,0,0,332,38,1,0,0,0,333, + 334,7,21,0,0,334,335,7,7,0,0,335,336,7,17,0,0,336,337,7,13,0,0,337,40, + 1,0,0,0,338,339,7,17,0,0,339,340,7,13,0,0,340,42,1,0,0,0,341,342,7,7,0, + 0,342,343,7,13,0,0,343,44,1,0,0,0,344,345,7,7,0,0,345,346,7,6,0,0,346, + 347,7,14,0,0,347,348,7,1,0,0,348,349,7,6,0,0,349,46,1,0,0,0,350,351,7, + 19,0,0,351,352,7,6,0,0,352,353,7,7,0,0,353,354,7,20,0,0,354,355,7,11,0, + 0,355,48,1,0,0,0,356,357,7,17,0,0,357,358,7,0,0,0,358,50,1,0,0,0,359,360, + 7,15,0,0,360,361,7,22,0,0,361,52,1,0,0,0,362,363,7,10,0,0,363,364,7,12, + 0,0,364,365,7,23,0,0,365,366,7,17,0,0,366,367,7,13,0,0,367,368,7,19,0, + 0,368,54,1,0,0,0,369,370,7,1,0,0,370,371,7,16,0,0,371,372,7,17,0,0,372, + 373,7,0,0,0,373,374,7,4,0,0,374,375,7,0,0,0,375,56,1,0,0,0,376,377,7,17, + 0,0,377,378,7,5,0,0,378,58,1,0,0,0,379,380,7,4,0,0,380,381,7,10,0,0,381, + 382,7,1,0,0,382,383,7,13,0,0,383,60,1,0,0,0,384,385,7,14,0,0,385,386,7, + 17,0,0,386,387,7,0,0,0,387,388,7,4,0,0,388,389,7,17,0,0,389,390,7,13,0, + 0,390,391,7,3,0,0,391,392,7,4,0,0,392,393,7,6,0,0,393,394,7,7,0,0,394, + 395,7,9,0,0,395,62,1,0,0,0,396,397,7,14,0,0,397,398,7,17,0,0,398,399,7, + 0,0,0,399,400,7,4,0,0,400,401,7,17,0,0,401,402,7,13,0,0,402,403,7,3,0, + 0,403,404,7,4,0,0,404,64,1,0,0,0,405,406,7,11,0,0,406,407,7,1,0,0,407, + 408,7,6,0,0,408,409,7,3,0,0,409,410,7,1,0,0,410,411,7,13,0,0,411,412,7, + 4,0,0,412,66,1,0,0,0,413,414,7,15,0,0,414,415,7,1,0,0,415,416,7,4,0,0, + 416,417,7,9,0,0,417,418,7,1,0,0,418,419,7,1,0,0,419,420,7,13,0,0,420,68, + 1,0,0,0,421,422,7,20,0,0,422,423,7,13,0,0,423,424,7,17,0,0,424,425,7,7, + 0,0,425,426,7,13,0,0,426,70,1,0,0,0,427,428,7,12,0,0,428,429,7,2,0,0,429, + 430,7,2,0,0,430,72,1,0,0,0,431,432,7,17,0,0,432,433,7,13,0,0,433,434,7, + 4,0,0,434,435,7,1,0,0,435,436,7,6,0,0,436,437,7,0,0,0,437,438,7,1,0,0, + 438,439,7,3,0,0,439,440,7,4,0,0,440,74,1,0,0,0,441,442,7,1,0,0,442,443, + 7,16,0,0,443,444,7,3,0,0,444,445,7,1,0,0,445,446,7,11,0,0,446,447,7,4, + 0,0,447,76,1,0,0,0,448,449,7,3,0,0,449,450,7,6,0,0,450,451,7,1,0,0,451, + 452,7,12,0,0,452,453,7,4,0,0,453,454,7,1,0,0,454,78,1,0,0,0,455,456,7, + 4,0,0,456,457,7,12,0,0,457,458,7,15,0,0,458,459,7,2,0,0,459,460,7,1,0, + 0,460,80,1,0,0,0,461,462,7,15,0,0,462,463,7,1,0,0,463,464,7,19,0,0,464, + 465,7,17,0,0,465,466,7,13,0,0,466,82,1,0,0,0,467,468,7,3,0,0,468,469,7, + 7,0,0,469,470,7,8,0,0,470,471,7,8,0,0,471,472,7,17,0,0,472,473,7,4,0,0, + 473,84,1,0,0,0,474,475,7,6,0,0,475,476,7,7,0,0,476,477,7,2,0,0,477,478, + 7,2,0,0,478,479,7,15,0,0,479,480,7,12,0,0,480,481,7,3,0,0,481,482,7,18, + 0,0,482,86,1,0,0,0,483,484,7,4,0,0,484,485,7,6,0,0,485,486,7,12,0,0,486, + 487,7,13,0,0,487,488,7,0,0,0,488,489,7,12,0,0,489,490,7,3,0,0,490,491, + 7,4,0,0,491,492,7,17,0,0,492,493,7,7,0,0,493,494,7,13,0,0,494,88,1,0,0, + 0,495,496,7,9,0,0,496,497,7,7,0,0,497,498,7,6,0,0,498,499,7,18,0,0,499, + 90,1,0,0,0,500,501,7,12,0,0,501,502,7,2,0,0,502,503,7,4,0,0,503,504,7, + 1,0,0,504,505,7,6,0,0,505,92,1,0,0,0,506,507,7,6,0,0,507,508,7,1,0,0,508, + 509,7,13,0,0,509,510,7,12,0,0,510,511,7,8,0,0,511,512,7,1,0,0,512,94,1, + 0,0,0,513,514,7,4,0,0,514,515,7,7,0,0,515,96,1,0,0,0,516,517,7,12,0,0, + 517,518,7,14,0,0,518,519,7,14,0,0,519,98,1,0,0,0,520,521,7,14,0,0,521, + 522,7,6,0,0,522,523,7,7,0,0,523,524,7,11,0,0,524,100,1,0,0,0,525,526,7, + 3,0,0,526,527,7,7,0,0,527,528,7,2,0,0,528,529,7,20,0,0,529,530,7,8,0,0, + 530,531,7,13,0,0,531,102,1,0,0,0,532,533,7,17,0,0,533,534,7,13,0,0,534, + 535,7,0,0,0,535,536,7,1,0,0,536,537,7,6,0,0,537,538,7,4,0,0,538,104,1, + 0,0,0,539,540,7,17,0,0,540,541,7,13,0,0,541,542,7,4,0,0,542,543,7,7,0, + 0,543,106,1,0,0,0,544,545,7,23,0,0,545,546,7,12,0,0,546,547,7,2,0,0,547, + 548,7,20,0,0,548,549,7,1,0,0,549,550,7,0,0,0,550,108,1,0,0,0,551,552,7, + 11,0,0,552,553,7,6,0,0,553,554,7,17,0,0,554,555,7,8,0,0,555,556,7,12,0, + 0,556,557,7,6,0,0,557,558,7,22,0,0,558,110,1,0,0,0,559,560,7,18,0,0,560, + 561,7,1,0,0,561,562,7,22,0,0,562,112,1,0,0,0,563,564,7,3,0,0,564,565,7, + 7,0,0,565,566,7,13,0,0,566,567,7,0,0,0,567,568,7,4,0,0,568,569,7,6,0,0, + 569,570,7,12,0,0,570,571,7,17,0,0,571,572,7,13,0,0,572,573,7,4,0,0,573, + 114,1,0,0,0,574,575,7,5,0,0,575,576,7,7,0,0,576,577,7,6,0,0,577,578,7, + 1,0,0,578,579,7,17,0,0,579,580,7,19,0,0,580,581,7,13,0,0,581,116,1,0,0, + 0,582,583,7,6,0,0,583,584,7,1,0,0,584,585,7,5,0,0,585,586,7,1,0,0,586, + 587,7,6,0,0,587,588,7,1,0,0,588,589,7,13,0,0,589,590,7,3,0,0,590,591,7, + 1,0,0,591,592,7,0,0,0,592,118,1,0,0,0,593,594,7,14,0,0,594,595,7,1,0,0, + 595,596,7,2,0,0,596,597,7,1,0,0,597,598,7,4,0,0,598,599,7,1,0,0,599,120, + 1,0,0,0,600,601,7,20,0,0,601,602,7,11,0,0,602,603,7,14,0,0,603,604,7,12, + 0,0,604,605,7,4,0,0,605,606,7,1,0,0,606,122,1,0,0,0,607,608,7,3,0,0,608, + 609,7,12,0,0,609,610,7,0,0,0,610,611,7,3,0,0,611,612,7,12,0,0,612,613, + 7,14,0,0,613,614,7,1,0,0,614,124,1,0,0,0,615,616,7,6,0,0,616,617,7,1,0, + 0,617,618,7,0,0,0,618,619,7,4,0,0,619,620,7,6,0,0,620,621,7,17,0,0,621, + 622,7,3,0,0,622,623,7,4,0,0,623,126,1,0,0,0,624,625,7,12,0,0,625,626,7, + 3,0,0,626,627,7,4,0,0,627,628,7,17,0,0,628,629,7,7,0,0,629,630,7,13,0, + 0,630,128,1,0,0,0,631,632,7,0,0,0,632,633,7,1,0,0,633,634,7,4,0,0,634, + 130,1,0,0,0,635,636,7,14,0,0,636,637,7,1,0,0,637,638,7,5,0,0,638,639,7, + 12,0,0,639,640,7,20,0,0,640,641,7,2,0,0,641,642,7,4,0,0,642,132,1,0,0, + 0,643,644,7,13,0,0,644,645,7,7,0,0,645,134,1,0,0,0,646,647,7,20,0,0,647, + 648,7,13,0,0,648,649,7,17,0,0,649,650,7,24,0,0,650,651,7,20,0,0,651,652, + 7,1,0,0,652,136,1,0,0,0,653,654,7,17,0,0,654,655,7,13,0,0,655,656,7,14, + 0,0,656,657,7,1,0,0,657,658,7,16,0,0,658,138,1,0,0,0,659,660,7,4,0,0,660, + 661,7,1,0,0,661,662,7,8,0,0,662,663,7,11,0,0,663,664,7,7,0,0,664,665,7, + 6,0,0,665,666,7,12,0,0,666,667,7,6,0,0,667,668,7,22,0,0,668,140,1,0,0, + 0,669,670,7,9,0,0,670,671,7,17,0,0,671,672,7,4,0,0,672,673,7,10,0,0,673, + 142,1,0,0,0,674,675,7,3,0,0,675,676,7,7,0,0,676,677,7,8,0,0,677,678,7, + 11,0,0,678,679,7,6,0,0,679,680,7,1,0,0,680,681,7,0,0,0,681,682,7,0,0,0, + 682,683,7,17,0,0,683,684,7,7,0,0,684,685,7,13,0,0,685,144,1,0,0,0,686, + 687,7,3,0,0,687,688,7,7,0,0,688,689,7,8,0,0,689,690,7,11,0,0,690,146,1, + 0,0,0,691,692,7,14,0,0,692,693,7,17,0,0,693,694,7,0,0,0,694,695,7,12,0, + 0,695,696,7,2,0,0,696,697,7,2,0,0,697,698,7,7,0,0,698,699,7,9,0,0,699, + 148,1,0,0,0,700,701,7,17,0,0,701,702,7,19,0,0,702,703,7,13,0,0,703,704, + 7,7,0,0,704,705,7,6,0,0,705,706,7,1,0,0,706,150,1,0,0,0,707,708,7,3,0, + 0,708,709,7,10,0,0,709,710,7,1,0,0,710,711,7,3,0,0,711,712,7,18,0,0,712, + 152,1,0,0,0,713,714,7,23,0,0,714,715,7,17,0,0,715,716,7,1,0,0,716,717, + 7,9,0,0,717,154,1,0,0,0,718,719,7,11,0,0,719,720,7,6,0,0,720,721,7,7,0, + 0,721,722,7,3,0,0,722,723,7,1,0,0,723,724,7,14,0,0,724,725,7,20,0,0,725, + 726,7,6,0,0,726,727,7,1,0,0,727,156,1,0,0,0,728,729,7,11,0,0,729,730,7, + 12,0,0,730,731,7,6,0,0,731,732,7,12,0,0,732,733,7,8,0,0,733,734,7,1,0, + 0,734,735,7,4,0,0,735,736,7,1,0,0,736,737,7,6,0,0,737,738,7,0,0,0,738, + 158,1,0,0,0,739,740,7,1,0,0,740,741,7,16,0,0,741,742,7,1,0,0,742,743,7, + 3,0,0,743,744,7,20,0,0,744,745,7,4,0,0,745,746,7,1,0,0,746,160,1,0,0,0, + 747,748,7,1,0,0,748,749,7,16,0,0,749,750,7,1,0,0,750,751,7,3,0,0,751,162, + 1,0,0,0,752,753,7,12,0,0,753,754,7,0,0,0,754,755,7,3,0,0,755,164,1,0,0, + 0,756,757,7,14,0,0,757,758,7,1,0,0,758,759,7,0,0,0,759,760,7,3,0,0,760, + 166,1,0,0,0,761,762,7,4,0,0,762,763,7,6,0,0,763,764,7,20,0,0,764,765,7, + 1,0,0,765,168,1,0,0,0,766,767,7,5,0,0,767,768,7,12,0,0,768,769,7,2,0,0, + 769,770,7,0,0,0,770,771,7,1,0,0,771,170,1,0,0,0,772,773,7,13,0,0,773,774, + 7,20,0,0,774,775,7,2,0,0,775,776,7,2,0,0,776,172,1,0,0,0,777,778,5,42, + 0,0,778,174,1,0,0,0,779,780,5,47,0,0,780,176,1,0,0,0,781,782,5,92,0,0, + 782,178,1,0,0,0,783,784,5,94,0,0,784,180,1,0,0,0,785,786,5,43,0,0,786, + 182,1,0,0,0,787,788,5,45,0,0,788,184,1,0,0,0,789,790,5,38,0,0,790,186, + 1,0,0,0,791,792,5,61,0,0,792,188,1,0,0,0,793,794,5,60,0,0,794,798,5,62, + 0,0,795,796,5,33,0,0,796,798,5,61,0,0,797,793,1,0,0,0,797,795,1,0,0,0, + 798,190,1,0,0,0,799,800,5,60,0,0,800,801,5,61,0,0,801,192,1,0,0,0,802, + 803,5,62,0,0,803,804,5,61,0,0,804,194,1,0,0,0,805,806,5,60,0,0,806,196, + 1,0,0,0,807,808,5,62,0,0,808,198,1,0,0,0,809,810,5,40,0,0,810,200,1,0, + 0,0,811,812,5,41,0,0,812,202,1,0,0,0,813,814,5,44,0,0,814,204,1,0,0,0, + 815,816,5,46,0,0,816,206,1,0,0,0,817,818,5,59,0,0,818,208,1,0,0,0,819, + 820,5,64,0,0,820,821,5,64,0,0,821,822,1,0,0,0,822,826,7,25,0,0,823,825, + 7,26,0,0,824,823,1,0,0,0,825,828,1,0,0,0,826,824,1,0,0,0,826,827,1,0,0, + 0,827,210,1,0,0,0,828,826,1,0,0,0,829,839,5,63,0,0,830,831,5,64,0,0,831, + 835,7,25,0,0,832,834,7,26,0,0,833,832,1,0,0,0,834,837,1,0,0,0,835,833, + 1,0,0,0,835,836,1,0,0,0,836,839,1,0,0,0,837,835,1,0,0,0,838,829,1,0,0, + 0,838,830,1,0,0,0,839,212,1,0,0,0,840,841,5,48,0,0,841,843,7,16,0,0,842, + 844,7,27,0,0,843,842,1,0,0,0,844,845,1,0,0,0,845,843,1,0,0,0,845,846,1, + 0,0,0,846,214,1,0,0,0,847,849,7,28,0,0,848,847,1,0,0,0,849,850,1,0,0,0, + 850,848,1,0,0,0,850,851,1,0,0,0,851,216,1,0,0,0,852,854,7,28,0,0,853,852, + 1,0,0,0,854,855,1,0,0,0,855,853,1,0,0,0,855,856,1,0,0,0,856,857,1,0,0, + 0,857,861,5,46,0,0,858,860,7,28,0,0,859,858,1,0,0,0,860,863,1,0,0,0,861, + 859,1,0,0,0,861,862,1,0,0,0,862,865,1,0,0,0,863,861,1,0,0,0,864,866,3, + 219,109,0,865,864,1,0,0,0,865,866,1,0,0,0,866,883,1,0,0,0,867,869,5,46, + 0,0,868,870,7,28,0,0,869,868,1,0,0,0,870,871,1,0,0,0,871,869,1,0,0,0,871, + 872,1,0,0,0,872,874,1,0,0,0,873,875,3,219,109,0,874,873,1,0,0,0,874,875, + 1,0,0,0,875,883,1,0,0,0,876,878,7,28,0,0,877,876,1,0,0,0,878,879,1,0,0, + 0,879,877,1,0,0,0,879,880,1,0,0,0,880,881,1,0,0,0,881,883,3,219,109,0, + 882,853,1,0,0,0,882,867,1,0,0,0,882,877,1,0,0,0,883,218,1,0,0,0,884,886, + 7,1,0,0,885,887,7,29,0,0,886,885,1,0,0,0,886,887,1,0,0,0,887,889,1,0,0, + 0,888,890,7,28,0,0,889,888,1,0,0,0,890,891,1,0,0,0,891,889,1,0,0,0,891, + 892,1,0,0,0,892,220,1,0,0,0,893,899,5,34,0,0,894,898,8,30,0,0,895,896, + 5,34,0,0,896,898,5,34,0,0,897,894,1,0,0,0,897,895,1,0,0,0,898,901,1,0, + 0,0,899,897,1,0,0,0,899,900,1,0,0,0,900,902,1,0,0,0,901,899,1,0,0,0,902, + 914,5,34,0,0,903,909,5,39,0,0,904,908,8,31,0,0,905,906,5,39,0,0,906,908, + 5,39,0,0,907,904,1,0,0,0,907,905,1,0,0,0,908,911,1,0,0,0,909,907,1,0,0, + 0,909,910,1,0,0,0,910,912,1,0,0,0,911,909,1,0,0,0,912,914,5,39,0,0,913, + 893,1,0,0,0,913,903,1,0,0,0,914,222,1,0,0,0,915,919,5,35,0,0,916,918,8, + 32,0,0,917,916,1,0,0,0,918,921,1,0,0,0,919,917,1,0,0,0,919,920,1,0,0,0, + 920,922,1,0,0,0,921,919,1,0,0,0,922,923,5,35,0,0,923,224,1,0,0,0,924,926, + 5,123,0,0,925,927,3,227,113,0,926,925,1,0,0,0,927,928,1,0,0,0,928,926, + 1,0,0,0,928,929,1,0,0,0,929,930,1,0,0,0,930,932,5,45,0,0,931,933,3,227, + 113,0,932,931,1,0,0,0,933,934,1,0,0,0,934,932,1,0,0,0,934,935,1,0,0,0, + 935,936,1,0,0,0,936,938,5,45,0,0,937,939,3,227,113,0,938,937,1,0,0,0,939, + 940,1,0,0,0,940,938,1,0,0,0,940,941,1,0,0,0,941,942,1,0,0,0,942,944,5, + 45,0,0,943,945,3,227,113,0,944,943,1,0,0,0,945,946,1,0,0,0,946,944,1,0, + 0,0,946,947,1,0,0,0,947,948,1,0,0,0,948,950,5,45,0,0,949,951,3,227,113, + 0,950,949,1,0,0,0,951,952,1,0,0,0,952,950,1,0,0,0,952,953,1,0,0,0,953, + 954,1,0,0,0,954,955,5,125,0,0,955,226,1,0,0,0,956,957,7,27,0,0,957,228, + 1,0,0,0,958,960,5,91,0,0,959,961,8,33,0,0,960,959,1,0,0,0,961,962,1,0, + 0,0,962,960,1,0,0,0,962,963,1,0,0,0,963,964,1,0,0,0,964,965,5,93,0,0,965, + 230,1,0,0,0,966,968,5,96,0,0,967,969,8,34,0,0,968,967,1,0,0,0,969,970, + 1,0,0,0,970,968,1,0,0,0,970,971,1,0,0,0,971,972,1,0,0,0,972,973,5,96,0, + 0,973,232,1,0,0,0,974,978,7,25,0,0,975,977,7,26,0,0,976,975,1,0,0,0,977, + 980,1,0,0,0,978,976,1,0,0,0,978,979,1,0,0,0,979,982,1,0,0,0,980,978,1, + 0,0,0,981,983,5,36,0,0,982,981,1,0,0,0,982,983,1,0,0,0,983,234,1,0,0,0, + 984,986,7,35,0,0,985,984,1,0,0,0,986,987,1,0,0,0,987,985,1,0,0,0,987,988, + 1,0,0,0,988,989,1,0,0,0,989,990,6,117,0,0,990,236,1,0,0,0,991,992,5,45, + 0,0,992,993,5,45,0,0,993,997,1,0,0,0,994,996,8,36,0,0,995,994,1,0,0,0, + 996,999,1,0,0,0,997,995,1,0,0,0,997,998,1,0,0,0,998,1000,1,0,0,0,999,997, + 1,0,0,0,1000,1001,6,118,0,0,1001,238,1,0,0,0,1002,1003,5,47,0,0,1003,1004, + 5,42,0,0,1004,1008,1,0,0,0,1005,1007,9,0,0,0,1006,1005,1,0,0,0,1007,1010, + 1,0,0,0,1008,1009,1,0,0,0,1008,1006,1,0,0,0,1009,1011,1,0,0,0,1010,1008, + 1,0,0,0,1011,1012,5,42,0,0,1012,1013,5,47,0,0,1013,1014,1,0,0,0,1014,1015, + 6,119,0,0,1015,240,1,0,0,0,34,0,797,826,835,838,845,850,855,861,865,871, + 874,879,882,886,891,897,899,907,909,913,919,928,934,940,946,952,962,970, + 978,982,987,997,1008,1,6,0,0 }; public static readonly ATN _ATN = diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs index 9c9b6ba08..1c81aa87e 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs @@ -38,22 +38,22 @@ public partial class AccessSqlParser : Parser { protected static PredictionContextCache sharedContextCache = new PredictionContextCache(); public const int SELECT=1, FROM=2, WHERE=3, TOP=4, AS=5, AND=6, OR=7, NOT=8, BAND=9, BOR=10, - BXOR=11, BNOT=12, LIKE=13, MOD=14, INNER=15, LEFT=16, RIGHT=17, OUTER=18, - JOIN=19, IN=20, ON=21, ORDER=22, GROUP=23, IS=24, BY=25, HAVING=26, EXISTS=27, - IF=28, THEN=29, DISTINCTROW=30, DISTINCT=31, PERCENT=32, BETWEEN=33, UNION=34, - ALL=35, INTERSECT=36, EXCEPT=37, CREATE=38, TABLE=39, BEGIN=40, COMMIT=41, - ROLLBACK=42, TRANSACTION=43, WORK=44, ALTER=45, RENAME=46, TO=47, ADD=48, - DROP=49, COLUMN=50, INSERT=51, INTO=52, VALUES=53, PRIMARY=54, KEY=55, - CONSTRAINT=56, FOREIGN=57, REFERENCES=58, DELETE=59, UPDATE=60, CASCADE=61, - RESTRICT=62, ACTION=63, SET=64, DEFAULT=65, NO=66, UNIQUE=67, INDEX=68, - TEMPORARY=69, WITH=70, COMPRESSION=71, COMP=72, DISALLOW=73, IGNORE=74, - CHECK=75, VIEW=76, PROCEDURE=77, PARAMETERS=78, EXECUTE=79, EXEC=80, ASC=81, - DESC=82, TRUE=83, FALSE=84, NULL=85, STAR=86, SLASH=87, BACKSLASH=88, - CARET=89, PLUS=90, MINUS=91, AMP=92, EQ=93, NEQ=94, LTE=95, GTE=96, LT=97, - GT=98, LPAREN=99, RPAREN=100, COMMA=101, DOT=102, SEMI=103, SYSVAR=104, - PARAM=105, HEX_LITERAL=106, INTEGER_LITERAL=107, NUMBER_LITERAL=108, STRING_LITERAL=109, - DATE_LITERAL=110, GUID_LITERAL=111, BRACKET_ID=112, BACKTICK_ID=113, IDENTIFIER=114, - WS=115, LINE_COMMENT=116, BLOCK_COMMENT=117; + BXOR=11, BNOT=12, LIKE=13, MOD=14, INNER=15, LEFT=16, RIGHT=17, FULL=18, + OUTER=19, JOIN=20, IN=21, ON=22, ORDER=23, GROUP=24, IS=25, BY=26, HAVING=27, + EXISTS=28, IF=29, THEN=30, DISTINCTROW=31, DISTINCT=32, PERCENT=33, BETWEEN=34, + UNION=35, ALL=36, INTERSECT=37, EXCEPT=38, CREATE=39, TABLE=40, BEGIN=41, + COMMIT=42, ROLLBACK=43, TRANSACTION=44, WORK=45, ALTER=46, RENAME=47, + TO=48, ADD=49, DROP=50, COLUMN=51, INSERT=52, INTO=53, VALUES=54, PRIMARY=55, + KEY=56, CONSTRAINT=57, FOREIGN=58, REFERENCES=59, DELETE=60, UPDATE=61, + CASCADE=62, RESTRICT=63, ACTION=64, SET=65, DEFAULT=66, NO=67, UNIQUE=68, + INDEX=69, TEMPORARY=70, WITH=71, COMPRESSION=72, COMP=73, DISALLOW=74, + IGNORE=75, CHECK=76, VIEW=77, PROCEDURE=78, PARAMETERS=79, EXECUTE=80, + EXEC=81, ASC=82, DESC=83, TRUE=84, FALSE=85, NULL=86, STAR=87, SLASH=88, + BACKSLASH=89, CARET=90, PLUS=91, MINUS=92, AMP=93, EQ=94, NEQ=95, LTE=96, + GTE=97, LT=98, GT=99, LPAREN=100, RPAREN=101, COMMA=102, DOT=103, SEMI=104, + SYSVAR=105, PARAM=106, HEX_LITERAL=107, INTEGER_LITERAL=108, NUMBER_LITERAL=109, + STRING_LITERAL=110, DATE_LITERAL=111, GUID_LITERAL=112, BRACKET_ID=113, + BACKTICK_ID=114, IDENTIFIER=115, WS=116, LINE_COMMENT=117, BLOCK_COMMENT=118; public const int RULE_statement = 0, RULE_ifThenStatement = 1, RULE_thenBody = 2, RULE_executeStatement = 3, RULE_updateStatement = 4, RULE_assignment = 5, RULE_deleteStatement = 6, @@ -99,13 +99,13 @@ public const int null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, "'*'", "'/'", "'\\'", "'^'", "'+'", "'-'", "'&'", "'='", null, - "'<='", "'>='", "'<'", "'>'", "'('", "')'", "','", "'.'", "';'" + null, null, null, "'*'", "'/'", "'\\'", "'^'", "'+'", "'-'", "'&'", "'='", + null, "'<='", "'>='", "'<'", "'>'", "'('", "')'", "','", "'.'", "';'" }; private static readonly string[] _SymbolicNames = { null, "SELECT", "FROM", "WHERE", "TOP", "AS", "AND", "OR", "NOT", "BAND", - "BOR", "BXOR", "BNOT", "LIKE", "MOD", "INNER", "LEFT", "RIGHT", "OUTER", - "JOIN", "IN", "ON", "ORDER", "GROUP", "IS", "BY", "HAVING", "EXISTS", + "BOR", "BXOR", "BNOT", "LIKE", "MOD", "INNER", "LEFT", "RIGHT", "FULL", + "OUTER", "JOIN", "IN", "ON", "ORDER", "GROUP", "IS", "BY", "HAVING", "EXISTS", "IF", "THEN", "DISTINCTROW", "DISTINCT", "PERCENT", "BETWEEN", "UNION", "ALL", "INTERSECT", "EXCEPT", "CREATE", "TABLE", "BEGIN", "COMMIT", "ROLLBACK", "TRANSACTION", "WORK", "ALTER", "RENAME", "TO", "ADD", "DROP", "COLUMN", @@ -613,7 +613,7 @@ public ExecuteStatementContext executeStatement() { State = 178; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 134418688L) != 0) || ((((_la - 81)) & ~0x3f) == 0 && ((1L << (_la - 81)) & 17171743773L) != 0)) { + if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 268636416L) != 0) || ((((_la - 82)) & ~0x3f) == 0 && ((1L << (_la - 82)) & 17171743773L) != 0)) { { State = 170; expression(0); @@ -974,7 +974,7 @@ public SysVarItemContext sysVarItem() { State = 225; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 112)) & ~0x3f) == 0 && ((1L << (_la - 112)) & 7L) != 0)) { + if (_la==AS || ((((_la - 113)) & ~0x3f) == 0 && ((1L << (_la - 113)) & 7L) != 0)) { { State = 222; ErrorHandler.Sync(this); @@ -1335,7 +1335,7 @@ public CreateProcedureStatementContext createProcedureStatement() { State = 285; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (((((_la - 99)) & ~0x3f) == 0 && ((1L << (_la - 99)) & 57409L) != 0)) { + if (((((_la - 100)) & ~0x3f) == 0 && ((1L << (_la - 100)) & 57409L) != 0)) { { State = 284; procParamList(); @@ -1863,7 +1863,7 @@ public AlterTableActionContext alterTableAction() { State = 339; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 378302368699121920L) != 0) || ((((_la - 65)) & ~0x3f) == 0 && ((1L << (_la - 65)) & 1049637L) != 0)) { + while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 756604737398243584L) != 0) || ((((_la - 66)) & ~0x3f) == 0 && ((1L << (_la - 66)) & 1049637L) != 0)) { { { State = 336; @@ -2544,7 +2544,7 @@ public ColumnDefinitionContext columnDefinition() { State = 442; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 378302368699121920L) != 0) || ((((_la - 65)) & ~0x3f) == 0 && ((1L << (_la - 65)) & 1049637L) != 0)) { + while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 756604737398243584L) != 0) || ((((_la - 66)) & ~0x3f) == 0 && ((1L << (_la - 66)) & 1049637L) != 0)) { { { State = 439; @@ -2625,7 +2625,7 @@ public DataTypeContext dataType() { State = 450; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (((((_la - 112)) & ~0x3f) == 0 && ((1L << (_la - 112)) & 7L) != 0)) { + if (((((_la - 113)) & ~0x3f) == 0 && ((1L << (_la - 113)) & 7L) != 0)) { { State = 449; _localctx.extra2 = identifier(); @@ -3482,7 +3482,7 @@ public CheckBodyContext checkBody() { State = 613; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - while ((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & 18014329790005247L) != 0)) { + while ((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & 36028659580010495L) != 0)) { { State = 611; ErrorHandler.Sync(this); @@ -3504,6 +3504,7 @@ public CheckBodyContext checkBody() { case INNER: case LEFT: case RIGHT: + case FULL: case OUTER: case JOIN: case IN: @@ -3868,6 +3869,7 @@ public partial class InsertStatementContext : ParserRuleContext { public IdentifierContext table; public IdentifierContext _identifier; public IList _columns = new List(); + public QueryExpressionContext source; [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode INSERT() { return GetToken(AccessSqlParser.INSERT, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode INTO() { return GetToken(AccessSqlParser.INTO, 0); } [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext[] identifier() { @@ -3876,6 +3878,7 @@ [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext[] identifier() [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier(int i) { return GetRuleContext(i); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DEFAULT() { return GetToken(AccessSqlParser.DEFAULT, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode VALUES() { return GetToken(AccessSqlParser.VALUES, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode[] LPAREN() { return GetTokens(AccessSqlParser.LPAREN); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode LPAREN(int i) { @@ -3891,7 +3894,9 @@ [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression(int [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode RPAREN(int i) { return GetToken(AccessSqlParser.RPAREN, i); } - [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DEFAULT() { return GetToken(AccessSqlParser.DEFAULT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public QueryExpressionContext queryExpression() { + return GetRuleContext(0); + } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode[] COMMA() { return GetTokens(AccessSqlParser.COMMA); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMMA(int i) { return GetToken(AccessSqlParser.COMMA, i); @@ -3923,16 +3928,17 @@ public InsertStatementContext insertStatement() { Match(INTO); State = 636; _localctx.table = identifier(); - State = 664; + State = 667; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { + case SELECT: case VALUES: case LPAREN: { State = 648; ErrorHandler.Sync(this); - _la = TokenStream.LA(1); - if (_la==LPAREN) { + switch ( Interpreter.AdaptivePredict(TokenStream,69,Context) ) { + case 1: { State = 637; Match(LPAREN); @@ -3959,39 +3965,56 @@ public InsertStatementContext insertStatement() { State = 646; Match(RPAREN); } + break; } - - State = 650; - Match(VALUES); - State = 651; - Match(LPAREN); - State = 652; - expression(0); - State = 657; + State = 663; ErrorHandler.Sync(this); - _la = TokenStream.LA(1); - while (_la==COMMA) { - { + switch (TokenStream.LA(1)) { + case VALUES: { - State = 653; - Match(COMMA); - State = 654; + State = 650; + Match(VALUES); + State = 651; + Match(LPAREN); + State = 652; expression(0); - } - } - State = 659; + State = 657; ErrorHandler.Sync(this); _la = TokenStream.LA(1); + while (_la==COMMA) { + { + { + State = 653; + Match(COMMA); + State = 654; + expression(0); + } + } + State = 659; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + } + State = 660; + Match(RPAREN); + } + break; + case SELECT: + case LPAREN: + { + State = 662; + _localctx.source = queryExpression(); + } + break; + default: + throw new NoViableAltException(this); } - State = 660; - Match(RPAREN); } break; case DEFAULT: { - State = 662; + State = 665; Match(DEFAULT); - State = 663; + State = 666; Match(VALUES); } break; @@ -4045,21 +4068,21 @@ public QueryExpressionContext queryExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 666; + State = 669; queryTerm(); - State = 672; + State = 675; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 223338299392L) != 0)) { + while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 446676598784L) != 0)) { { { - State = 667; + State = 670; setOperator(); - State = 668; + State = 671; queryTerm(); } } - State = 674; + State = 677; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4120,14 +4143,14 @@ public QueryTermContext queryTerm() { QueryTermContext _localctx = new QueryTermContext(Context, State); EnterRule(_localctx, 66, RULE_queryTerm); try { - State = 680; + State = 683; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: _localctx = new SelectTermContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 675; + State = 678; selectStatement(); } break; @@ -4135,11 +4158,11 @@ public QueryTermContext queryTerm() { _localctx = new ParenTermContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 676; + State = 679; Match(LPAREN); - State = 677; + State = 680; queryExpression(); - State = 678; + State = 681; Match(RPAREN); } break; @@ -4182,20 +4205,20 @@ public SetOperatorContext setOperator() { EnterRule(_localctx, 68, RULE_setOperator); int _la; try { - State = 688; + State = 691; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case UNION: EnterOuterAlt(_localctx, 1); { - State = 682; + State = 685; Match(UNION); - State = 684; + State = 687; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ALL) { { - State = 683; + State = 686; Match(ALL); } } @@ -4205,14 +4228,14 @@ public SetOperatorContext setOperator() { case INTERSECT: EnterOuterAlt(_localctx, 2); { - State = 686; + State = 689; Match(INTERSECT); } break; case EXCEPT: EnterOuterAlt(_localctx, 3); { - State = 687; + State = 690; Match(EXCEPT); } break; @@ -4233,6 +4256,7 @@ public SetOperatorContext setOperator() { public partial class SelectStatementContext : ParserRuleContext { public SelectPredicateContext predicate; + public IdentifierContext into; [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode SELECT() { return GetToken(AccessSqlParser.SELECT, 0); } [System.Diagnostics.DebuggerNonUserCode] public SelectListContext selectList() { return GetRuleContext(0); @@ -4240,6 +4264,7 @@ [System.Diagnostics.DebuggerNonUserCode] public SelectListContext selectList() { [System.Diagnostics.DebuggerNonUserCode] public TopClauseContext topClause() { return GetRuleContext(0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode INTO() { return GetToken(AccessSqlParser.INTO, 0); } [System.Diagnostics.DebuggerNonUserCode] public FromClauseContext fromClause() { return GetRuleContext(0); } @@ -4258,6 +4283,9 @@ [System.Diagnostics.DebuggerNonUserCode] public OrderByClauseContext orderByClau [System.Diagnostics.DebuggerNonUserCode] public SelectPredicateContext selectPredicate() { return GetRuleContext(0); } + [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() { + return GetRuleContext(0); + } public SelectStatementContext(ParserRuleContext parent, int invokingState) : base(parent, invokingState) { @@ -4279,76 +4307,88 @@ public SelectStatementContext selectStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 690; + State = 693; Match(SELECT); - State = 692; + State = 695; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 37580963840L) != 0)) { + if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 75161927680L) != 0)) { { - State = 691; + State = 694; _localctx.predicate = selectPredicate(); } } - State = 695; + State = 698; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TOP) { { - State = 694; + State = 697; topClause(); } } - State = 697; + State = 700; selectList(); - State = 699; + State = 703; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + if (_la==INTO) { + { + State = 701; + Match(INTO); + State = 702; + _localctx.into = identifier(); + } + } + + State = 706; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FROM) { { - State = 698; + State = 705; fromClause(); } } - State = 702; + State = 709; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 701; + State = 708; whereClause(); } } - State = 705; + State = 712; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==GROUP) { { - State = 704; + State = 711; groupByClause(); } } - State = 708; + State = 715; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==HAVING) { { - State = 707; + State = 714; havingClause(); } } - State = 711; + State = 718; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ORDER) { { - State = 710; + State = 717; orderByClause(); } } @@ -4391,9 +4431,9 @@ public SelectPredicateContext selectPredicate() { try { EnterOuterAlt(_localctx, 1); { - State = 713; + State = 720; _la = TokenStream.LA(1); - if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 37580963840L) != 0)) ) { + if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 75161927680L) != 0)) ) { ErrorHandler.RecoverInline(this); } else { @@ -4447,25 +4487,25 @@ public GroupByClauseContext groupByClause() { try { EnterOuterAlt(_localctx, 1); { - State = 715; + State = 722; Match(GROUP); - State = 716; + State = 723; Match(BY); - State = 717; + State = 724; expression(0); - State = 722; + State = 729; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 718; + State = 725; Match(COMMA); - State = 719; + State = 726; expression(0); } } - State = 724; + State = 731; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4507,9 +4547,9 @@ public HavingClauseContext havingClause() { try { EnterOuterAlt(_localctx, 1); { - State = 725; + State = 732; Match(HAVING); - State = 726; + State = 733; expression(0); } } @@ -4564,18 +4604,18 @@ public TopClauseContext topClause() { int _alt; EnterOuterAlt(_localctx, 1); { - State = 728; + State = 735; Match(TOP); - State = 729; + State = 736; topOperand(); - State = 734; + State = 741; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,84,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,86,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 730; + State = 737; _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { ErrorHandler.RecoverInline(this); @@ -4584,21 +4624,21 @@ public TopClauseContext topClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 731; + State = 738; topOperand(); } } } - State = 736; + State = 743; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,84,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,86,Context); } - State = 738; + State = 745; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PERCENT) { { - State = 737; + State = 744; _localctx.percent = Match(PERCENT); } } @@ -4642,31 +4682,31 @@ public TopOperandContext topOperand() { TopOperandContext _localctx = new TopOperandContext(Context, State); EnterRule(_localctx, 80, RULE_topOperand); try { - State = 746; + State = 753; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: EnterOuterAlt(_localctx, 1); { - State = 740; + State = 747; Match(INTEGER_LITERAL); } break; case PARAM: EnterOuterAlt(_localctx, 2); { - State = 741; + State = 748; Match(PARAM); } break; case LPAREN: EnterOuterAlt(_localctx, 3); { - State = 742; + State = 749; Match(LPAREN); - State = 743; + State = 750; expression(0); - State = 744; + State = 751; Match(RPAREN); } break; @@ -4716,13 +4756,13 @@ public SelectListContext selectList() { EnterRule(_localctx, 82, RULE_selectList); int _la; try { - State = 757; + State = 764; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: EnterOuterAlt(_localctx, 1); { - State = 748; + State = 755; Match(STAR); } break; @@ -4750,21 +4790,21 @@ public SelectListContext selectList() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 749; + State = 756; selectItem(); - State = 754; + State = 761; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 750; + State = 757; Match(COMMA); - State = 751; + State = 758; selectItem(); } } - State = 756; + State = 763; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4836,18 +4876,18 @@ public SelectItemContext selectItem() { EnterRule(_localctx, 84, RULE_selectItem); int _la; try { - State = 770; + State = 777; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,91,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,93,Context) ) { case 1: _localctx = new QualifiedStarSelectItemContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 759; + State = 766; ((QualifiedStarSelectItemContext)_localctx).qualifier = identifier(); - State = 760; + State = 767; Match(DOT); - State = 761; + State = 768; Match(STAR); } break; @@ -4855,24 +4895,24 @@ public SelectItemContext selectItem() { _localctx = new ExpressionSelectItemContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 763; + State = 770; expression(0); - State = 768; + State = 775; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 112)) & ~0x3f) == 0 && ((1L << (_la - 112)) & 7L) != 0)) { + if (_la==AS || ((((_la - 113)) & ~0x3f) == 0 && ((1L << (_la - 113)) & 7L) != 0)) { { - State = 765; + State = 772; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 764; + State = 771; Match(AS); } } - State = 767; + State = 774; ((ExpressionSelectItemContext)_localctx).alias = identifier(); } } @@ -4925,23 +4965,23 @@ public FromClauseContext fromClause() { try { EnterOuterAlt(_localctx, 1); { - State = 772; + State = 779; Match(FROM); - State = 773; + State = 780; tableSource(); - State = 778; + State = 785; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 774; + State = 781; Match(COMMA); - State = 775; + State = 782; tableSource(); } } - State = 780; + State = 787; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4989,19 +5029,19 @@ public TableSourceContext tableSource() { try { EnterOuterAlt(_localctx, 1); { - State = 781; + State = 788; tablePrimary(); - State = 785; + State = 792; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 753664L) != 0)) { + while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 1540096L) != 0)) { { { - State = 782; + State = 789; joinClause(); } } - State = 787; + State = 794; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5088,31 +5128,31 @@ public TablePrimaryContext tablePrimary() { EnterRule(_localctx, 90, RULE_tablePrimary); int _la; try { - State = 808; + State = 815; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,98,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,100,Context) ) { case 1: _localctx = new NamedTablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 788; + State = 795; ((NamedTablePrimaryContext)_localctx).table = identifier(); - State = 793; + State = 800; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 112)) & ~0x3f) == 0 && ((1L << (_la - 112)) & 7L) != 0)) { + if (_la==AS || ((((_la - 113)) & ~0x3f) == 0 && ((1L << (_la - 113)) & 7L) != 0)) { { - State = 790; + State = 797; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 789; + State = 796; Match(AS); } } - State = 792; + State = 799; ((NamedTablePrimaryContext)_localctx).alias = identifier(); } } @@ -5123,28 +5163,28 @@ public TablePrimaryContext tablePrimary() { _localctx = new SubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 795; + State = 802; Match(LPAREN); - State = 796; + State = 803; queryExpression(); - State = 797; + State = 804; Match(RPAREN); - State = 802; + State = 809; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 112)) & ~0x3f) == 0 && ((1L << (_la - 112)) & 7L) != 0)) { + if (_la==AS || ((((_la - 113)) & ~0x3f) == 0 && ((1L << (_la - 113)) & 7L) != 0)) { { - State = 799; + State = 806; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 798; + State = 805; Match(AS); } } - State = 801; + State = 808; ((SubqueryPrimaryContext)_localctx).alias = identifier(); } } @@ -5155,11 +5195,11 @@ public TablePrimaryContext tablePrimary() { _localctx = new ParenJoinPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 804; + State = 811; Match(LPAREN); - State = 805; + State = 812; tableSource(); - State = 806; + State = 813; Match(RPAREN); } break; @@ -5208,15 +5248,15 @@ public JoinClauseContext joinClause() { try { EnterOuterAlt(_localctx, 1); { - State = 810; + State = 817; joinType(); - State = 811; + State = 818; Match(JOIN); - State = 812; + State = 819; tablePrimary(); - State = 813; + State = 820; Match(ON); - State = 814; + State = 821; expression(0); } } @@ -5254,6 +5294,17 @@ public override TResult Accept(IParseTreeVisitor visitor) { else return visitor.VisitChildren(this); } } + public partial class FullJoinContext : JoinTypeContext { + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FULL() { return GetToken(AccessSqlParser.FULL, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OUTER() { return GetToken(AccessSqlParser.OUTER, 0); } + public FullJoinContext(JoinTypeContext context) { CopyFrom(context); } + [System.Diagnostics.DebuggerNonUserCode] + public override TResult Accept(IParseTreeVisitor visitor) { + IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor; + if (typedVisitor != null) return typedVisitor.VisitFullJoin(this); + else return visitor.VisitChildren(this); + } + } public partial class LeftJoinContext : JoinTypeContext { [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode LEFT() { return GetToken(AccessSqlParser.LEFT, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OUTER() { return GetToken(AccessSqlParser.OUTER, 0); } @@ -5282,7 +5333,7 @@ public JoinTypeContext joinType() { EnterRule(_localctx, 94, RULE_joinType); int _la; try { - State = 827; + State = 838; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INNER: @@ -5290,12 +5341,12 @@ public JoinTypeContext joinType() { _localctx = new InnerJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 817; + State = 824; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==INNER) { { - State = 816; + State = 823; Match(INNER); } } @@ -5306,14 +5357,14 @@ public JoinTypeContext joinType() { _localctx = new LeftJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 819; + State = 826; Match(LEFT); - State = 821; + State = 828; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 820; + State = 827; Match(OUTER); } } @@ -5324,14 +5375,32 @@ public JoinTypeContext joinType() { _localctx = new RightJoinContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 823; + State = 830; Match(RIGHT); - State = 825; + State = 832; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 824; + State = 831; + Match(OUTER); + } + } + + } + break; + case FULL: + _localctx = new FullJoinContext(_localctx); + EnterOuterAlt(_localctx, 4); + { + State = 834; + Match(FULL); + State = 836; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + if (_la==OUTER) { + { + State = 835; Match(OUTER); } } @@ -5378,9 +5447,9 @@ public WhereClauseContext whereClause() { try { EnterOuterAlt(_localctx, 1); { - State = 829; + State = 840; Match(WHERE); - State = 830; + State = 841; expression(0); } } @@ -5429,25 +5498,25 @@ public OrderByClauseContext orderByClause() { try { EnterOuterAlt(_localctx, 1); { - State = 832; + State = 843; Match(ORDER); - State = 833; + State = 844; Match(BY); - State = 834; + State = 845; orderByItem(); - State = 839; + State = 850; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 835; + State = 846; Match(COMMA); - State = 836; + State = 847; orderByItem(); } } - State = 841; + State = 852; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5492,14 +5561,14 @@ public OrderByItemContext orderByItem() { try { EnterOuterAlt(_localctx, 1); { - State = 842; + State = 853; expression(0); - State = 844; + State = 855; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ASC || _la==DESC) { { - State = 843; + State = 854; _localctx.dir = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -5856,7 +5925,7 @@ private ExpressionContext expression(int _p) { int _alt; EnterOuterAlt(_localctx, 1); { - State = 854; + State = 865; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case NOT: @@ -5865,9 +5934,9 @@ private ExpressionContext expression(int _p) { Context = _localctx; _prevctx = _localctx; - State = 847; + State = 858; Match(NOT); - State = 848; + State = 859; expression(16); } break; @@ -5876,9 +5945,9 @@ private ExpressionContext expression(int _p) { _localctx = new BitNotExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 849; + State = 860; Match(BNOT); - State = 850; + State = 861; expression(15); } break; @@ -5887,9 +5956,9 @@ private ExpressionContext expression(int _p) { _localctx = new NegateExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 851; + State = 862; Match(MINUS); - State = 852; + State = 863; expression(14); } break; @@ -5916,7 +5985,7 @@ private ExpressionContext expression(int _p) { _localctx = new PrimaryExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 853; + State = 864; primary(); } break; @@ -5924,28 +5993,28 @@ private ExpressionContext expression(int _p) { throw new NoViableAltException(this); } Context.Stop = TokenStream.LT(-1); - State = 925; + State = 936; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,113,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,116,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { if ( ParseListeners!=null ) TriggerExitRuleEvent(); _prevctx = _localctx; { - State = 923; + State = 934; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,112,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,115,Context) ) { case 1: { _localctx = new PowExprContext(new ExpressionContext(_parentctx, _parentState)); ((PowExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 856; + State = 867; if (!(Precpred(Context, 13))) throw new FailedPredicateException(this, "Precpred(Context, 13)"); - State = 857; + State = 868; Match(CARET); - State = 858; + State = 869; ((PowExprContext)_localctx).right = expression(14); } break; @@ -5954,19 +6023,19 @@ private ExpressionContext expression(int _p) { _localctx = new MulDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((MulDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 859; + State = 870; if (!(Precpred(Context, 12))) throw new FailedPredicateException(this, "Precpred(Context, 12)"); - State = 860; + State = 871; ((MulDivExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); - if ( !(_la==MOD || ((((_la - 86)) & ~0x3f) == 0 && ((1L << (_la - 86)) & 7L) != 0)) ) { + if ( !(_la==MOD || ((((_la - 87)) & ~0x3f) == 0 && ((1L << (_la - 87)) & 7L) != 0)) ) { ((MulDivExprContext)_localctx).op = ErrorHandler.RecoverInline(this); } else { ErrorHandler.ReportMatch(this); Consume(); } - State = 861; + State = 872; ((MulDivExprContext)_localctx).right = expression(13); } break; @@ -5975,19 +6044,19 @@ private ExpressionContext expression(int _p) { _localctx = new AddConcatExprContext(new ExpressionContext(_parentctx, _parentState)); ((AddConcatExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 862; + State = 873; if (!(Precpred(Context, 11))) throw new FailedPredicateException(this, "Precpred(Context, 11)"); - State = 863; + State = 874; ((AddConcatExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); - if ( !(((((_la - 90)) & ~0x3f) == 0 && ((1L << (_la - 90)) & 7L) != 0)) ) { + if ( !(((((_la - 91)) & ~0x3f) == 0 && ((1L << (_la - 91)) & 7L) != 0)) ) { ((AddConcatExprContext)_localctx).op = ErrorHandler.RecoverInline(this); } else { ErrorHandler.ReportMatch(this); Consume(); } - State = 864; + State = 875; ((AddConcatExprContext)_localctx).right = expression(12); } break; @@ -5996,19 +6065,19 @@ private ExpressionContext expression(int _p) { _localctx = new ComparisonExprContext(new ExpressionContext(_parentctx, _parentState)); ((ComparisonExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 865; + State = 876; if (!(Precpred(Context, 10))) throw new FailedPredicateException(this, "Precpred(Context, 10)"); - State = 866; + State = 877; ((ComparisonExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); - if ( !(((((_la - 93)) & ~0x3f) == 0 && ((1L << (_la - 93)) & 63L) != 0)) ) { + if ( !(((((_la - 94)) & ~0x3f) == 0 && ((1L << (_la - 94)) & 63L) != 0)) ) { ((ComparisonExprContext)_localctx).op = ErrorHandler.RecoverInline(this); } else { ErrorHandler.ReportMatch(this); Consume(); } - State = 867; + State = 878; ((ComparisonExprContext)_localctx).right = expression(11); } break; @@ -6017,25 +6086,25 @@ private ExpressionContext expression(int _p) { _localctx = new BetweenExprContext(new ExpressionContext(_parentctx, _parentState)); ((BetweenExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 868; + State = 879; if (!(Precpred(Context, 9))) throw new FailedPredicateException(this, "Precpred(Context, 9)"); - State = 870; + State = 881; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 869; + State = 880; ((BetweenExprContext)_localctx).not = Match(NOT); } } - State = 872; + State = 883; Match(BETWEEN); - State = 873; + State = 884; ((BetweenExprContext)_localctx).lo = expression(0); - State = 874; + State = 885; Match(AND); - State = 875; + State = 886; ((BetweenExprContext)_localctx).hi = expression(10); } break; @@ -6044,21 +6113,21 @@ private ExpressionContext expression(int _p) { _localctx = new LikeExprContext(new ExpressionContext(_parentctx, _parentState)); ((LikeExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 877; + State = 888; if (!(Precpred(Context, 8))) throw new FailedPredicateException(this, "Precpred(Context, 8)"); - State = 879; + State = 890; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 878; + State = 889; ((LikeExprContext)_localctx).not = Match(NOT); } } - State = 881; + State = 892; Match(LIKE); - State = 882; + State = 893; ((LikeExprContext)_localctx).right = expression(9); } break; @@ -6067,9 +6136,9 @@ private ExpressionContext expression(int _p) { _localctx = new BitwiseExprContext(new ExpressionContext(_parentctx, _parentState)); ((BitwiseExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 883; + State = 894; if (!(Precpred(Context, 4))) throw new FailedPredicateException(this, "Precpred(Context, 4)"); - State = 884; + State = 895; ((BitwiseExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 3584L) != 0)) ) { @@ -6079,7 +6148,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 885; + State = 896; ((BitwiseExprContext)_localctx).right = expression(5); } break; @@ -6088,11 +6157,11 @@ private ExpressionContext expression(int _p) { _localctx = new AndExprContext(new ExpressionContext(_parentctx, _parentState)); ((AndExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 886; + State = 897; if (!(Precpred(Context, 3))) throw new FailedPredicateException(this, "Precpred(Context, 3)"); - State = 887; + State = 898; Match(AND); - State = 888; + State = 899; ((AndExprContext)_localctx).right = expression(4); } break; @@ -6101,11 +6170,11 @@ private ExpressionContext expression(int _p) { _localctx = new OrExprContext(new ExpressionContext(_parentctx, _parentState)); ((OrExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 889; + State = 900; if (!(Precpred(Context, 2))) throw new FailedPredicateException(this, "Precpred(Context, 2)"); - State = 890; + State = 901; Match(OR); - State = 891; + State = 902; ((OrExprContext)_localctx).right = expression(3); } break; @@ -6114,25 +6183,25 @@ private ExpressionContext expression(int _p) { _localctx = new InSubqueryExprContext(new ExpressionContext(_parentctx, _parentState)); ((InSubqueryExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 892; + State = 903; if (!(Precpred(Context, 7))) throw new FailedPredicateException(this, "Precpred(Context, 7)"); - State = 894; + State = 905; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 893; + State = 904; ((InSubqueryExprContext)_localctx).not = Match(NOT); } } - State = 896; + State = 907; Match(IN); - State = 897; + State = 908; Match(LPAREN); - State = 898; + State = 909; ((InSubqueryExprContext)_localctx).sub = selectStatement(); - State = 899; + State = 910; Match(RPAREN); } break; @@ -6141,43 +6210,43 @@ private ExpressionContext expression(int _p) { _localctx = new InExprContext(new ExpressionContext(_parentctx, _parentState)); ((InExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 901; + State = 912; if (!(Precpred(Context, 6))) throw new FailedPredicateException(this, "Precpred(Context, 6)"); - State = 903; + State = 914; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 902; + State = 913; ((InExprContext)_localctx).not = Match(NOT); } } - State = 905; + State = 916; Match(IN); - State = 906; + State = 917; Match(LPAREN); - State = 907; + State = 918; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); - State = 912; + State = 923; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 908; + State = 919; Match(COMMA); - State = 909; + State = 920; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); } } - State = 914; + State = 925; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 915; + State = 926; Match(RPAREN); } break; @@ -6186,30 +6255,30 @@ private ExpressionContext expression(int _p) { _localctx = new IsNullExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsNullExprContext)_localctx).operand = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 917; + State = 928; if (!(Precpred(Context, 5))) throw new FailedPredicateException(this, "Precpred(Context, 5)"); - State = 918; + State = 929; Match(IS); - State = 920; + State = 931; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 919; + State = 930; ((IsNullExprContext)_localctx).not = Match(NOT); } } - State = 922; + State = 933; Match(NULL); } break; } } } - State = 927; + State = 938; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,113,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,116,Context); } } } @@ -6341,14 +6410,14 @@ public PrimaryContext primary() { PrimaryContext _localctx = new PrimaryContext(Context, State); EnterRule(_localctx, 104, RULE_primary); try { - State = 946; + State = 957; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,114,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,117,Context) ) { case 1: _localctx = new LiteralPrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 928; + State = 939; literal(); } break; @@ -6356,7 +6425,7 @@ public PrimaryContext primary() { _localctx = new FunctionCallPrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 929; + State = 940; functionCall(); } break; @@ -6364,7 +6433,7 @@ public PrimaryContext primary() { _localctx = new ColumnPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 930; + State = 941; columnRef(); } break; @@ -6372,7 +6441,7 @@ public PrimaryContext primary() { _localctx = new ParamPrimaryContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 931; + State = 942; Match(PARAM); } break; @@ -6380,7 +6449,7 @@ public PrimaryContext primary() { _localctx = new SystemVariablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 932; + State = 943; Match(SYSVAR); } break; @@ -6388,13 +6457,13 @@ public PrimaryContext primary() { _localctx = new ExistsPrimaryContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 933; + State = 944; Match(EXISTS); - State = 934; + State = 945; Match(LPAREN); - State = 935; + State = 946; selectStatement(); - State = 936; + State = 947; Match(RPAREN); } break; @@ -6402,11 +6471,11 @@ public PrimaryContext primary() { _localctx = new ScalarSubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 938; + State = 949; Match(LPAREN); - State = 939; + State = 950; selectStatement(); - State = 940; + State = 951; Match(RPAREN); } break; @@ -6414,11 +6483,11 @@ public PrimaryContext primary() { _localctx = new ParenPrimaryContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 942; + State = 953; Match(LPAREN); - State = 943; + State = 954; expression(0); - State = 944; + State = 955; Match(RPAREN); } break; @@ -6477,16 +6546,16 @@ public FunctionCallContext functionCall() { try { EnterOuterAlt(_localctx, 1); { - State = 948; + State = 959; _localctx.name = functionName(); - State = 949; + State = 960; Match(LPAREN); - State = 962; + State = 973; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: { - State = 950; + State = 961; _localctx.star = Match(STAR); } break; @@ -6515,31 +6584,31 @@ public FunctionCallContext functionCall() { case IDENTIFIER: { { - State = 952; + State = 963; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==DISTINCT) { { - State = 951; + State = 962; _localctx.distinct = Match(DISTINCT); } } - State = 954; + State = 965; expression(0); - State = 959; + State = 970; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 955; + State = 966; Match(COMMA); - State = 956; + State = 967; expression(0); } } - State = 961; + State = 972; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -6551,7 +6620,7 @@ public FunctionCallContext functionCall() { default: break; } - State = 964; + State = 975; Match(RPAREN); } } @@ -6591,7 +6660,7 @@ public FunctionNameContext functionName() { FunctionNameContext _localctx = new FunctionNameContext(Context, State); EnterRule(_localctx, 108, RULE_functionName); try { - State = 970; + State = 981; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BRACKET_ID: @@ -6599,28 +6668,28 @@ public FunctionNameContext functionName() { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 966; + State = 977; identifier(); } break; case LEFT: EnterOuterAlt(_localctx, 2); { - State = 967; + State = 978; Match(LEFT); } break; case RIGHT: EnterOuterAlt(_localctx, 3); { - State = 968; + State = 979; Match(RIGHT); } break; case ASC: EnterOuterAlt(_localctx, 4); { - State = 969; + State = 980; Match(ASC); } break; @@ -6669,19 +6738,19 @@ public ColumnRefContext columnRef() { try { EnterOuterAlt(_localctx, 1); { - State = 975; + State = 986; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,119,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,122,Context) ) { case 1: { - State = 972; + State = 983; _localctx.qualifier = identifier(); - State = 973; + State = 984; Match(DOT); } break; } - State = 977; + State = 988; _localctx.name = identifier(); } } @@ -6721,9 +6790,9 @@ public IdentifierContext identifier() { try { EnterOuterAlt(_localctx, 1); { - State = 979; + State = 990; _la = TokenStream.LA(1); - if ( !(((((_la - 112)) & ~0x3f) == 0 && ((1L << (_la - 112)) & 7L) != 0)) ) { + if ( !(((((_la - 113)) & ~0x3f) == 0 && ((1L << (_la - 113)) & 7L) != 0)) ) { ErrorHandler.RecoverInline(this); } else { @@ -6851,14 +6920,14 @@ public LiteralContext literal() { LiteralContext _localctx = new LiteralContext(Context, State); EnterRule(_localctx, 114, RULE_literal); try { - State = 990; + State = 1001; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: _localctx = new IntLiteralContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 981; + State = 992; Match(INTEGER_LITERAL); } break; @@ -6866,7 +6935,7 @@ public LiteralContext literal() { _localctx = new NumberLiteralContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 982; + State = 993; Match(NUMBER_LITERAL); } break; @@ -6874,7 +6943,7 @@ public LiteralContext literal() { _localctx = new HexLiteralContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 983; + State = 994; Match(HEX_LITERAL); } break; @@ -6882,7 +6951,7 @@ public LiteralContext literal() { _localctx = new StringLiteralContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 984; + State = 995; Match(STRING_LITERAL); } break; @@ -6890,7 +6959,7 @@ public LiteralContext literal() { _localctx = new DateLiteralContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 985; + State = 996; Match(DATE_LITERAL); } break; @@ -6898,7 +6967,7 @@ public LiteralContext literal() { _localctx = new GuidLiteralContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 986; + State = 997; Match(GUID_LITERAL); } break; @@ -6906,7 +6975,7 @@ public LiteralContext literal() { _localctx = new TrueLiteralContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 987; + State = 998; Match(TRUE); } break; @@ -6914,7 +6983,7 @@ public LiteralContext literal() { _localctx = new FalseLiteralContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 988; + State = 999; Match(FALSE); } break; @@ -6922,7 +6991,7 @@ public LiteralContext literal() { _localctx = new NullLiteralContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 989; + State = 1000; Match(NULL); } break; @@ -6996,21 +7065,21 @@ public TransactionStatementContext transactionStatement() { EnterRule(_localctx, 116, RULE_transactionStatement); int _la; try { - State = 1004; + State = 1015; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BEGIN: _localctx = new BeginTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 992; + State = 1003; Match(BEGIN); - State = 994; + State = 1005; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 993; + State = 1004; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -7028,14 +7097,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new CommitTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 996; + State = 1007; Match(COMMIT); - State = 998; + State = 1009; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 997; + State = 1008; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -7053,14 +7122,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new RollbackTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1000; + State = 1011; Match(ROLLBACK); - State = 1002; + State = 1013; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1001; + State = 1012; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -7114,9 +7183,9 @@ public StandaloneExpressionContext standaloneExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 1006; + State = 1017; expression(0); - State = 1007; + State = 1018; Match(Eof); } } @@ -7156,7 +7225,7 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { } private static int[] _serializedATN = { - 4,1,117,1010,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2, + 4,1,118,1021,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2, 7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14, 2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21, 2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28, @@ -7206,318 +7275,324 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { 12,28,615,9,28,1,29,1,29,1,29,1,29,1,29,1,29,3,29,623,8,29,1,30,1,30,1, 30,1,30,1,30,1,30,1,30,1,30,3,30,633,8,30,1,31,1,31,1,31,1,31,1,31,1,31, 1,31,5,31,642,8,31,10,31,12,31,645,9,31,1,31,1,31,3,31,649,8,31,1,31,1, - 31,1,31,1,31,1,31,5,31,656,8,31,10,31,12,31,659,9,31,1,31,1,31,1,31,1, - 31,3,31,665,8,31,1,32,1,32,1,32,1,32,5,32,671,8,32,10,32,12,32,674,9,32, - 1,33,1,33,1,33,1,33,1,33,3,33,681,8,33,1,34,1,34,3,34,685,8,34,1,34,1, - 34,3,34,689,8,34,1,35,1,35,3,35,693,8,35,1,35,3,35,696,8,35,1,35,1,35, - 3,35,700,8,35,1,35,3,35,703,8,35,1,35,3,35,706,8,35,1,35,3,35,709,8,35, - 1,35,3,35,712,8,35,1,36,1,36,1,37,1,37,1,37,1,37,1,37,5,37,721,8,37,10, - 37,12,37,724,9,37,1,38,1,38,1,38,1,39,1,39,1,39,1,39,5,39,733,8,39,10, - 39,12,39,736,9,39,1,39,3,39,739,8,39,1,40,1,40,1,40,1,40,1,40,1,40,3,40, - 747,8,40,1,41,1,41,1,41,1,41,5,41,753,8,41,10,41,12,41,756,9,41,3,41,758, - 8,41,1,42,1,42,1,42,1,42,1,42,1,42,3,42,766,8,42,1,42,3,42,769,8,42,3, - 42,771,8,42,1,43,1,43,1,43,1,43,5,43,777,8,43,10,43,12,43,780,9,43,1,44, - 1,44,5,44,784,8,44,10,44,12,44,787,9,44,1,45,1,45,3,45,791,8,45,1,45,3, - 45,794,8,45,1,45,1,45,1,45,1,45,3,45,800,8,45,1,45,3,45,803,8,45,1,45, - 1,45,1,45,1,45,3,45,809,8,45,1,46,1,46,1,46,1,46,1,46,1,46,1,47,3,47,818, - 8,47,1,47,1,47,3,47,822,8,47,1,47,1,47,3,47,826,8,47,3,47,828,8,47,1,48, - 1,48,1,48,1,49,1,49,1,49,1,49,1,49,5,49,838,8,49,10,49,12,49,841,9,49, - 1,50,1,50,3,50,845,8,50,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,3,51,855, + 31,1,31,1,31,1,31,5,31,656,8,31,10,31,12,31,659,9,31,1,31,1,31,1,31,3, + 31,664,8,31,1,31,1,31,3,31,668,8,31,1,32,1,32,1,32,1,32,5,32,674,8,32, + 10,32,12,32,677,9,32,1,33,1,33,1,33,1,33,1,33,3,33,684,8,33,1,34,1,34, + 3,34,688,8,34,1,34,1,34,3,34,692,8,34,1,35,1,35,3,35,696,8,35,1,35,3,35, + 699,8,35,1,35,1,35,1,35,3,35,704,8,35,1,35,3,35,707,8,35,1,35,3,35,710, + 8,35,1,35,3,35,713,8,35,1,35,3,35,716,8,35,1,35,3,35,719,8,35,1,36,1,36, + 1,37,1,37,1,37,1,37,1,37,5,37,728,8,37,10,37,12,37,731,9,37,1,38,1,38, + 1,38,1,39,1,39,1,39,1,39,5,39,740,8,39,10,39,12,39,743,9,39,1,39,3,39, + 746,8,39,1,40,1,40,1,40,1,40,1,40,1,40,3,40,754,8,40,1,41,1,41,1,41,1, + 41,5,41,760,8,41,10,41,12,41,763,9,41,3,41,765,8,41,1,42,1,42,1,42,1,42, + 1,42,1,42,3,42,773,8,42,1,42,3,42,776,8,42,3,42,778,8,42,1,43,1,43,1,43, + 1,43,5,43,784,8,43,10,43,12,43,787,9,43,1,44,1,44,5,44,791,8,44,10,44, + 12,44,794,9,44,1,45,1,45,3,45,798,8,45,1,45,3,45,801,8,45,1,45,1,45,1, + 45,1,45,3,45,807,8,45,1,45,3,45,810,8,45,1,45,1,45,1,45,1,45,3,45,816, + 8,45,1,46,1,46,1,46,1,46,1,46,1,46,1,47,3,47,825,8,47,1,47,1,47,3,47,829, + 8,47,1,47,1,47,3,47,833,8,47,1,47,1,47,3,47,837,8,47,3,47,839,8,47,1,48, + 1,48,1,48,1,49,1,49,1,49,1,49,1,49,5,49,849,8,49,10,49,12,49,852,9,49, + 1,50,1,50,3,50,856,8,50,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,3,51,866, 8,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51, - 1,51,3,51,871,8,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,3,51,880,8,51,1, - 51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,3,51,895, - 8,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,3,51,904,8,51,1,51,1,51,1,51,1, - 51,1,51,5,51,911,8,51,10,51,12,51,914,9,51,1,51,1,51,1,51,1,51,1,51,3, - 51,921,8,51,1,51,5,51,924,8,51,10,51,12,51,927,9,51,1,52,1,52,1,52,1,52, + 1,51,3,51,882,8,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,3,51,891,8,51,1, + 51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,3,51,906, + 8,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,3,51,915,8,51,1,51,1,51,1,51,1, + 51,1,51,5,51,922,8,51,10,51,12,51,925,9,51,1,51,1,51,1,51,1,51,1,51,3, + 51,932,8,51,1,51,5,51,935,8,51,10,51,12,51,938,9,51,1,52,1,52,1,52,1,52, 1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52, - 3,52,947,8,52,1,53,1,53,1,53,1,53,3,53,953,8,53,1,53,1,53,1,53,5,53,958, - 8,53,10,53,12,53,961,9,53,3,53,963,8,53,1,53,1,53,1,54,1,54,1,54,1,54, - 3,54,971,8,54,1,55,1,55,1,55,3,55,976,8,55,1,55,1,55,1,56,1,56,1,57,1, - 57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,3,57,991,8,57,1,58,1,58,3,58,995, - 8,58,1,58,1,58,3,58,999,8,58,1,58,1,58,3,58,1003,8,58,3,58,1005,8,58,1, - 59,1,59,1,59,1,59,0,1,102,60,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30, - 32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78, - 80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118, - 0,12,1,0,79,80,1,0,81,82,1,0,71,72,1,0,99,100,2,0,30,31,35,35,1,0,90,91, - 2,0,14,14,86,88,1,0,90,92,1,0,93,98,1,0,9,11,1,0,112,114,1,0,43,44,1152, - 0,121,1,0,0,0,2,144,1,0,0,0,4,166,1,0,0,0,6,168,1,0,0,0,8,180,1,0,0,0, - 10,194,1,0,0,0,12,198,1,0,0,0,14,211,1,0,0,0,16,220,1,0,0,0,18,227,1,0, - 0,0,20,238,1,0,0,0,22,262,1,0,0,0,24,281,1,0,0,0,26,309,1,0,0,0,28,311, - 1,0,0,0,30,316,1,0,0,0,32,318,1,0,0,0,34,380,1,0,0,0,36,385,1,0,0,0,38, - 387,1,0,0,0,40,424,1,0,0,0,42,426,1,0,0,0,44,435,1,0,0,0,46,437,1,0,0, - 0,48,445,1,0,0,0,50,463,1,0,0,0,52,519,1,0,0,0,54,604,1,0,0,0,56,613,1, - 0,0,0,58,622,1,0,0,0,60,632,1,0,0,0,62,634,1,0,0,0,64,666,1,0,0,0,66,680, - 1,0,0,0,68,688,1,0,0,0,70,690,1,0,0,0,72,713,1,0,0,0,74,715,1,0,0,0,76, - 725,1,0,0,0,78,728,1,0,0,0,80,746,1,0,0,0,82,757,1,0,0,0,84,770,1,0,0, - 0,86,772,1,0,0,0,88,781,1,0,0,0,90,808,1,0,0,0,92,810,1,0,0,0,94,827,1, - 0,0,0,96,829,1,0,0,0,98,832,1,0,0,0,100,842,1,0,0,0,102,854,1,0,0,0,104, - 946,1,0,0,0,106,948,1,0,0,0,108,970,1,0,0,0,110,975,1,0,0,0,112,979,1, - 0,0,0,114,990,1,0,0,0,116,1004,1,0,0,0,118,1006,1,0,0,0,120,122,3,18,9, - 0,121,120,1,0,0,0,121,122,1,0,0,0,122,137,1,0,0,0,123,138,3,2,1,0,124, + 3,52,958,8,52,1,53,1,53,1,53,1,53,3,53,964,8,53,1,53,1,53,1,53,5,53,969, + 8,53,10,53,12,53,972,9,53,3,53,974,8,53,1,53,1,53,1,54,1,54,1,54,1,54, + 3,54,982,8,54,1,55,1,55,1,55,3,55,987,8,55,1,55,1,55,1,56,1,56,1,57,1, + 57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,3,57,1002,8,57,1,58,1,58,3,58,1006, + 8,58,1,58,1,58,3,58,1010,8,58,1,58,1,58,3,58,1014,8,58,3,58,1016,8,58, + 1,59,1,59,1,59,1,59,0,1,102,60,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28, + 30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76, + 78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118, + 0,12,1,0,80,81,1,0,82,83,1,0,72,73,1,0,100,101,2,0,31,32,36,36,1,0,91, + 92,2,0,14,14,87,89,1,0,91,93,1,0,94,99,1,0,9,11,1,0,113,115,1,0,44,45, + 1167,0,121,1,0,0,0,2,144,1,0,0,0,4,166,1,0,0,0,6,168,1,0,0,0,8,180,1,0, + 0,0,10,194,1,0,0,0,12,198,1,0,0,0,14,211,1,0,0,0,16,220,1,0,0,0,18,227, + 1,0,0,0,20,238,1,0,0,0,22,262,1,0,0,0,24,281,1,0,0,0,26,309,1,0,0,0,28, + 311,1,0,0,0,30,316,1,0,0,0,32,318,1,0,0,0,34,380,1,0,0,0,36,385,1,0,0, + 0,38,387,1,0,0,0,40,424,1,0,0,0,42,426,1,0,0,0,44,435,1,0,0,0,46,437,1, + 0,0,0,48,445,1,0,0,0,50,463,1,0,0,0,52,519,1,0,0,0,54,604,1,0,0,0,56,613, + 1,0,0,0,58,622,1,0,0,0,60,632,1,0,0,0,62,634,1,0,0,0,64,669,1,0,0,0,66, + 683,1,0,0,0,68,691,1,0,0,0,70,693,1,0,0,0,72,720,1,0,0,0,74,722,1,0,0, + 0,76,732,1,0,0,0,78,735,1,0,0,0,80,753,1,0,0,0,82,764,1,0,0,0,84,777,1, + 0,0,0,86,779,1,0,0,0,88,788,1,0,0,0,90,815,1,0,0,0,92,817,1,0,0,0,94,838, + 1,0,0,0,96,840,1,0,0,0,98,843,1,0,0,0,100,853,1,0,0,0,102,865,1,0,0,0, + 104,957,1,0,0,0,106,959,1,0,0,0,108,981,1,0,0,0,110,986,1,0,0,0,112,990, + 1,0,0,0,114,1001,1,0,0,0,116,1015,1,0,0,0,118,1017,1,0,0,0,120,122,3,18, + 9,0,121,120,1,0,0,0,121,122,1,0,0,0,122,137,1,0,0,0,123,138,3,2,1,0,124, 138,3,20,10,0,125,138,3,38,19,0,126,138,3,22,11,0,127,138,3,24,12,0,128, 138,3,32,16,0,129,138,3,40,20,0,130,138,3,62,31,0,131,138,3,8,4,0,132, 138,3,12,6,0,133,138,3,116,58,0,134,138,3,6,3,0,135,138,3,14,7,0,136,138, 3,64,32,0,137,123,1,0,0,0,137,124,1,0,0,0,137,125,1,0,0,0,137,126,1,0, 0,0,137,127,1,0,0,0,137,128,1,0,0,0,137,129,1,0,0,0,137,130,1,0,0,0,137, 131,1,0,0,0,137,132,1,0,0,0,137,133,1,0,0,0,137,134,1,0,0,0,137,135,1, - 0,0,0,137,136,1,0,0,0,138,140,1,0,0,0,139,141,5,103,0,0,140,139,1,0,0, + 0,0,0,137,136,1,0,0,0,138,140,1,0,0,0,139,141,5,104,0,0,140,139,1,0,0, 0,140,141,1,0,0,0,141,142,1,0,0,0,142,143,5,0,0,1,143,1,1,0,0,0,144,146, - 5,28,0,0,145,147,5,8,0,0,146,145,1,0,0,0,146,147,1,0,0,0,147,148,1,0,0, - 0,148,149,5,27,0,0,149,150,5,99,0,0,150,151,3,70,35,0,151,152,5,100,0, - 0,152,153,5,29,0,0,153,154,3,4,2,0,154,3,1,0,0,0,155,167,3,20,10,0,156, + 5,29,0,0,145,147,5,8,0,0,146,145,1,0,0,0,146,147,1,0,0,0,147,148,1,0,0, + 0,148,149,5,28,0,0,149,150,5,100,0,0,150,151,3,70,35,0,151,152,5,101,0, + 0,152,153,5,30,0,0,153,154,3,4,2,0,154,3,1,0,0,0,155,167,3,20,10,0,156, 167,3,38,19,0,157,167,3,22,11,0,158,167,3,24,12,0,159,167,3,32,16,0,160, 167,3,40,20,0,161,167,3,62,31,0,162,167,3,8,4,0,163,167,3,12,6,0,164,167, 3,6,3,0,165,167,3,64,32,0,166,155,1,0,0,0,166,156,1,0,0,0,166,157,1,0, 0,0,166,158,1,0,0,0,166,159,1,0,0,0,166,160,1,0,0,0,166,161,1,0,0,0,166, 162,1,0,0,0,166,163,1,0,0,0,166,164,1,0,0,0,166,165,1,0,0,0,167,5,1,0, - 0,0,168,169,7,0,0,0,169,178,3,112,56,0,170,175,3,102,51,0,171,172,5,101, + 0,0,168,169,7,0,0,0,169,178,3,112,56,0,170,175,3,102,51,0,171,172,5,102, 0,0,172,174,3,102,51,0,173,171,1,0,0,0,174,177,1,0,0,0,175,173,1,0,0,0, 175,176,1,0,0,0,176,179,1,0,0,0,177,175,1,0,0,0,178,170,1,0,0,0,178,179, - 1,0,0,0,179,7,1,0,0,0,180,181,5,60,0,0,181,182,3,88,44,0,182,183,5,64, - 0,0,183,188,3,10,5,0,184,185,5,101,0,0,185,187,3,10,5,0,186,184,1,0,0, + 1,0,0,0,179,7,1,0,0,0,180,181,5,61,0,0,181,182,3,88,44,0,182,183,5,65, + 0,0,183,188,3,10,5,0,184,185,5,102,0,0,185,187,3,10,5,0,186,184,1,0,0, 0,187,190,1,0,0,0,188,186,1,0,0,0,188,189,1,0,0,0,189,192,1,0,0,0,190, 188,1,0,0,0,191,193,3,96,48,0,192,191,1,0,0,0,192,193,1,0,0,0,193,9,1, - 0,0,0,194,195,3,110,55,0,195,196,5,93,0,0,196,197,3,102,51,0,197,11,1, - 0,0,0,198,204,5,59,0,0,199,200,3,112,56,0,200,201,5,102,0,0,201,202,5, - 86,0,0,202,205,1,0,0,0,203,205,5,86,0,0,204,199,1,0,0,0,204,203,1,0,0, + 0,0,0,194,195,3,110,55,0,195,196,5,94,0,0,196,197,3,102,51,0,197,11,1, + 0,0,0,198,204,5,60,0,0,199,200,3,112,56,0,200,201,5,103,0,0,201,202,5, + 87,0,0,202,205,1,0,0,0,203,205,5,87,0,0,204,199,1,0,0,0,204,203,1,0,0, 0,204,205,1,0,0,0,205,206,1,0,0,0,206,207,5,2,0,0,207,209,3,88,44,0,208, 210,3,96,48,0,209,208,1,0,0,0,209,210,1,0,0,0,210,13,1,0,0,0,211,212,5, - 1,0,0,212,217,3,16,8,0,213,214,5,101,0,0,214,216,3,16,8,0,215,213,1,0, + 1,0,0,212,217,3,16,8,0,213,214,5,102,0,0,214,216,3,16,8,0,215,213,1,0, 0,0,216,219,1,0,0,0,217,215,1,0,0,0,217,218,1,0,0,0,218,15,1,0,0,0,219, - 217,1,0,0,0,220,225,5,104,0,0,221,223,5,5,0,0,222,221,1,0,0,0,222,223, + 217,1,0,0,0,220,225,5,105,0,0,221,223,5,5,0,0,222,221,1,0,0,0,222,223, 1,0,0,0,223,224,1,0,0,0,224,226,3,112,56,0,225,222,1,0,0,0,225,226,1,0, - 0,0,226,17,1,0,0,0,227,228,5,78,0,0,228,233,3,28,14,0,229,230,5,101,0, + 0,0,226,17,1,0,0,0,227,228,5,79,0,0,228,233,3,28,14,0,229,230,5,102,0, 0,230,232,3,28,14,0,231,229,1,0,0,0,232,235,1,0,0,0,233,231,1,0,0,0,233, - 234,1,0,0,0,234,236,1,0,0,0,235,233,1,0,0,0,236,237,5,103,0,0,237,19,1, - 0,0,0,238,240,5,38,0,0,239,241,5,69,0,0,240,239,1,0,0,0,240,241,1,0,0, - 0,241,242,1,0,0,0,242,243,5,39,0,0,243,244,3,112,56,0,244,245,5,99,0,0, - 245,250,3,46,23,0,246,247,5,101,0,0,247,249,3,46,23,0,248,246,1,0,0,0, - 249,252,1,0,0,0,250,248,1,0,0,0,250,251,1,0,0,0,251,257,1,0,0,0,252,250, - 1,0,0,0,253,254,5,101,0,0,254,256,3,54,27,0,255,253,1,0,0,0,256,259,1, - 0,0,0,257,255,1,0,0,0,257,258,1,0,0,0,258,260,1,0,0,0,259,257,1,0,0,0, - 260,261,5,100,0,0,261,21,1,0,0,0,262,263,5,38,0,0,263,264,5,76,0,0,264, - 276,3,112,56,0,265,266,5,99,0,0,266,271,3,112,56,0,267,268,5,101,0,0,268, - 270,3,112,56,0,269,267,1,0,0,0,270,273,1,0,0,0,271,269,1,0,0,0,271,272, - 1,0,0,0,272,274,1,0,0,0,273,271,1,0,0,0,274,275,5,100,0,0,275,277,1,0, - 0,0,276,265,1,0,0,0,276,277,1,0,0,0,277,278,1,0,0,0,278,279,5,5,0,0,279, - 280,3,64,32,0,280,23,1,0,0,0,281,282,5,38,0,0,282,283,5,77,0,0,283,285, - 3,112,56,0,284,286,3,26,13,0,285,284,1,0,0,0,285,286,1,0,0,0,286,287,1, - 0,0,0,287,288,5,5,0,0,288,289,3,36,18,0,289,25,1,0,0,0,290,291,5,99,0, - 0,291,296,3,28,14,0,292,293,5,101,0,0,293,295,3,28,14,0,294,292,1,0,0, - 0,295,298,1,0,0,0,296,294,1,0,0,0,296,297,1,0,0,0,297,299,1,0,0,0,298, - 296,1,0,0,0,299,300,5,100,0,0,300,310,1,0,0,0,301,306,3,28,14,0,302,303, - 5,101,0,0,303,305,3,28,14,0,304,302,1,0,0,0,305,308,1,0,0,0,306,304,1, + 234,1,0,0,0,234,236,1,0,0,0,235,233,1,0,0,0,236,237,5,104,0,0,237,19,1, + 0,0,0,238,240,5,39,0,0,239,241,5,70,0,0,240,239,1,0,0,0,240,241,1,0,0, + 0,241,242,1,0,0,0,242,243,5,40,0,0,243,244,3,112,56,0,244,245,5,100,0, + 0,245,250,3,46,23,0,246,247,5,102,0,0,247,249,3,46,23,0,248,246,1,0,0, + 0,249,252,1,0,0,0,250,248,1,0,0,0,250,251,1,0,0,0,251,257,1,0,0,0,252, + 250,1,0,0,0,253,254,5,102,0,0,254,256,3,54,27,0,255,253,1,0,0,0,256,259, + 1,0,0,0,257,255,1,0,0,0,257,258,1,0,0,0,258,260,1,0,0,0,259,257,1,0,0, + 0,260,261,5,101,0,0,261,21,1,0,0,0,262,263,5,39,0,0,263,264,5,77,0,0,264, + 276,3,112,56,0,265,266,5,100,0,0,266,271,3,112,56,0,267,268,5,102,0,0, + 268,270,3,112,56,0,269,267,1,0,0,0,270,273,1,0,0,0,271,269,1,0,0,0,271, + 272,1,0,0,0,272,274,1,0,0,0,273,271,1,0,0,0,274,275,5,101,0,0,275,277, + 1,0,0,0,276,265,1,0,0,0,276,277,1,0,0,0,277,278,1,0,0,0,278,279,5,5,0, + 0,279,280,3,64,32,0,280,23,1,0,0,0,281,282,5,39,0,0,282,283,5,78,0,0,283, + 285,3,112,56,0,284,286,3,26,13,0,285,284,1,0,0,0,285,286,1,0,0,0,286,287, + 1,0,0,0,287,288,5,5,0,0,288,289,3,36,18,0,289,25,1,0,0,0,290,291,5,100, + 0,0,291,296,3,28,14,0,292,293,5,102,0,0,293,295,3,28,14,0,294,292,1,0, + 0,0,295,298,1,0,0,0,296,294,1,0,0,0,296,297,1,0,0,0,297,299,1,0,0,0,298, + 296,1,0,0,0,299,300,5,101,0,0,300,310,1,0,0,0,301,306,3,28,14,0,302,303, + 5,102,0,0,303,305,3,28,14,0,304,302,1,0,0,0,305,308,1,0,0,0,306,304,1, 0,0,0,306,307,1,0,0,0,307,310,1,0,0,0,308,306,1,0,0,0,309,290,1,0,0,0, 309,301,1,0,0,0,310,27,1,0,0,0,311,312,3,30,15,0,312,313,3,48,24,0,313, - 29,1,0,0,0,314,317,3,112,56,0,315,317,5,105,0,0,316,314,1,0,0,0,316,315, - 1,0,0,0,317,31,1,0,0,0,318,319,5,45,0,0,319,320,5,39,0,0,320,321,3,112, - 56,0,321,322,3,34,17,0,322,33,1,0,0,0,323,325,5,48,0,0,324,326,5,50,0, + 29,1,0,0,0,314,317,3,112,56,0,315,317,5,106,0,0,316,314,1,0,0,0,316,315, + 1,0,0,0,317,31,1,0,0,0,318,319,5,46,0,0,319,320,5,40,0,0,320,321,3,112, + 56,0,321,322,3,34,17,0,322,33,1,0,0,0,323,325,5,49,0,0,324,326,5,51,0, 0,325,324,1,0,0,0,325,326,1,0,0,0,326,327,1,0,0,0,327,381,3,46,23,0,328, - 329,5,48,0,0,329,381,3,54,27,0,330,332,5,45,0,0,331,333,5,50,0,0,332,331, + 329,5,49,0,0,329,381,3,54,27,0,330,332,5,46,0,0,331,333,5,51,0,0,332,331, 1,0,0,0,332,333,1,0,0,0,333,334,1,0,0,0,334,335,3,112,56,0,335,339,3,48, 24,0,336,338,3,52,26,0,337,336,1,0,0,0,338,341,1,0,0,0,339,337,1,0,0,0, - 339,340,1,0,0,0,340,381,1,0,0,0,341,339,1,0,0,0,342,344,5,45,0,0,343,345, - 5,50,0,0,344,343,1,0,0,0,344,345,1,0,0,0,345,346,1,0,0,0,346,347,3,112, - 56,0,347,348,5,64,0,0,348,349,5,65,0,0,349,350,3,102,51,0,350,381,1,0, - 0,0,351,353,5,45,0,0,352,354,5,50,0,0,353,352,1,0,0,0,353,354,1,0,0,0, - 354,355,1,0,0,0,355,356,3,112,56,0,356,357,5,49,0,0,357,358,5,65,0,0,358, - 381,1,0,0,0,359,360,5,49,0,0,360,361,5,50,0,0,361,381,3,112,56,0,362,363, - 5,49,0,0,363,364,5,56,0,0,364,381,3,112,56,0,365,366,5,46,0,0,366,367, - 5,47,0,0,367,381,3,112,56,0,368,369,5,46,0,0,369,370,5,50,0,0,370,371, - 3,112,56,0,371,372,5,47,0,0,372,373,3,112,56,0,373,381,1,0,0,0,374,375, - 5,46,0,0,375,376,5,68,0,0,376,377,3,112,56,0,377,378,5,47,0,0,378,379, + 339,340,1,0,0,0,340,381,1,0,0,0,341,339,1,0,0,0,342,344,5,46,0,0,343,345, + 5,51,0,0,344,343,1,0,0,0,344,345,1,0,0,0,345,346,1,0,0,0,346,347,3,112, + 56,0,347,348,5,65,0,0,348,349,5,66,0,0,349,350,3,102,51,0,350,381,1,0, + 0,0,351,353,5,46,0,0,352,354,5,51,0,0,353,352,1,0,0,0,353,354,1,0,0,0, + 354,355,1,0,0,0,355,356,3,112,56,0,356,357,5,50,0,0,357,358,5,66,0,0,358, + 381,1,0,0,0,359,360,5,50,0,0,360,361,5,51,0,0,361,381,3,112,56,0,362,363, + 5,50,0,0,363,364,5,57,0,0,364,381,3,112,56,0,365,366,5,47,0,0,366,367, + 5,48,0,0,367,381,3,112,56,0,368,369,5,47,0,0,369,370,5,51,0,0,370,371, + 3,112,56,0,371,372,5,48,0,0,372,373,3,112,56,0,373,381,1,0,0,0,374,375, + 5,47,0,0,375,376,5,69,0,0,376,377,3,112,56,0,377,378,5,48,0,0,378,379, 3,112,56,0,379,381,1,0,0,0,380,323,1,0,0,0,380,328,1,0,0,0,380,330,1,0, 0,0,380,342,1,0,0,0,380,351,1,0,0,0,380,359,1,0,0,0,380,362,1,0,0,0,380, 365,1,0,0,0,380,368,1,0,0,0,380,374,1,0,0,0,381,35,1,0,0,0,382,386,3,64, 32,0,383,386,3,62,31,0,384,386,3,20,10,0,385,382,1,0,0,0,385,383,1,0,0, - 0,385,384,1,0,0,0,386,37,1,0,0,0,387,389,5,38,0,0,388,390,5,67,0,0,389, - 388,1,0,0,0,389,390,1,0,0,0,390,391,1,0,0,0,391,392,5,68,0,0,392,393,3, - 112,56,0,393,394,5,21,0,0,394,395,3,112,56,0,395,396,5,99,0,0,396,401, - 3,42,21,0,397,398,5,101,0,0,398,400,3,42,21,0,399,397,1,0,0,0,400,403, + 0,385,384,1,0,0,0,386,37,1,0,0,0,387,389,5,39,0,0,388,390,5,68,0,0,389, + 388,1,0,0,0,389,390,1,0,0,0,390,391,1,0,0,0,391,392,5,69,0,0,392,393,3, + 112,56,0,393,394,5,22,0,0,394,395,3,112,56,0,395,396,5,100,0,0,396,401, + 3,42,21,0,397,398,5,102,0,0,398,400,3,42,21,0,399,397,1,0,0,0,400,403, 1,0,0,0,401,399,1,0,0,0,401,402,1,0,0,0,402,404,1,0,0,0,403,401,1,0,0, - 0,404,407,5,100,0,0,405,406,5,70,0,0,406,408,3,44,22,0,407,405,1,0,0,0, - 407,408,1,0,0,0,408,39,1,0,0,0,409,410,5,49,0,0,410,411,5,39,0,0,411,425, - 3,112,56,0,412,413,5,49,0,0,413,414,5,68,0,0,414,415,3,112,56,0,415,416, - 5,21,0,0,416,417,3,112,56,0,417,425,1,0,0,0,418,419,5,49,0,0,419,420,5, - 77,0,0,420,425,3,112,56,0,421,422,5,49,0,0,422,423,5,76,0,0,423,425,3, + 0,404,407,5,101,0,0,405,406,5,71,0,0,406,408,3,44,22,0,407,405,1,0,0,0, + 407,408,1,0,0,0,408,39,1,0,0,0,409,410,5,50,0,0,410,411,5,40,0,0,411,425, + 3,112,56,0,412,413,5,50,0,0,413,414,5,69,0,0,414,415,3,112,56,0,415,416, + 5,22,0,0,416,417,3,112,56,0,417,425,1,0,0,0,418,419,5,50,0,0,419,420,5, + 78,0,0,420,425,3,112,56,0,421,422,5,50,0,0,422,423,5,77,0,0,423,425,3, 112,56,0,424,409,1,0,0,0,424,412,1,0,0,0,424,418,1,0,0,0,424,421,1,0,0, 0,425,41,1,0,0,0,426,428,3,112,56,0,427,429,7,1,0,0,428,427,1,0,0,0,428, - 429,1,0,0,0,429,43,1,0,0,0,430,436,5,54,0,0,431,432,5,73,0,0,432,436,5, - 85,0,0,433,434,5,74,0,0,434,436,5,85,0,0,435,430,1,0,0,0,435,431,1,0,0, + 429,1,0,0,0,429,43,1,0,0,0,430,436,5,55,0,0,431,432,5,74,0,0,432,436,5, + 86,0,0,433,434,5,75,0,0,434,436,5,86,0,0,435,430,1,0,0,0,435,431,1,0,0, 0,435,433,1,0,0,0,436,45,1,0,0,0,437,438,3,112,56,0,438,442,3,48,24,0, 439,441,3,52,26,0,440,439,1,0,0,0,441,444,1,0,0,0,442,440,1,0,0,0,442, 443,1,0,0,0,443,47,1,0,0,0,444,442,1,0,0,0,445,447,3,112,56,0,446,448, 3,112,56,0,447,446,1,0,0,0,447,448,1,0,0,0,448,450,1,0,0,0,449,451,3,112, - 56,0,450,449,1,0,0,0,450,451,1,0,0,0,451,460,1,0,0,0,452,453,5,99,0,0, - 453,456,3,50,25,0,454,455,5,101,0,0,455,457,3,50,25,0,456,454,1,0,0,0, - 456,457,1,0,0,0,457,458,1,0,0,0,458,459,5,100,0,0,459,461,1,0,0,0,460, - 452,1,0,0,0,460,461,1,0,0,0,461,49,1,0,0,0,462,464,5,91,0,0,463,462,1, - 0,0,0,463,464,1,0,0,0,464,465,1,0,0,0,465,466,5,107,0,0,466,51,1,0,0,0, - 467,468,5,8,0,0,468,520,5,85,0,0,469,520,5,85,0,0,470,471,5,65,0,0,471, - 520,3,102,51,0,472,473,5,70,0,0,473,520,7,2,0,0,474,475,5,56,0,0,475,477, - 3,112,56,0,476,474,1,0,0,0,476,477,1,0,0,0,477,478,1,0,0,0,478,479,5,75, - 0,0,479,480,5,99,0,0,480,481,3,56,28,0,481,482,5,100,0,0,482,520,1,0,0, - 0,483,484,5,56,0,0,484,486,3,112,56,0,485,483,1,0,0,0,485,486,1,0,0,0, - 486,487,1,0,0,0,487,488,5,54,0,0,488,520,5,55,0,0,489,490,5,56,0,0,490, + 56,0,450,449,1,0,0,0,450,451,1,0,0,0,451,460,1,0,0,0,452,453,5,100,0,0, + 453,456,3,50,25,0,454,455,5,102,0,0,455,457,3,50,25,0,456,454,1,0,0,0, + 456,457,1,0,0,0,457,458,1,0,0,0,458,459,5,101,0,0,459,461,1,0,0,0,460, + 452,1,0,0,0,460,461,1,0,0,0,461,49,1,0,0,0,462,464,5,92,0,0,463,462,1, + 0,0,0,463,464,1,0,0,0,464,465,1,0,0,0,465,466,5,108,0,0,466,51,1,0,0,0, + 467,468,5,8,0,0,468,520,5,86,0,0,469,520,5,86,0,0,470,471,5,66,0,0,471, + 520,3,102,51,0,472,473,5,71,0,0,473,520,7,2,0,0,474,475,5,57,0,0,475,477, + 3,112,56,0,476,474,1,0,0,0,476,477,1,0,0,0,477,478,1,0,0,0,478,479,5,76, + 0,0,479,480,5,100,0,0,480,481,3,56,28,0,481,482,5,101,0,0,482,520,1,0, + 0,0,483,484,5,57,0,0,484,486,3,112,56,0,485,483,1,0,0,0,485,486,1,0,0, + 0,486,487,1,0,0,0,487,488,5,55,0,0,488,520,5,56,0,0,489,490,5,57,0,0,490, 492,3,112,56,0,491,489,1,0,0,0,491,492,1,0,0,0,492,493,1,0,0,0,493,520, - 5,67,0,0,494,495,5,56,0,0,495,497,3,112,56,0,496,494,1,0,0,0,496,497,1, - 0,0,0,497,498,1,0,0,0,498,499,5,58,0,0,499,511,3,112,56,0,500,501,5,99, - 0,0,501,506,3,112,56,0,502,503,5,101,0,0,503,505,3,112,56,0,504,502,1, + 5,68,0,0,494,495,5,57,0,0,495,497,3,112,56,0,496,494,1,0,0,0,496,497,1, + 0,0,0,497,498,1,0,0,0,498,499,5,59,0,0,499,511,3,112,56,0,500,501,5,100, + 0,0,501,506,3,112,56,0,502,503,5,102,0,0,503,505,3,112,56,0,504,502,1, 0,0,0,505,508,1,0,0,0,506,504,1,0,0,0,506,507,1,0,0,0,507,509,1,0,0,0, - 508,506,1,0,0,0,509,510,5,100,0,0,510,512,1,0,0,0,511,500,1,0,0,0,511, + 508,506,1,0,0,0,509,510,5,101,0,0,510,512,1,0,0,0,511,500,1,0,0,0,511, 512,1,0,0,0,512,516,1,0,0,0,513,515,3,58,29,0,514,513,1,0,0,0,515,518, 1,0,0,0,516,514,1,0,0,0,516,517,1,0,0,0,517,520,1,0,0,0,518,516,1,0,0, 0,519,467,1,0,0,0,519,469,1,0,0,0,519,470,1,0,0,0,519,472,1,0,0,0,519, 476,1,0,0,0,519,485,1,0,0,0,519,491,1,0,0,0,519,496,1,0,0,0,520,53,1,0, - 0,0,521,522,5,56,0,0,522,524,3,112,56,0,523,521,1,0,0,0,523,524,1,0,0, - 0,524,525,1,0,0,0,525,526,5,54,0,0,526,527,5,55,0,0,527,528,5,99,0,0,528, - 533,3,112,56,0,529,530,5,101,0,0,530,532,3,112,56,0,531,529,1,0,0,0,532, - 535,1,0,0,0,533,531,1,0,0,0,533,534,1,0,0,0,534,536,1,0,0,0,535,533,1, - 0,0,0,536,537,5,100,0,0,537,605,1,0,0,0,538,539,5,56,0,0,539,541,3,112, - 56,0,540,538,1,0,0,0,540,541,1,0,0,0,541,542,1,0,0,0,542,543,5,67,0,0, - 543,544,5,99,0,0,544,549,3,112,56,0,545,546,5,101,0,0,546,548,3,112,56, - 0,547,545,1,0,0,0,548,551,1,0,0,0,549,547,1,0,0,0,549,550,1,0,0,0,550, - 552,1,0,0,0,551,549,1,0,0,0,552,553,5,100,0,0,553,605,1,0,0,0,554,555, - 5,56,0,0,555,557,3,112,56,0,556,554,1,0,0,0,556,557,1,0,0,0,557,558,1, - 0,0,0,558,559,5,57,0,0,559,562,5,55,0,0,560,561,5,66,0,0,561,563,5,68, - 0,0,562,560,1,0,0,0,562,563,1,0,0,0,563,564,1,0,0,0,564,565,5,99,0,0,565, - 570,3,112,56,0,566,567,5,101,0,0,567,569,3,112,56,0,568,566,1,0,0,0,569, - 572,1,0,0,0,570,568,1,0,0,0,570,571,1,0,0,0,571,573,1,0,0,0,572,570,1, - 0,0,0,573,574,5,100,0,0,574,575,5,58,0,0,575,587,3,112,56,0,576,577,5, - 99,0,0,577,582,3,112,56,0,578,579,5,101,0,0,579,581,3,112,56,0,580,578, - 1,0,0,0,581,584,1,0,0,0,582,580,1,0,0,0,582,583,1,0,0,0,583,585,1,0,0, - 0,584,582,1,0,0,0,585,586,5,100,0,0,586,588,1,0,0,0,587,576,1,0,0,0,587, - 588,1,0,0,0,588,592,1,0,0,0,589,591,3,58,29,0,590,589,1,0,0,0,591,594, - 1,0,0,0,592,590,1,0,0,0,592,593,1,0,0,0,593,605,1,0,0,0,594,592,1,0,0, - 0,595,596,5,56,0,0,596,598,3,112,56,0,597,595,1,0,0,0,597,598,1,0,0,0, - 598,599,1,0,0,0,599,600,5,75,0,0,600,601,5,99,0,0,601,602,3,56,28,0,602, - 603,5,100,0,0,603,605,1,0,0,0,604,523,1,0,0,0,604,540,1,0,0,0,604,556, - 1,0,0,0,604,597,1,0,0,0,605,55,1,0,0,0,606,612,8,3,0,0,607,608,5,99,0, - 0,608,609,3,56,28,0,609,610,5,100,0,0,610,612,1,0,0,0,611,606,1,0,0,0, - 611,607,1,0,0,0,612,615,1,0,0,0,613,611,1,0,0,0,613,614,1,0,0,0,614,57, - 1,0,0,0,615,613,1,0,0,0,616,617,5,21,0,0,617,618,5,60,0,0,618,623,3,60, - 30,0,619,620,5,21,0,0,620,621,5,59,0,0,621,623,3,60,30,0,622,616,1,0,0, - 0,622,619,1,0,0,0,623,59,1,0,0,0,624,633,5,61,0,0,625,626,5,66,0,0,626, - 633,5,63,0,0,627,633,5,62,0,0,628,629,5,64,0,0,629,633,5,85,0,0,630,631, - 5,64,0,0,631,633,5,65,0,0,632,624,1,0,0,0,632,625,1,0,0,0,632,627,1,0, - 0,0,632,628,1,0,0,0,632,630,1,0,0,0,633,61,1,0,0,0,634,635,5,51,0,0,635, - 636,5,52,0,0,636,664,3,112,56,0,637,638,5,99,0,0,638,643,3,112,56,0,639, - 640,5,101,0,0,640,642,3,112,56,0,641,639,1,0,0,0,642,645,1,0,0,0,643,641, - 1,0,0,0,643,644,1,0,0,0,644,646,1,0,0,0,645,643,1,0,0,0,646,647,5,100, - 0,0,647,649,1,0,0,0,648,637,1,0,0,0,648,649,1,0,0,0,649,650,1,0,0,0,650, - 651,5,53,0,0,651,652,5,99,0,0,652,657,3,102,51,0,653,654,5,101,0,0,654, - 656,3,102,51,0,655,653,1,0,0,0,656,659,1,0,0,0,657,655,1,0,0,0,657,658, - 1,0,0,0,658,660,1,0,0,0,659,657,1,0,0,0,660,661,5,100,0,0,661,665,1,0, - 0,0,662,663,5,65,0,0,663,665,5,53,0,0,664,648,1,0,0,0,664,662,1,0,0,0, - 665,63,1,0,0,0,666,672,3,66,33,0,667,668,3,68,34,0,668,669,3,66,33,0,669, - 671,1,0,0,0,670,667,1,0,0,0,671,674,1,0,0,0,672,670,1,0,0,0,672,673,1, - 0,0,0,673,65,1,0,0,0,674,672,1,0,0,0,675,681,3,70,35,0,676,677,5,99,0, - 0,677,678,3,64,32,0,678,679,5,100,0,0,679,681,1,0,0,0,680,675,1,0,0,0, - 680,676,1,0,0,0,681,67,1,0,0,0,682,684,5,34,0,0,683,685,5,35,0,0,684,683, - 1,0,0,0,684,685,1,0,0,0,685,689,1,0,0,0,686,689,5,36,0,0,687,689,5,37, - 0,0,688,682,1,0,0,0,688,686,1,0,0,0,688,687,1,0,0,0,689,69,1,0,0,0,690, - 692,5,1,0,0,691,693,3,72,36,0,692,691,1,0,0,0,692,693,1,0,0,0,693,695, - 1,0,0,0,694,696,3,78,39,0,695,694,1,0,0,0,695,696,1,0,0,0,696,697,1,0, - 0,0,697,699,3,82,41,0,698,700,3,86,43,0,699,698,1,0,0,0,699,700,1,0,0, - 0,700,702,1,0,0,0,701,703,3,96,48,0,702,701,1,0,0,0,702,703,1,0,0,0,703, - 705,1,0,0,0,704,706,3,74,37,0,705,704,1,0,0,0,705,706,1,0,0,0,706,708, - 1,0,0,0,707,709,3,76,38,0,708,707,1,0,0,0,708,709,1,0,0,0,709,711,1,0, - 0,0,710,712,3,98,49,0,711,710,1,0,0,0,711,712,1,0,0,0,712,71,1,0,0,0,713, - 714,7,4,0,0,714,73,1,0,0,0,715,716,5,23,0,0,716,717,5,25,0,0,717,722,3, - 102,51,0,718,719,5,101,0,0,719,721,3,102,51,0,720,718,1,0,0,0,721,724, - 1,0,0,0,722,720,1,0,0,0,722,723,1,0,0,0,723,75,1,0,0,0,724,722,1,0,0,0, - 725,726,5,26,0,0,726,727,3,102,51,0,727,77,1,0,0,0,728,729,5,4,0,0,729, - 734,3,80,40,0,730,731,7,5,0,0,731,733,3,80,40,0,732,730,1,0,0,0,733,736, - 1,0,0,0,734,732,1,0,0,0,734,735,1,0,0,0,735,738,1,0,0,0,736,734,1,0,0, - 0,737,739,5,32,0,0,738,737,1,0,0,0,738,739,1,0,0,0,739,79,1,0,0,0,740, - 747,5,107,0,0,741,747,5,105,0,0,742,743,5,99,0,0,743,744,3,102,51,0,744, - 745,5,100,0,0,745,747,1,0,0,0,746,740,1,0,0,0,746,741,1,0,0,0,746,742, - 1,0,0,0,747,81,1,0,0,0,748,758,5,86,0,0,749,754,3,84,42,0,750,751,5,101, - 0,0,751,753,3,84,42,0,752,750,1,0,0,0,753,756,1,0,0,0,754,752,1,0,0,0, - 754,755,1,0,0,0,755,758,1,0,0,0,756,754,1,0,0,0,757,748,1,0,0,0,757,749, - 1,0,0,0,758,83,1,0,0,0,759,760,3,112,56,0,760,761,5,102,0,0,761,762,5, - 86,0,0,762,771,1,0,0,0,763,768,3,102,51,0,764,766,5,5,0,0,765,764,1,0, - 0,0,765,766,1,0,0,0,766,767,1,0,0,0,767,769,3,112,56,0,768,765,1,0,0,0, - 768,769,1,0,0,0,769,771,1,0,0,0,770,759,1,0,0,0,770,763,1,0,0,0,771,85, - 1,0,0,0,772,773,5,2,0,0,773,778,3,88,44,0,774,775,5,101,0,0,775,777,3, - 88,44,0,776,774,1,0,0,0,777,780,1,0,0,0,778,776,1,0,0,0,778,779,1,0,0, - 0,779,87,1,0,0,0,780,778,1,0,0,0,781,785,3,90,45,0,782,784,3,92,46,0,783, - 782,1,0,0,0,784,787,1,0,0,0,785,783,1,0,0,0,785,786,1,0,0,0,786,89,1,0, - 0,0,787,785,1,0,0,0,788,793,3,112,56,0,789,791,5,5,0,0,790,789,1,0,0,0, - 790,791,1,0,0,0,791,792,1,0,0,0,792,794,3,112,56,0,793,790,1,0,0,0,793, - 794,1,0,0,0,794,809,1,0,0,0,795,796,5,99,0,0,796,797,3,64,32,0,797,802, - 5,100,0,0,798,800,5,5,0,0,799,798,1,0,0,0,799,800,1,0,0,0,800,801,1,0, - 0,0,801,803,3,112,56,0,802,799,1,0,0,0,802,803,1,0,0,0,803,809,1,0,0,0, - 804,805,5,99,0,0,805,806,3,88,44,0,806,807,5,100,0,0,807,809,1,0,0,0,808, - 788,1,0,0,0,808,795,1,0,0,0,808,804,1,0,0,0,809,91,1,0,0,0,810,811,3,94, - 47,0,811,812,5,19,0,0,812,813,3,90,45,0,813,814,5,21,0,0,814,815,3,102, - 51,0,815,93,1,0,0,0,816,818,5,15,0,0,817,816,1,0,0,0,817,818,1,0,0,0,818, - 828,1,0,0,0,819,821,5,16,0,0,820,822,5,18,0,0,821,820,1,0,0,0,821,822, - 1,0,0,0,822,828,1,0,0,0,823,825,5,17,0,0,824,826,5,18,0,0,825,824,1,0, - 0,0,825,826,1,0,0,0,826,828,1,0,0,0,827,817,1,0,0,0,827,819,1,0,0,0,827, - 823,1,0,0,0,828,95,1,0,0,0,829,830,5,3,0,0,830,831,3,102,51,0,831,97,1, - 0,0,0,832,833,5,22,0,0,833,834,5,25,0,0,834,839,3,100,50,0,835,836,5,101, - 0,0,836,838,3,100,50,0,837,835,1,0,0,0,838,841,1,0,0,0,839,837,1,0,0,0, - 839,840,1,0,0,0,840,99,1,0,0,0,841,839,1,0,0,0,842,844,3,102,51,0,843, - 845,7,1,0,0,844,843,1,0,0,0,844,845,1,0,0,0,845,101,1,0,0,0,846,847,6, - 51,-1,0,847,848,5,8,0,0,848,855,3,102,51,16,849,850,5,12,0,0,850,855,3, - 102,51,15,851,852,5,91,0,0,852,855,3,102,51,14,853,855,3,104,52,0,854, - 846,1,0,0,0,854,849,1,0,0,0,854,851,1,0,0,0,854,853,1,0,0,0,855,925,1, - 0,0,0,856,857,10,13,0,0,857,858,5,89,0,0,858,924,3,102,51,14,859,860,10, - 12,0,0,860,861,7,6,0,0,861,924,3,102,51,13,862,863,10,11,0,0,863,864,7, - 7,0,0,864,924,3,102,51,12,865,866,10,10,0,0,866,867,7,8,0,0,867,924,3, - 102,51,11,868,870,10,9,0,0,869,871,5,8,0,0,870,869,1,0,0,0,870,871,1,0, - 0,0,871,872,1,0,0,0,872,873,5,33,0,0,873,874,3,102,51,0,874,875,5,6,0, - 0,875,876,3,102,51,10,876,924,1,0,0,0,877,879,10,8,0,0,878,880,5,8,0,0, - 879,878,1,0,0,0,879,880,1,0,0,0,880,881,1,0,0,0,881,882,5,13,0,0,882,924, - 3,102,51,9,883,884,10,4,0,0,884,885,7,9,0,0,885,924,3,102,51,5,886,887, - 10,3,0,0,887,888,5,6,0,0,888,924,3,102,51,4,889,890,10,2,0,0,890,891,5, - 7,0,0,891,924,3,102,51,3,892,894,10,7,0,0,893,895,5,8,0,0,894,893,1,0, - 0,0,894,895,1,0,0,0,895,896,1,0,0,0,896,897,5,20,0,0,897,898,5,99,0,0, - 898,899,3,70,35,0,899,900,5,100,0,0,900,924,1,0,0,0,901,903,10,6,0,0,902, - 904,5,8,0,0,903,902,1,0,0,0,903,904,1,0,0,0,904,905,1,0,0,0,905,906,5, - 20,0,0,906,907,5,99,0,0,907,912,3,102,51,0,908,909,5,101,0,0,909,911,3, - 102,51,0,910,908,1,0,0,0,911,914,1,0,0,0,912,910,1,0,0,0,912,913,1,0,0, - 0,913,915,1,0,0,0,914,912,1,0,0,0,915,916,5,100,0,0,916,924,1,0,0,0,917, - 918,10,5,0,0,918,920,5,24,0,0,919,921,5,8,0,0,920,919,1,0,0,0,920,921, - 1,0,0,0,921,922,1,0,0,0,922,924,5,85,0,0,923,856,1,0,0,0,923,859,1,0,0, - 0,923,862,1,0,0,0,923,865,1,0,0,0,923,868,1,0,0,0,923,877,1,0,0,0,923, - 883,1,0,0,0,923,886,1,0,0,0,923,889,1,0,0,0,923,892,1,0,0,0,923,901,1, - 0,0,0,923,917,1,0,0,0,924,927,1,0,0,0,925,923,1,0,0,0,925,926,1,0,0,0, - 926,103,1,0,0,0,927,925,1,0,0,0,928,947,3,114,57,0,929,947,3,106,53,0, - 930,947,3,110,55,0,931,947,5,105,0,0,932,947,5,104,0,0,933,934,5,27,0, - 0,934,935,5,99,0,0,935,936,3,70,35,0,936,937,5,100,0,0,937,947,1,0,0,0, - 938,939,5,99,0,0,939,940,3,70,35,0,940,941,5,100,0,0,941,947,1,0,0,0,942, - 943,5,99,0,0,943,944,3,102,51,0,944,945,5,100,0,0,945,947,1,0,0,0,946, - 928,1,0,0,0,946,929,1,0,0,0,946,930,1,0,0,0,946,931,1,0,0,0,946,932,1, - 0,0,0,946,933,1,0,0,0,946,938,1,0,0,0,946,942,1,0,0,0,947,105,1,0,0,0, - 948,949,3,108,54,0,949,962,5,99,0,0,950,963,5,86,0,0,951,953,5,31,0,0, - 952,951,1,0,0,0,952,953,1,0,0,0,953,954,1,0,0,0,954,959,3,102,51,0,955, - 956,5,101,0,0,956,958,3,102,51,0,957,955,1,0,0,0,958,961,1,0,0,0,959,957, - 1,0,0,0,959,960,1,0,0,0,960,963,1,0,0,0,961,959,1,0,0,0,962,950,1,0,0, - 0,962,952,1,0,0,0,962,963,1,0,0,0,963,964,1,0,0,0,964,965,5,100,0,0,965, - 107,1,0,0,0,966,971,3,112,56,0,967,971,5,16,0,0,968,971,5,17,0,0,969,971, - 5,81,0,0,970,966,1,0,0,0,970,967,1,0,0,0,970,968,1,0,0,0,970,969,1,0,0, - 0,971,109,1,0,0,0,972,973,3,112,56,0,973,974,5,102,0,0,974,976,1,0,0,0, - 975,972,1,0,0,0,975,976,1,0,0,0,976,977,1,0,0,0,977,978,3,112,56,0,978, - 111,1,0,0,0,979,980,7,10,0,0,980,113,1,0,0,0,981,991,5,107,0,0,982,991, - 5,108,0,0,983,991,5,106,0,0,984,991,5,109,0,0,985,991,5,110,0,0,986,991, - 5,111,0,0,987,991,5,83,0,0,988,991,5,84,0,0,989,991,5,85,0,0,990,981,1, - 0,0,0,990,982,1,0,0,0,990,983,1,0,0,0,990,984,1,0,0,0,990,985,1,0,0,0, - 990,986,1,0,0,0,990,987,1,0,0,0,990,988,1,0,0,0,990,989,1,0,0,0,991,115, - 1,0,0,0,992,994,5,40,0,0,993,995,7,11,0,0,994,993,1,0,0,0,994,995,1,0, - 0,0,995,1005,1,0,0,0,996,998,5,41,0,0,997,999,7,11,0,0,998,997,1,0,0,0, - 998,999,1,0,0,0,999,1005,1,0,0,0,1000,1002,5,42,0,0,1001,1003,7,11,0,0, - 1002,1001,1,0,0,0,1002,1003,1,0,0,0,1003,1005,1,0,0,0,1004,992,1,0,0,0, - 1004,996,1,0,0,0,1004,1000,1,0,0,0,1005,117,1,0,0,0,1006,1007,3,102,51, - 0,1007,1008,5,0,0,1,1008,119,1,0,0,0,125,121,137,140,146,166,175,178,188, - 192,204,209,217,222,225,233,240,250,257,271,276,285,296,306,309,316,325, - 332,339,344,353,380,385,389,401,407,424,428,435,442,447,450,456,460,463, - 476,485,491,496,506,511,516,519,523,533,540,549,556,562,570,582,587,592, - 597,604,611,613,622,632,643,648,657,664,672,680,684,688,692,695,699,702, - 705,708,711,722,734,738,746,754,757,765,768,770,778,785,790,793,799,802, - 808,817,821,825,827,839,844,854,870,879,894,903,912,920,923,925,946,952, - 959,962,970,975,990,994,998,1002,1004 + 0,0,521,522,5,57,0,0,522,524,3,112,56,0,523,521,1,0,0,0,523,524,1,0,0, + 0,524,525,1,0,0,0,525,526,5,55,0,0,526,527,5,56,0,0,527,528,5,100,0,0, + 528,533,3,112,56,0,529,530,5,102,0,0,530,532,3,112,56,0,531,529,1,0,0, + 0,532,535,1,0,0,0,533,531,1,0,0,0,533,534,1,0,0,0,534,536,1,0,0,0,535, + 533,1,0,0,0,536,537,5,101,0,0,537,605,1,0,0,0,538,539,5,57,0,0,539,541, + 3,112,56,0,540,538,1,0,0,0,540,541,1,0,0,0,541,542,1,0,0,0,542,543,5,68, + 0,0,543,544,5,100,0,0,544,549,3,112,56,0,545,546,5,102,0,0,546,548,3,112, + 56,0,547,545,1,0,0,0,548,551,1,0,0,0,549,547,1,0,0,0,549,550,1,0,0,0,550, + 552,1,0,0,0,551,549,1,0,0,0,552,553,5,101,0,0,553,605,1,0,0,0,554,555, + 5,57,0,0,555,557,3,112,56,0,556,554,1,0,0,0,556,557,1,0,0,0,557,558,1, + 0,0,0,558,559,5,58,0,0,559,562,5,56,0,0,560,561,5,67,0,0,561,563,5,69, + 0,0,562,560,1,0,0,0,562,563,1,0,0,0,563,564,1,0,0,0,564,565,5,100,0,0, + 565,570,3,112,56,0,566,567,5,102,0,0,567,569,3,112,56,0,568,566,1,0,0, + 0,569,572,1,0,0,0,570,568,1,0,0,0,570,571,1,0,0,0,571,573,1,0,0,0,572, + 570,1,0,0,0,573,574,5,101,0,0,574,575,5,59,0,0,575,587,3,112,56,0,576, + 577,5,100,0,0,577,582,3,112,56,0,578,579,5,102,0,0,579,581,3,112,56,0, + 580,578,1,0,0,0,581,584,1,0,0,0,582,580,1,0,0,0,582,583,1,0,0,0,583,585, + 1,0,0,0,584,582,1,0,0,0,585,586,5,101,0,0,586,588,1,0,0,0,587,576,1,0, + 0,0,587,588,1,0,0,0,588,592,1,0,0,0,589,591,3,58,29,0,590,589,1,0,0,0, + 591,594,1,0,0,0,592,590,1,0,0,0,592,593,1,0,0,0,593,605,1,0,0,0,594,592, + 1,0,0,0,595,596,5,57,0,0,596,598,3,112,56,0,597,595,1,0,0,0,597,598,1, + 0,0,0,598,599,1,0,0,0,599,600,5,76,0,0,600,601,5,100,0,0,601,602,3,56, + 28,0,602,603,5,101,0,0,603,605,1,0,0,0,604,523,1,0,0,0,604,540,1,0,0,0, + 604,556,1,0,0,0,604,597,1,0,0,0,605,55,1,0,0,0,606,612,8,3,0,0,607,608, + 5,100,0,0,608,609,3,56,28,0,609,610,5,101,0,0,610,612,1,0,0,0,611,606, + 1,0,0,0,611,607,1,0,0,0,612,615,1,0,0,0,613,611,1,0,0,0,613,614,1,0,0, + 0,614,57,1,0,0,0,615,613,1,0,0,0,616,617,5,22,0,0,617,618,5,61,0,0,618, + 623,3,60,30,0,619,620,5,22,0,0,620,621,5,60,0,0,621,623,3,60,30,0,622, + 616,1,0,0,0,622,619,1,0,0,0,623,59,1,0,0,0,624,633,5,62,0,0,625,626,5, + 67,0,0,626,633,5,64,0,0,627,633,5,63,0,0,628,629,5,65,0,0,629,633,5,86, + 0,0,630,631,5,65,0,0,631,633,5,66,0,0,632,624,1,0,0,0,632,625,1,0,0,0, + 632,627,1,0,0,0,632,628,1,0,0,0,632,630,1,0,0,0,633,61,1,0,0,0,634,635, + 5,52,0,0,635,636,5,53,0,0,636,667,3,112,56,0,637,638,5,100,0,0,638,643, + 3,112,56,0,639,640,5,102,0,0,640,642,3,112,56,0,641,639,1,0,0,0,642,645, + 1,0,0,0,643,641,1,0,0,0,643,644,1,0,0,0,644,646,1,0,0,0,645,643,1,0,0, + 0,646,647,5,101,0,0,647,649,1,0,0,0,648,637,1,0,0,0,648,649,1,0,0,0,649, + 663,1,0,0,0,650,651,5,54,0,0,651,652,5,100,0,0,652,657,3,102,51,0,653, + 654,5,102,0,0,654,656,3,102,51,0,655,653,1,0,0,0,656,659,1,0,0,0,657,655, + 1,0,0,0,657,658,1,0,0,0,658,660,1,0,0,0,659,657,1,0,0,0,660,661,5,101, + 0,0,661,664,1,0,0,0,662,664,3,64,32,0,663,650,1,0,0,0,663,662,1,0,0,0, + 664,668,1,0,0,0,665,666,5,66,0,0,666,668,5,54,0,0,667,648,1,0,0,0,667, + 665,1,0,0,0,668,63,1,0,0,0,669,675,3,66,33,0,670,671,3,68,34,0,671,672, + 3,66,33,0,672,674,1,0,0,0,673,670,1,0,0,0,674,677,1,0,0,0,675,673,1,0, + 0,0,675,676,1,0,0,0,676,65,1,0,0,0,677,675,1,0,0,0,678,684,3,70,35,0,679, + 680,5,100,0,0,680,681,3,64,32,0,681,682,5,101,0,0,682,684,1,0,0,0,683, + 678,1,0,0,0,683,679,1,0,0,0,684,67,1,0,0,0,685,687,5,35,0,0,686,688,5, + 36,0,0,687,686,1,0,0,0,687,688,1,0,0,0,688,692,1,0,0,0,689,692,5,37,0, + 0,690,692,5,38,0,0,691,685,1,0,0,0,691,689,1,0,0,0,691,690,1,0,0,0,692, + 69,1,0,0,0,693,695,5,1,0,0,694,696,3,72,36,0,695,694,1,0,0,0,695,696,1, + 0,0,0,696,698,1,0,0,0,697,699,3,78,39,0,698,697,1,0,0,0,698,699,1,0,0, + 0,699,700,1,0,0,0,700,703,3,82,41,0,701,702,5,53,0,0,702,704,3,112,56, + 0,703,701,1,0,0,0,703,704,1,0,0,0,704,706,1,0,0,0,705,707,3,86,43,0,706, + 705,1,0,0,0,706,707,1,0,0,0,707,709,1,0,0,0,708,710,3,96,48,0,709,708, + 1,0,0,0,709,710,1,0,0,0,710,712,1,0,0,0,711,713,3,74,37,0,712,711,1,0, + 0,0,712,713,1,0,0,0,713,715,1,0,0,0,714,716,3,76,38,0,715,714,1,0,0,0, + 715,716,1,0,0,0,716,718,1,0,0,0,717,719,3,98,49,0,718,717,1,0,0,0,718, + 719,1,0,0,0,719,71,1,0,0,0,720,721,7,4,0,0,721,73,1,0,0,0,722,723,5,24, + 0,0,723,724,5,26,0,0,724,729,3,102,51,0,725,726,5,102,0,0,726,728,3,102, + 51,0,727,725,1,0,0,0,728,731,1,0,0,0,729,727,1,0,0,0,729,730,1,0,0,0,730, + 75,1,0,0,0,731,729,1,0,0,0,732,733,5,27,0,0,733,734,3,102,51,0,734,77, + 1,0,0,0,735,736,5,4,0,0,736,741,3,80,40,0,737,738,7,5,0,0,738,740,3,80, + 40,0,739,737,1,0,0,0,740,743,1,0,0,0,741,739,1,0,0,0,741,742,1,0,0,0,742, + 745,1,0,0,0,743,741,1,0,0,0,744,746,5,33,0,0,745,744,1,0,0,0,745,746,1, + 0,0,0,746,79,1,0,0,0,747,754,5,108,0,0,748,754,5,106,0,0,749,750,5,100, + 0,0,750,751,3,102,51,0,751,752,5,101,0,0,752,754,1,0,0,0,753,747,1,0,0, + 0,753,748,1,0,0,0,753,749,1,0,0,0,754,81,1,0,0,0,755,765,5,87,0,0,756, + 761,3,84,42,0,757,758,5,102,0,0,758,760,3,84,42,0,759,757,1,0,0,0,760, + 763,1,0,0,0,761,759,1,0,0,0,761,762,1,0,0,0,762,765,1,0,0,0,763,761,1, + 0,0,0,764,755,1,0,0,0,764,756,1,0,0,0,765,83,1,0,0,0,766,767,3,112,56, + 0,767,768,5,103,0,0,768,769,5,87,0,0,769,778,1,0,0,0,770,775,3,102,51, + 0,771,773,5,5,0,0,772,771,1,0,0,0,772,773,1,0,0,0,773,774,1,0,0,0,774, + 776,3,112,56,0,775,772,1,0,0,0,775,776,1,0,0,0,776,778,1,0,0,0,777,766, + 1,0,0,0,777,770,1,0,0,0,778,85,1,0,0,0,779,780,5,2,0,0,780,785,3,88,44, + 0,781,782,5,102,0,0,782,784,3,88,44,0,783,781,1,0,0,0,784,787,1,0,0,0, + 785,783,1,0,0,0,785,786,1,0,0,0,786,87,1,0,0,0,787,785,1,0,0,0,788,792, + 3,90,45,0,789,791,3,92,46,0,790,789,1,0,0,0,791,794,1,0,0,0,792,790,1, + 0,0,0,792,793,1,0,0,0,793,89,1,0,0,0,794,792,1,0,0,0,795,800,3,112,56, + 0,796,798,5,5,0,0,797,796,1,0,0,0,797,798,1,0,0,0,798,799,1,0,0,0,799, + 801,3,112,56,0,800,797,1,0,0,0,800,801,1,0,0,0,801,816,1,0,0,0,802,803, + 5,100,0,0,803,804,3,64,32,0,804,809,5,101,0,0,805,807,5,5,0,0,806,805, + 1,0,0,0,806,807,1,0,0,0,807,808,1,0,0,0,808,810,3,112,56,0,809,806,1,0, + 0,0,809,810,1,0,0,0,810,816,1,0,0,0,811,812,5,100,0,0,812,813,3,88,44, + 0,813,814,5,101,0,0,814,816,1,0,0,0,815,795,1,0,0,0,815,802,1,0,0,0,815, + 811,1,0,0,0,816,91,1,0,0,0,817,818,3,94,47,0,818,819,5,20,0,0,819,820, + 3,90,45,0,820,821,5,22,0,0,821,822,3,102,51,0,822,93,1,0,0,0,823,825,5, + 15,0,0,824,823,1,0,0,0,824,825,1,0,0,0,825,839,1,0,0,0,826,828,5,16,0, + 0,827,829,5,19,0,0,828,827,1,0,0,0,828,829,1,0,0,0,829,839,1,0,0,0,830, + 832,5,17,0,0,831,833,5,19,0,0,832,831,1,0,0,0,832,833,1,0,0,0,833,839, + 1,0,0,0,834,836,5,18,0,0,835,837,5,19,0,0,836,835,1,0,0,0,836,837,1,0, + 0,0,837,839,1,0,0,0,838,824,1,0,0,0,838,826,1,0,0,0,838,830,1,0,0,0,838, + 834,1,0,0,0,839,95,1,0,0,0,840,841,5,3,0,0,841,842,3,102,51,0,842,97,1, + 0,0,0,843,844,5,23,0,0,844,845,5,26,0,0,845,850,3,100,50,0,846,847,5,102, + 0,0,847,849,3,100,50,0,848,846,1,0,0,0,849,852,1,0,0,0,850,848,1,0,0,0, + 850,851,1,0,0,0,851,99,1,0,0,0,852,850,1,0,0,0,853,855,3,102,51,0,854, + 856,7,1,0,0,855,854,1,0,0,0,855,856,1,0,0,0,856,101,1,0,0,0,857,858,6, + 51,-1,0,858,859,5,8,0,0,859,866,3,102,51,16,860,861,5,12,0,0,861,866,3, + 102,51,15,862,863,5,92,0,0,863,866,3,102,51,14,864,866,3,104,52,0,865, + 857,1,0,0,0,865,860,1,0,0,0,865,862,1,0,0,0,865,864,1,0,0,0,866,936,1, + 0,0,0,867,868,10,13,0,0,868,869,5,90,0,0,869,935,3,102,51,14,870,871,10, + 12,0,0,871,872,7,6,0,0,872,935,3,102,51,13,873,874,10,11,0,0,874,875,7, + 7,0,0,875,935,3,102,51,12,876,877,10,10,0,0,877,878,7,8,0,0,878,935,3, + 102,51,11,879,881,10,9,0,0,880,882,5,8,0,0,881,880,1,0,0,0,881,882,1,0, + 0,0,882,883,1,0,0,0,883,884,5,34,0,0,884,885,3,102,51,0,885,886,5,6,0, + 0,886,887,3,102,51,10,887,935,1,0,0,0,888,890,10,8,0,0,889,891,5,8,0,0, + 890,889,1,0,0,0,890,891,1,0,0,0,891,892,1,0,0,0,892,893,5,13,0,0,893,935, + 3,102,51,9,894,895,10,4,0,0,895,896,7,9,0,0,896,935,3,102,51,5,897,898, + 10,3,0,0,898,899,5,6,0,0,899,935,3,102,51,4,900,901,10,2,0,0,901,902,5, + 7,0,0,902,935,3,102,51,3,903,905,10,7,0,0,904,906,5,8,0,0,905,904,1,0, + 0,0,905,906,1,0,0,0,906,907,1,0,0,0,907,908,5,21,0,0,908,909,5,100,0,0, + 909,910,3,70,35,0,910,911,5,101,0,0,911,935,1,0,0,0,912,914,10,6,0,0,913, + 915,5,8,0,0,914,913,1,0,0,0,914,915,1,0,0,0,915,916,1,0,0,0,916,917,5, + 21,0,0,917,918,5,100,0,0,918,923,3,102,51,0,919,920,5,102,0,0,920,922, + 3,102,51,0,921,919,1,0,0,0,922,925,1,0,0,0,923,921,1,0,0,0,923,924,1,0, + 0,0,924,926,1,0,0,0,925,923,1,0,0,0,926,927,5,101,0,0,927,935,1,0,0,0, + 928,929,10,5,0,0,929,931,5,25,0,0,930,932,5,8,0,0,931,930,1,0,0,0,931, + 932,1,0,0,0,932,933,1,0,0,0,933,935,5,86,0,0,934,867,1,0,0,0,934,870,1, + 0,0,0,934,873,1,0,0,0,934,876,1,0,0,0,934,879,1,0,0,0,934,888,1,0,0,0, + 934,894,1,0,0,0,934,897,1,0,0,0,934,900,1,0,0,0,934,903,1,0,0,0,934,912, + 1,0,0,0,934,928,1,0,0,0,935,938,1,0,0,0,936,934,1,0,0,0,936,937,1,0,0, + 0,937,103,1,0,0,0,938,936,1,0,0,0,939,958,3,114,57,0,940,958,3,106,53, + 0,941,958,3,110,55,0,942,958,5,106,0,0,943,958,5,105,0,0,944,945,5,28, + 0,0,945,946,5,100,0,0,946,947,3,70,35,0,947,948,5,101,0,0,948,958,1,0, + 0,0,949,950,5,100,0,0,950,951,3,70,35,0,951,952,5,101,0,0,952,958,1,0, + 0,0,953,954,5,100,0,0,954,955,3,102,51,0,955,956,5,101,0,0,956,958,1,0, + 0,0,957,939,1,0,0,0,957,940,1,0,0,0,957,941,1,0,0,0,957,942,1,0,0,0,957, + 943,1,0,0,0,957,944,1,0,0,0,957,949,1,0,0,0,957,953,1,0,0,0,958,105,1, + 0,0,0,959,960,3,108,54,0,960,973,5,100,0,0,961,974,5,87,0,0,962,964,5, + 32,0,0,963,962,1,0,0,0,963,964,1,0,0,0,964,965,1,0,0,0,965,970,3,102,51, + 0,966,967,5,102,0,0,967,969,3,102,51,0,968,966,1,0,0,0,969,972,1,0,0,0, + 970,968,1,0,0,0,970,971,1,0,0,0,971,974,1,0,0,0,972,970,1,0,0,0,973,961, + 1,0,0,0,973,963,1,0,0,0,973,974,1,0,0,0,974,975,1,0,0,0,975,976,5,101, + 0,0,976,107,1,0,0,0,977,982,3,112,56,0,978,982,5,16,0,0,979,982,5,17,0, + 0,980,982,5,82,0,0,981,977,1,0,0,0,981,978,1,0,0,0,981,979,1,0,0,0,981, + 980,1,0,0,0,982,109,1,0,0,0,983,984,3,112,56,0,984,985,5,103,0,0,985,987, + 1,0,0,0,986,983,1,0,0,0,986,987,1,0,0,0,987,988,1,0,0,0,988,989,3,112, + 56,0,989,111,1,0,0,0,990,991,7,10,0,0,991,113,1,0,0,0,992,1002,5,108,0, + 0,993,1002,5,109,0,0,994,1002,5,107,0,0,995,1002,5,110,0,0,996,1002,5, + 111,0,0,997,1002,5,112,0,0,998,1002,5,84,0,0,999,1002,5,85,0,0,1000,1002, + 5,86,0,0,1001,992,1,0,0,0,1001,993,1,0,0,0,1001,994,1,0,0,0,1001,995,1, + 0,0,0,1001,996,1,0,0,0,1001,997,1,0,0,0,1001,998,1,0,0,0,1001,999,1,0, + 0,0,1001,1000,1,0,0,0,1002,115,1,0,0,0,1003,1005,5,41,0,0,1004,1006,7, + 11,0,0,1005,1004,1,0,0,0,1005,1006,1,0,0,0,1006,1016,1,0,0,0,1007,1009, + 5,42,0,0,1008,1010,7,11,0,0,1009,1008,1,0,0,0,1009,1010,1,0,0,0,1010,1016, + 1,0,0,0,1011,1013,5,43,0,0,1012,1014,7,11,0,0,1013,1012,1,0,0,0,1013,1014, + 1,0,0,0,1014,1016,1,0,0,0,1015,1003,1,0,0,0,1015,1007,1,0,0,0,1015,1011, + 1,0,0,0,1016,117,1,0,0,0,1017,1018,3,102,51,0,1018,1019,5,0,0,1,1019,119, + 1,0,0,0,128,121,137,140,146,166,175,178,188,192,204,209,217,222,225,233, + 240,250,257,271,276,285,296,306,309,316,325,332,339,344,353,380,385,389, + 401,407,424,428,435,442,447,450,456,460,463,476,485,491,496,506,511,516, + 519,523,533,540,549,556,562,570,582,587,592,597,604,611,613,622,632,643, + 648,657,663,667,675,683,687,691,695,698,703,706,709,712,715,718,729,741, + 745,753,761,764,772,775,777,785,792,797,800,806,809,815,824,828,832,836, + 838,850,855,865,881,890,905,914,923,931,934,936,957,963,970,973,981,986, + 1001,1005,1009,1013,1015 }; public static readonly ATN _ATN = diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs index f47399721..a8f165056 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs @@ -577,6 +577,13 @@ public interface IAccessSqlVisitor : IParseTreeVisitor { /// The visitor result. Result VisitRightJoin([NotNull] AccessSqlParser.RightJoinContext context); /// + /// Visit a parse tree produced by the FullJoin + /// labeled alternative in . + /// + /// The parse tree. + /// The visitor result. + Result VisitFullJoin([NotNull] AccessSqlParser.FullJoinContext context); + /// /// Visit a parse tree produced by . /// /// The parse tree. diff --git a/src/LibRed/LibRed.Sql/Parsing/AntlrSqlParser.cs b/src/LibRed/LibRed.Sql/Parsing/AntlrSqlParser.cs index 69ca93337..8df87990b 100644 --- a/src/LibRed/LibRed.Sql/Parsing/AntlrSqlParser.cs +++ b/src/LibRed/LibRed.Sql/Parsing/AntlrSqlParser.cs @@ -27,6 +27,18 @@ public SqlStatement ParseStatement(string sql) return new AstBuilder().Build(parser.statement()); } + public bool IsStatementless(string sql) + { + if (string.IsNullOrWhiteSpace(sql)) return true; + + // Ask the lexer, not a text scan: WS/LINE_COMMENT/BLOCK_COMMENT are `-> skip`, so text made only of + // those produces no tokens at all. Anything else yields at least one, and a `--` inside a string + // literal stays part of that literal's token rather than starting a comment. + var lexer = new AccessSqlLexer(new AntlrInputStream(sql)); + lexer.RemoveErrorListeners(); // a malformed statement is ParseStatement's error to report, not ours + return lexer.NextToken().Type == TokenConstants.EOF; + } + public Expression ParseExpression(string sql) { ArgumentException.ThrowIfNullOrWhiteSpace(sql); diff --git a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs index cfe14e998..e0587b060 100644 --- a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs +++ b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs @@ -392,6 +392,9 @@ private static string TypeName(DataTypeContext type) => string.Join(' ', Rows = ins.Rows .Select(r => (IReadOnlyList)r.Select(e => LowerExpr(e, names)).ToList()) .ToList(), + // The multiple-record form's source is a query in its own right, so a declared parameter can + // appear in its WHERE just as it can in a VALUES list. + Source = ins.Source is null ? null : LowerParameters(ins.Source, names), }, _ => s, }; @@ -542,6 +545,11 @@ private static (ViewSource Source, string Alias) BuildViewSource(TablePrimaryCon { LeftJoinContext => ViewJoinKind.Left, RightJoinContext => ViewJoinKind.Right, + // A view is stored in Access's own query format, which has no full outer join to encode - so unlike a + // FULL JOIN executed directly, this one cannot be represented on disk. Refuse rather than silently + // storing the INNER the fall-through would otherwise pick. + FullJoinContext => throw new NotSupportedException( + "A FULL JOIN cannot be stored in a view: the Access query format has no representation for it."), _ => ViewJoinKind.Inner, }; @@ -552,6 +560,11 @@ private static SqlStatement BuildInsert(InsertStatementContext ctx) return new InsertStatement(table, [], [(IReadOnlyList)[]], DefaultValues: true); var columns = ctx._columns.Select(Identifier).ToList(); + + // The multiple-record form: the rows come from a query rather than a VALUES list. + if (ctx.source is not null) + return new InsertStatement(table, columns, [], Source: BuildQueryExpression(ctx.source)); + var values = ctx.expression().Select(BuildExpression).ToList(); return new InsertStatement(table, columns, [values]); } @@ -639,7 +652,8 @@ private static SelectStatement BuildSelect(SelectStatementContext ctx) return new SelectStatement(projection, star, from, where, groupBy, having, orderBy, top, Distinct: predicate?.DISTINCT() is not null, DistinctRow: predicate?.DISTINCTROW() is not null, - TopPercent: topPercent); + TopPercent: topPercent, + Into: ctx.into is null ? null : Identifier(ctx.into)); } /// The TOP count expression: a single operand, or a left-associative +/- chain of them (each @@ -701,6 +715,7 @@ private static TableReference BuildTableSource(TableSourceContext ctx) { LeftJoinContext => JoinKind.Left, RightJoinContext => JoinKind.Right, + FullJoinContext => JoinKind.Full, _ => JoinKind.Inner, }; diff --git a/src/LibRed/LibRed.Sql/Parsing/ISqlParser.cs b/src/LibRed/LibRed.Sql/Parsing/ISqlParser.cs index cab73222b..f075a25d9 100644 --- a/src/LibRed/LibRed.Sql/Parsing/ISqlParser.cs +++ b/src/LibRed/LibRed.Sql/Parsing/ISqlParser.cs @@ -12,6 +12,19 @@ public interface ISqlParser /// Parses a single statement. Throws on syntax errors. SqlStatement ParseStatement(string sql); + /// + /// True when carries no statement at all — it is empty, whitespace, or nothing but + /// comments. Such text is not a statement to execute; callers run it as a no-op rather than parsing it, + /// since the grammar has no production for "nothing" and would report a syntax error at EOF. + /// + /// + /// EF Core produces this from migrationBuilder.Sql("--some note"), which is legitimate and must + /// succeed silently. The check has to come from the lexer rather than from scanning for --: in + /// SELECT '--' the dashes are inside a string literal, and a textual strip would wrongly reduce a + /// real statement to nothing. + /// + bool IsStatementless(string sql); + /// Parses a complete bare scalar expression (e.g. a column DEFAULT value), consuming /// the entire input apart from skipped whitespace/comments. Throws /// on syntax errors or trailing tokens. diff --git a/src/LibRed/README.md b/src/LibRed/README.md index 911f3708f..6818dda1b 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -127,13 +127,28 @@ LibRed-side `CHECK` enforcement, self-pointing self-references, and writing Memo both `CASCADE` directions work. - **1:1 relationships** (`dbRelationUnique` = `0x01`) — never written; a 1:1 migration would render as 1:many in Access (the unique index still enforces uniqueness). Probe a real 1:1's `grbit` first. -- **ACE-16 types** — `Int64`/BIGINT (`0x13`) index-key encoding is trivial but unverified; - `DateTimeExtended`/DATETIME2 (`0x14`) is **read-only** (no write codec), which also blocks indexing it. - The **ACE-16 auto-upgrade** (using BIGINT/DATETIME2 bumps the format version) is deliberately refused - until we probe what else the upgrade changes. -- **Version-1 "General" text collation** (Access 2010+) — the v1 weight table needs an Access-2010+ - fixture to reverse-engineer; the encoder gates (throws) on any non-legacy collation meanwhile. Also: - populate `JetDatabase.Collation` from the page-0 sort order (needs page-0 de-obfuscation). +- **ACE 16/17 types — `Int64`/BIGINT (`0x13`) and `DateTimeExtended`/DATETIME2 (`0x14`) are both done**: read, + write, `CREATE TABLE`, index keys (ascending and descending), and native creation at the format each needs. + `LibRedConnection.CreateDatabase(…, version:)` takes a `JetVersion`, defaulting to ACE 12 so an ordinary + database still opens in every Access from 2007. Every step is verified against ACE, ending with Access's own + engine reading values out of files LibRed synthesised from nothing + (`DateTime2CreatedDatabaseAccessTests`, `BigIntCreatedDatabaseAccessTests`). + Two traps worth knowing: BIGINT is stored **variable**-length despite always being 8 bytes, and the two + types sit at **different** formats — `0x05` for BIGINT, `0x06` for DATETIME2. + The **format auto-upgrade** is implemented too: DDL that introduces either type raises the open file's + version byte rather than refusing, which is what Access itself does (`docs/format/page-00-database.md`, + where both routes are now measured). The raise joins the statement's transaction, so a failed CREATE/ALTER + takes it back down. ACE opens a file LibRed upgraded this way and reads the value that forced it. The + upgrade is one-way and unavoidable: an older Access cannot open the result, but neither could it read the + column. `CreateDatabase(…, version:)` remains the way to start at a format rather than arrive at one. +- **Non-English text collations** — index-key weights exist for the two General (1033) orders only: + General-Legacy (v0) and General (v1). Any other locale is refused by `IndexKeyEncoder` rather than encoded + with the English table. *(The v1 gap is closed: v1 keys are the Windows NLS weights verbatim, from the + Windows Server 2008 sorting weight table that ACE froze — see `docs/format/page-03-04-index-btree.md` §10.4 + and `tools/sortkey-table/generate.ps1`. v0's ancestor table is still unidentified: its ordering matches + every published Windows version across the range LibRed covers, so discriminating it needs characters whose + weights actually moved.)* + - **Computed / calculated columns** (ACE 14) — the evaluation half exists; the gap is the on-disk TDEF/`LvProp` storage of the expression (and persisted-vs-virtual semantics). - **`LvProp` properties not modelled** — `ValidationRule`/`ValidationText` (UI-authored validation, diff --git a/src/LibRed/docs/design/transactions.md b/src/LibRed/docs/design/transactions.md index a038f3bf8..e6694f011 100644 --- a/src/LibRed/docs/design/transactions.md +++ b/src/LibRed/docs/design/transactions.md @@ -14,7 +14,15 @@ Status: **draft / accepted direction** · Date: 2026-07-18 > writer-serialization / reader-blocking of strict 2PL (§4) is not needed for isolation, and EF's parallel > shared-store tests (each mutating inside a rolled-back transaction) no longer leak into concurrent readers. > The exclusive page lock is now held only for the duration of an individual committed page write, not the -> whole transaction. The undo log described below is gone. Everything else here — the lock-manager layering +> whole transaction. Before publishing, commit compares every page's committed plaintext image with the image +> from which that overlay page was first derived, under a per-file publish gate. Writers touching disjoint pages +> may both commit; if another connection changed an overlapping data/index/TDEF/usage-map page, the stale commit +> fails with a write conflict and remains open for rollback. This prevents silent lost updates without strict +> two-phase locking. If publication itself fails after writing some pages, that prefix is restored from the +> validated baselines (and appended tail pages are truncated), leaving the transaction open and rollbackable. +> Schema-changing commits also advance a shared per-file catalog generation; other open +> connections invalidate their parsed table/relationship/view caches on the next catalog access, while ordinary +> DML does not force a catalog reload. The undo log described below is gone. Everything else here — the lock-manager layering > (L0), the ACE co-residency constraint (§2), commit-byte / cross-process protocol, cascade worklist — still > stands as the roadmap. See `TransactionIsolationTests` and [[libred-parallel-dirty-read-flakiness]]. @@ -216,9 +224,8 @@ Build correctness first with lock seams stubbed; drop the Jet lock manager in la the process-local monitor implementation. 5. **L4 ADO enforcement.** ✅ done. Wire `LibRedTransaction`/`LibRedCommand` to L2; reject stale/foreign transactions; EF savepoint support (`SupportsSavepoints`). -6. **SQL transaction-control statements (with nesting).** Add engine-native `BEGIN`/`COMMIT`/ - `ROLLBACK [TRANSACTION|WORK]` (and Access's `BEGIN TRANS`), plus named `SAVE`/`ROLLBACK - TRANSACTION `. Parse to AST → a new `QueryEngine.Route` branch that drives a +6. **SQL transaction-control statements (with nesting).** ✅ done. Engine-native `BEGIN`/`COMMIT`/ + `ROLLBACK [TRANSACTION|WORK]`. Parse to AST → a new `QueryEngine.Route` branch that drives a per-connection **transaction controller** (the §4 depth counter) on the *same* L2 as the ADO front door — so a SQL `BEGIN` and an ADO `BeginTransaction` can't open parallel transactions, and the controller is the single source of `InTransaction`. Two must-haves: @@ -243,9 +250,14 @@ don't move. ## 7. Open questions -- **Reader isolation while a writer commits:** do readers under shared locks see the - pre-commit page (blocked until release) or is a dirty-read window acceptable initially? - Proposed: block (strict 2PL) — simplest correct default. +- **Reader isolation while a writer commits:** ✅ every statement holds the shared cache's publication gate for + its complete duration, so a reader sees either the complete pre-commit or complete post-commit page set, never + a torn mixture while an overlay is being published. The gate is a **reader/writer** lock, not a mutex: a + statement that cannot write (`SELECT`, a set operation, a system-variable select) takes it **shared**, so + concurrent readers on one file still run together; everything else takes it **exclusive** for the whole + statement, which is what makes a multi-page write atomic to readers. Anything not provably read-only takes the + exclusive scope — the shared scope cannot be upgraded and says so rather than deadlocking. Parsing happens + before the scope is taken (it touches no pages). See `ReaderWriterIsolationTests`. - **Implicit-txn cost:** per-statement begin/commit must be cheap for read-only statements (no dirty pages ⇒ commit is just lock release). Ensure a read-only statement never touches the commit-byte. diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index c81709803..a0f50b7bf 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -38,7 +38,7 @@ All integers little-endian unless noted; offsets are hex, relative to the struct | `0x3E` | 4 | Database/encryption key (`0` = not encrypted) | | `0x42` | 40 | Password (Jet4; Jet3 = 20) — also XOR `(int)creationDate` | | `0x6A` | 4 | Fixed constant `0x000011A6` | -| `0x6E` | 4 | Collation: LCID (2, LE) + sort-order version at `0x71` (`0` Legacy, `1` General) | +| `0x6E` | 4 | Collation, a 32-bit LCID with the version in its top byte: LANGID (2, LE), sort id at `0x70`, sort-order version at `0x71` (`0` legacy table, `1` Access-2010) | | `0x72` | 8 | Creation timestamp — OLE `double` (days from 1899-12-30) | | `0x98` | 4 | Fixed constant `0x00000654` (past the masked window) | | `0x9C` | 4 | Engine version string `"4.0"` (ASCII, NUL-term) | @@ -83,7 +83,7 @@ Variable section (`varOffsetTable`+`numVar`) omitted when the table has no varia | `0x08` | 4 | TDEF length (total logical bytes) | | `0x0C` | 4 | Constant marker `0x00000659` | | `0x10` | 4 | Row count | -| `0x14` | 4 | AutoNumber high-water = last assigned id (next = `+ 0x18`); seed `= 0x14 + increment` | +| `0x14` | 4 | AutoNumber high-water = last assigned id (next = `+ 0x18`, unchecked — **wraps** at the int32 boundary); seed `= 0x14 + increment` | | `0x18` | 4 | AutoNumber increment (signed int32; default 1) | | `0x1C` | 4 | Complex-type AutoNumber high-water | | `0x20` | 8 | Unknown / reserved (zero) | @@ -115,7 +115,8 @@ Variable section (`varOffsetTable`+`numVar`) omitted when the table has no varia | `0x09` | 2 | Column number (= id, until an `ALTER COLUMN` burns a new id at `0x05`) | | `0x0B` | 1 | Precision (Decimal) — else locale low byte `0x09` | | `0x0C` | 1 | Scale (Decimal) — else locale high byte `0x04` | -| `0x0D` | 2 | Sort-order version (0 = General legacy) | +| `0x0D` | 1 | Collation sort id — the LCID's high word (`0x01` = an alternate sort order, e.g. Hungarian Technical) | +| `0x0E` | 1 | Sort-order version (`0` legacy table, `1` Access-2010) | | `0x0F` | 1 | Flags: `0x01` fixed, `0x02` updatable, `0x04` auto-number, `0x40` auto-number GUID, `0x80` hyperlink | | `0x10` | 1 | Extended flags: `0x01` compressed-Unicode capable, `0xC0` calculated | | `0x11` | 4 | Unknown (zero) | diff --git a/src/LibRed/docs/format/data-types.md b/src/LibRed/docs/format/data-types.md index e4afc0ee7..be2780bd0 100644 --- a/src/LibRed/docs/format/data-types.md +++ b/src/LibRed/docs/format/data-types.md @@ -21,7 +21,7 @@ | `0x0F` | GUID | 16 raw bytes | | `0x10` | FixedPoint (Numeric/Decimal) | 17 bytes: sign byte (`0x80` = negative) + 128-bit magnitude (four 32-bit little-endian words, low word last); value = magnitude / 10^scale. Precision/scale from the column descriptor (§3.4) | | `0x12` | Complex (multi-value / attachment) | descriptor parsed; contents not materialized (out of scope for SQL/EF) | -| `0x13` | Int64 — **BIGINT** (ACE 16 / Access 2016) | 8-byte little-endian signed integer. Stored as a *variable*-length column | +| `0x13` | Int64 — **BIGINT** (ACE 16 / Access 2016) | 8-byte little-endian signed integer. Stored as a *variable*-length column (see below) | | `0x14` | DateTimeExtended — **DATETIME2** (ACE 17 / Access 2019+) | fixed 42-byte ASCII `: