From 843da01f4701dc08570c049a6ce93535c2431403 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 23 Sep 2026 17:14:06 +0800 Subject: [PATCH 01/88] Start 11.0.0-alpha.4 - Bump to alpha.4 after the alpha.3 release - Remove the MyGet feeds; CI builds stay available as the nupkgs artifact - Convert a char to a number by its code point (AscW), not by parsing its text - Accept any DbConnection in UseLibRed Co-Authored-By: Claude Opus 5.5 --- .github/workflows/push.yml | 16 ------------- AGENTS.md | 8 ++++--- CLAUDE.md | 8 ++++--- NuGet.Config | 5 ---- Version.props | 2 +- docs/README.md | 19 +++------------ .../Sql/Internal/JetQuerySqlGenerator.cs | 10 ++++++++ ...LibRedDbContextOptionsBuilderExtensions.cs | 10 -------- .../Sql/Internal/LibRedQuerySqlGenerator.cs | 10 ++++++++ .../Query/NorthwindJoinQueryJetTest.cs | 23 ++++++++++++++++++- .../Migrations/MigrationsLibRedTest.cs | 3 +-- .../Query/NorthwindJoinQueryLibRedTest.cs | 18 ++++++++++++++- .../Query/NorthwindJoinQueryLibRedTest.cs | 23 ++++++++++++++++++- 13 files changed, 96 insertions(+), 59 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 8296ec1d..e860207f 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -888,12 +888,8 @@ jobs: $buildSha = '${{ github.sha }}'.SubString(0, 7); $pack = $officialBuild -or $ciBuildOnly - $pushToAzureArtifacts = $pack - $pushToMygetOrg = $pack $pushToNugetOrg = $pack -and $officialBuild - echo "pushToAzureArtifacts: $pushToAzureArtifacts" - echo "pushToMygetOrg: $pushToMygetOrg" echo "pushToNugetOrg: $pushToNugetOrg" echo "officialBuild: $officialBuild" @@ -944,24 +940,12 @@ jobs: } } - echo "pushToAzureArtifacts=$pushToAzureArtifacts" >> $env:GITHUB_ENV - echo "pushToMygetOrg=$pushToMygetOrg" >> $env:GITHUB_ENV echo "pushToNugetOrg=$pushToNugetOrg" >> $env:GITHUB_ENV - name: Upload Artifacts uses: actions/upload-artifact@v6 with: name: nupkgs path: nupkgs - - name: "NuGet Push - myget.org - Debug" - if: ${{ env.pushToMygetOrg == 'true' }} - working-directory: nupkgs - shell: pwsh - run: dotnet nuget push './Debug/withPdbs/**/*.nupkg' --api-key '${{ secrets.MYGETORG_CIRRUSRED_ALLPACKAGES_DEBUG_PUSHNEW }}' --source 'https://www.myget.org/F/cirrusred-debug/api/v3/index.json' --skip-duplicate - - name: "NuGet Push - myget.org - Release" - if: ${{ env.pushToMygetOrg == 'true' }} - working-directory: nupkgs - shell: pwsh - run: dotnet nuget push './Release/default/**/*.nupkg' --api-key '${{ secrets.MYGETORG_CIRRUSRED_ALLPACKAGES_PUSHNEW }}' --source 'https://www.myget.org/F/cirrusred/api/v3/index.json' --skip-duplicate - name: "NuGet Push - nuget.org - Release" if: ${{ env.pushToNugetOrg == 'true' }} working-directory: nupkgs diff --git a/AGENTS.md b/AGENTS.md index c78072b4..f66f1f99 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ It is kept in sync with `CLAUDE.md`; if you change guidance in one, check whethe 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.0-alpha.3` (`Version.props`) targeting EF Core 11 and `net11.0`; `global.json` pins the 11.0.100 RC1 SDK with `rollForward: latestFeature`. The test projects use **xunit v3**. +Current version: `11.0.0-alpha.4` (`Version.props`) targeting EF Core 11 and `net11.0`; `global.json` pins the 11.0.100 RC1 SDK with `rollForward: latestFeature`. The test projects use **xunit v3**. ### Which layer am I touching? @@ -343,11 +343,13 @@ 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. +- **LibRed** — `LibRed.Engine.Tests` and `LibRed.Core.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** — both `EFCore.LibRed.FunctionalTests` (compatible mode) and `EFCore.LibRed.Extended.FunctionalTests` (extended mode) on the five-platform matrix, `continue-on-error`. -- **NuGet** — packs and pushes to MyGet/NuGet for `master`, `*-servicing`, `*-wip` and release tags. +- **NuGet** — packs for `master`, `*-servicing`, `*-wip` and release tags, attaching the packages to the run + as the `nupkgs` artifact. It pushes to nuget.org on **release tags only**; there is no CI package feed. ## Versioning diff --git a/CLAUDE.md b/CLAUDE.md index e7c6234a..5d504473 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 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.0-alpha.3` (`Version.props`) targeting EF Core 11 and `net11.0`; `global.json` pins the 11.0.100 RC1 SDK with `rollForward: latestFeature`. The test projects use **xunit v3**. +Current version: `11.0.0-alpha.4` (`Version.props`) targeting EF Core 11 and `net11.0`; `global.json` pins the 11.0.100 RC1 SDK with `rollForward: latestFeature`. The test projects use **xunit v3**. ### Which layer am I touching? @@ -365,11 +365,13 @@ 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. +- **LibRed** — `LibRed.Engine.Tests` and `LibRed.Core.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** — both `EFCore.LibRed.FunctionalTests` (compatible mode) and `EFCore.LibRed.Extended.FunctionalTests` (extended mode) on the five-platform matrix, `continue-on-error`. -- **NuGet** — packs and pushes to MyGet/NuGet for `master`, `*-servicing`, `*-wip` and release tags. +- **NuGet** — packs for `master`, `*-servicing`, `*-wip` and release tags, attaching the packages to the run + as the `nupkgs` artifact. It pushes to nuget.org on **release tags only**; there is no CI package feed. ## Versioning diff --git a/NuGet.Config b/NuGet.Config index aa6e59c7..24616176 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -10,11 +10,6 @@ feed the three suites that inherit EF's test bases fail to restore with NU1101. --> - - \ No newline at end of file diff --git a/Version.props b/Version.props index f7528b9b..6b69c475 100644 --- a/Version.props +++ b/Version.props @@ -17,7 +17,7 @@ --> 11.0.0 alpha - 3 + 4 + net10.0;$(JetTargetFramework) diff --git a/src/LibRed/LibRed.Ado/LibRed.Ado.csproj b/src/LibRed/LibRed.Ado/LibRed.Ado.csproj index 458b6747..97f0314c 100644 --- a/src/LibRed/LibRed.Ado/LibRed.Ado.csproj +++ b/src/LibRed/LibRed.Ado/LibRed.Ado.csproj @@ -1,7 +1,7 @@ - $(JetTargetFramework) + $(LibRedTargetFrameworks) LibRed.Data LibRed.Ado ADO.NET provider over the LibRed engine: DbConnection, DbCommand, DbDataReader, DbParameter, DbTransaction and DbProviderFactory. diff --git a/src/LibRed/LibRed.Core/LibRed.Core.csproj b/src/LibRed/LibRed.Core/LibRed.Core.csproj index 63c2f8a3..c88724b6 100644 --- a/src/LibRed/LibRed.Core/LibRed.Core.csproj +++ b/src/LibRed/LibRed.Core/LibRed.Core.csproj @@ -1,7 +1,7 @@ - $(JetTargetFramework) + $(LibRedTargetFrameworks) LibRed LibRed.Core Native, fully managed reader/writer for the Microsoft Jet/ACE (Access MDB/ACCDB) file format: pages, tables, columns, indexes, catalog, encoding and memo/OLE fields. diff --git a/src/LibRed/LibRed.Engine/LibRed.Engine.csproj b/src/LibRed/LibRed.Engine/LibRed.Engine.csproj index 75c32052..f4084673 100644 --- a/src/LibRed/LibRed.Engine/LibRed.Engine.csproj +++ b/src/LibRed/LibRed.Engine/LibRed.Engine.csproj @@ -1,7 +1,7 @@ - $(JetTargetFramework) + $(LibRedTargetFrameworks) LibRed.Engine LibRed.Engine Query engine: plans and executes bound SQL statements (select/insert/update/delete, joins, filters, expressions) against the LibRed.Core storage layer. diff --git a/src/LibRed/LibRed.Sql/LibRed.Sql.csproj b/src/LibRed/LibRed.Sql/LibRed.Sql.csproj index 3c80e849..90cf07c1 100644 --- a/src/LibRed/LibRed.Sql/LibRed.Sql.csproj +++ b/src/LibRed/LibRed.Sql/LibRed.Sql.csproj @@ -1,7 +1,7 @@ - $(JetTargetFramework) + $(LibRedTargetFrameworks) LibRed.Sql LibRed.Sql Jet/ACE SQL front end: lexer, parser, AST and binder. Has no dependency on the storage engine; binding resolves names through an injected schema abstraction. From f319d2b4cadc582ce46012c8384d410f9b4aad68 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sat, 26 Sep 2026 04:38:18 +0800 Subject: [PATCH 17/88] Record the page-0 header as RC4 and the SIDs as a per-file keystream - The 128-byte page-0 header mask is the RC4 keystream of the fixed key C7 DA 39 6B, verified over all 128 bytes. - An on-disk SID is its workgroup SID XOR'd with a per-file keystream from its first byte; the 2-byte short-SID mask is that keystream's first two bytes, and the 102-byte SIDs Access adds use the same keystream. - How the keystream derives from the creation date is recorded as not known, with the derivations ruled out, replacing the shared-PRNG guess. The workgroup Admins SID is 102 bytes, not 98. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Formats/JetFormatBase.cs | 3 +- .../Pages/DatabaseDefinitionPage.cs | 2 +- .../LibRed.Core/Storage/DatabaseCreator.cs | 25 +++---- src/LibRed/docs/format/appendix-structures.md | 2 +- src/LibRed/docs/format/page-00-database.md | 65 +++++++++++-------- 5 files changed, 55 insertions(+), 42 deletions(-) diff --git a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs index 57639a32..888ee1ac 100644 --- a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs +++ b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs @@ -50,7 +50,8 @@ public abstract class JetFormatBase public const string JetSystemIdentifier = "Jet System DB"; // --- Page 0 obfuscated header (0x18..0x98) --- - // The header is XOR-obfuscated with a fixed 128-byte mask (below). Field offsets and the mask + // The header is XOR-obfuscated with a fixed 128-byte mask (below): the RC4 keystream of the key + // C7 DA 39 6B, the same for every file. Field offsets and the mask // are corroborated by mdbtools and Jackcess AND verified against real files here: the mask // reproduces the code page (0x3C), collation LCID (0x6E) and creation date (0x72) bytes we // recovered independently by known-plaintext, and it decodes every fixture's header to sensible diff --git a/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs index d2723ef5..6641e4cf 100644 --- a/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs +++ b/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs @@ -63,7 +63,7 @@ public override void Read(PageBuffer buffer, Formats.JetFormatBase format) FormatIdentifier = Formats.JetFormatBase.ReadFormatIdentifier(buffer.Span); JetVersion = buffer.ReadByte(Formats.JetFormatBase.VersionOffset); - // The header from 0x18 is XOR-obfuscated with the fixed 128-byte mask; de-obfuscate the + // The header from 0x18 is XOR-obfuscated with a fixed RC4 keystream; de-obfuscate the // whole region once, then read the fields out of the clear copy. Span clear = stackalloc byte[Formats.JetFormatBase.PageZeroHeaderMask.Length]; Demask(buffer.Span, clear); diff --git a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs index 94edce24..56ffb4c7 100644 --- a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs @@ -206,19 +206,20 @@ private static readonly (string Name, ColumnSpec[] Columns)[] MSysComplexTypeTab 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 - // a per-file 2-byte mask. The account SIDs were read verbatim from a real System.mdw (the file Access opens - // first to authenticate): admin(user)=03-01, Users(group)=02-01, Engine=02-03, Creator=02-04; the Admins - // group alone has a long per-workgroup SID (which Access materialises as a 98-byte SID on first open, so we - // don't emit it). Object ownership uses the "user" form (byte0 0x03) of Engine/Creator, matching real DAO - // files. Verified against WideTable with mask 24-CC: Users 02-01^24-CC = 26-CD, admin 03-01^24-CC = 27-CD, - // Engine-as-user 03-03^24-CC = 27-CF (system-object owner), Creator-as-user 03-04^24-CC = 27-C8. + // Per-file SID cluster. A database's on-disk SIDs are the DEFAULT WORKGROUP's account SIDs XOR'd with a + // per-file keystream, whose first two bytes are all a 2-byte SID meets — so it reads as a 2-byte mask. The + // account SIDs were read verbatim from a real System.mdw (the file Access opens first to authenticate): + // admin(user)=03-01, Users(group)=02-01, Engine=02-03, Creator=02-04; the Admins group alone has a 102-byte + // SID, which Access adds on first open, so we don't emit it. Object ownership uses the "user" form (byte0 + // 0x03) of Engine/Creator, matching real DAO files. Verified against WideTable with mask 24-CC: Users + // 02-01^24-CC = 26-CD, admin 03-01^24-CC = 27-CD, Engine-as-user 03-03^24-CC = 27-CF (system-object owner), + // Creator-as-user 03-04^24-CC = 27-C8. // - // The mask is bound to the millisecond-precise creation date: Access derives both from a shared PRNG state - // at create time, so there is NO closed-form date->mask function (144 files, no checksum/PRNG fit) and the - // two MUST travel together. We bake one verified, self-consistent (creation-date, mask) pair — the from- - // scratch analogue of the account-SID constants — giving an Access-openable file with no template/graft. - // TODO: per-file-random dates (and custom/secured workgroups) need the date<->mask coupling cracked. + // The keystream is bound to the millisecond-precise creation date, and how it derives from the date is not + // known (docs/format/page-00-database.md §2.3), so the two MUST travel together. We bake one verified, + // self-consistent (creation-date, mask) pair — the from-scratch analogue of the account-SID constants — + // giving an Access-openable file with no template/graft. + // TODO: per-file-random dates (and custom/secured workgroups) need the date<->keystream derivation. internal const long SeedCreationDateBits = 0x40E68F1E8943D217L; // 2026-06-27 22:54:07.716 (WideTable) — pairs with SidMask private static readonly byte[] SidMask = [0x24, 0xCC]; // WideTable's per-file mask (pairs with SeedCreationDateBits) private static byte[] Masked(byte b0, byte b1) => [(byte)(b0 ^ SidMask[0]), (byte)(b1 ^ SidMask[1])]; diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index 24bf2224..8f04af14 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -32,7 +32,7 @@ All integers little-endian unless noted; offsets are hex, relative to the struct | `0x14` | 1 | Version byte (`0x00` Jet3, `0x01` Jet4, `0x02` ACE12, `0x03` ACE14, `0x04` ACE15/2013 never emitted and refused by ACE, `0x05` ACE16, `0x06` ACE17) | | `0x15` | 1 | Version minor byte (`0x01` on a file created as ACE14/Access 2010, else `0x00`; a version raise writes `0x00`) | | `0x16` | 2 | Unknown (zero) | -| `0x18`–`0x98` | 128 | **Obfuscated header** — XOR'd with the fixed 128-byte mask; the `0x18`–`0x72` fields below are offsets into it (Jet3 masks 126) | +| `0x18`–`0x98` | 128 | **Obfuscated header** — XOR'd with the RC4 keystream of the key `C7 DA 39 6B`; the `0x18`–`0x72` fields below are offsets into it (Jet3 masks 126) | | `0x18` | 4 | Global free-pages map pointer `[row:1][page:3]` (`0x00000100` = page 1 row 0) | | `0x1C` | 4 | Global released-pages map pointer `[row:1][page:3]` (`0x00000101` = page 1 row 1) | | `0x20`–`0x2C` | 4×4 | Catalog bootstrap pointers — `MSysObjects`/`MSysACEs`/`MSysQueries`/`MSysRelationships` TDEF pages (`2`/`3`/`4`/`5`); `0x20` = catalog root | diff --git a/src/LibRed/docs/format/page-00-database.md b/src/LibRed/docs/format/page-00-database.md index 614583e2..3b592916 100644 --- a/src/LibRed/docs/format/page-00-database.md +++ b/src/LibRed/docs/format/page-00-database.md @@ -13,7 +13,7 @@ | `0x14` | 1 | Version byte (see below). mdbtools reads `jet_version` as a 4-byte word at `0x14`; the version is its low byte | | `0x15` | 1 | Version **minor** byte: **`0x01` on a database created in the 2010 format (version `0x03`)**, `0x00` when created in any other. **A version raise writes `0x00`** whatever the target — including a raise *onto* `0x03`. Purpose otherwise unknown | | `0x16` | 2 | Unknown (zero observed) | -| `0x18`–`0x98` | 128 | **Obfuscated header** — XOR'd with a fixed 128-byte mask (§2.1). Jet 3 masks 126 bytes. Fields below are offsets into it. | +| `0x18`–`0x98` | 128 | **Obfuscated header** — XOR'd with the RC4 keystream of the key `C7 DA 39 6B` (§2.1). Jet 3 masks 126 bytes. Fields below are offsets into it. | | `0x18` | 4 | **Global free-pages map pointer** — `[row:1][page:3]`; `0x00000100` = page 1 row 0 in every file ACE writes ([page-05 §9.1](page-05-usage-maps.md)) | | `0x1C` | 4 | **Global released-pages map pointer** — `[row:1][page:3]`; `0x00000101` = page 1 row 1 ([page-05 §9.1](page-05-usage-maps.md)) | | `0x20`–`0x2C` | 4×4 | **System-catalog bootstrap pointers**: TDEF pages of `MSysObjects` / `MSysACEs` / `MSysQueries` / `MSysRelationships` = `2, 3, 4, 5`. `0x20` is the **catalog root** (how the engine finds `MSysObjects`). | @@ -178,13 +178,14 @@ service-pack build. ### 2.1 The obfuscated header (`0x18`–`0x98`) From `0x18` for **128 bytes** (Jet 4 / ACE; 126 for Jet 3), page 0 is obfuscated by XOR-ing the -plaintext with a **fixed byte mask** — a constant baked into the format, not a per-file salt. Past -the window the bytes are in the clear: a fixed constant `0x00000654` at `0x98`, the NUL-terminated -ASCII engine-version string **`"4.0"`** at `0x9C` (the Jet 4.0 version — identical in `.mdb` and -`.accdb`), then zero padding. +plaintext with an **RC4 keystream under the fixed 4-byte key `C7 DA 39 6B`** — the same for every file, +not a per-file salt. Past the window the bytes are in the clear: a fixed constant `0x00000654` at `0x98`, +the NUL-terminated ASCII engine-version string **`"4.0"`** at `0x9C` (the Jet 4.0 version — identical in +`.mdb` and `.accdb`), then zero padding. -**The mask.** LibRed uses the 128-byte mask below (`JetFormatBase.PageZeroHeaderMask`), de-obfuscating -the whole region once in `DatabaseDefinitionPage.Read`: +**The mask.** The keystream's first 128 bytes are below; LibRed holds them as a constant +(`JetFormatBase.PageZeroHeaderMask`) and de-obfuscates the whole region once in +`DatabaseDefinitionPage.Read`. RC4 under `C7 DA 39 6B` reproduces them exactly (verified): ``` B5 6F 03 62 61 08 C2 55 EB A9 67 72 43 3F 00 9C ; 0x18 @@ -338,37 +339,47 @@ values Access shows in the `SID` column: | `Users` | group | `02-01` | | `Engine` | — | `02-03` | | `Creator` | — | `02-04` | -| `Admins` | group | `01-DB-87-93-20-81-4F-AB-38-…` (long, per-workgroup-unique) | +| `Admins` | group | `01-DB-87-93-20-81-4F-AB-38-…` (102 bytes) | These short SIDs are the well-known Access defaults (identical on every stock install — which is why a captured SID cluster opens cross-PC). Object ownership in a database uses the **"user" form** (byte0 `0x03`) of `Engine`/`Creator`. -The 2-byte on-disk SIDs in `MSysACEs.SID` / `MSysObjects.Owner` are each a **workgroup account SID XOR'd with a -per-file 2-byte mask**. Verified, e.g. with mask `24-CC`: `Users 02-01 ^ 24-CC = 26-CD`, -`admin 03-01 ^ 24-CC = 27-CD` (read grantee), `Engine 03-03 ^ 24-CC = 27-CF` (system-object owner), -`Creator 03-04 ^ 24-CC = 27-C8` (inheritable container grant). The long `Admins` SID isn't emitted — Access -materialises it (as a 98-byte SID) on first open. +An on-disk SID in `MSysACEs.SID` / `MSysObjects.Owner` is the **workgroup account SID XOR'd with a per-file +keystream**, from the keystream's first byte (verified). Every SID in a file shares the one keystream, so a +2-byte SID always meets its first two bytes, and against the short SIDs the keystream reads as a **2-byte +mask**. E.g. with mask `24-CC`: `Users 02-01 ^ 24-CC = 26-CD`, `admin 03-01 ^ 24-CC = 27-CD` (read grantee), +`Engine 03-03 ^ 24-CC = 27-CF` (system-object owner), `Creator 03-04 ^ 24-CC = 27-C8` (inheritable container +grant). + +Access adds **102-byte SIDs** on first open, and they use the same keystream: under the file's short-SID mask +their first two bytes read `00 DB`, and two of them in one file differ by the same bytes in files from different +installs, which only a shared keystream allows (verified). Beyond byte 1 neither their plaintext nor the +keystream is established, so only the first two keystream bytes are recoverable from a file. **The mask is recoverable from the file, even though it is stored nowhere (verified).** `MSysObjects` is owned -by the `Engine` account (`03-03`) in every file, so `mask = MSysObjects.Owner ^ 03-03`, and every other account -follows from it: an owner of `680E` gives mask `6B-0D`, under which that file's `690C` / `680C` / `6809` are -`Users` / `admin` / `Creator`. That is how a writer adding an object to a file it did not create gets the SIDs -right ([system-catalog §11](system-catalog.md)); the pair baked in below fits only the files this engine +by the `Engine` account (`03-03`) in every file, so `mask = MSysObjects.Owner ^ 03-03`, and every other short +account follows from it: an owner of `680E` gives mask `6B-0D`, under which that file's `690C` / `680C` / `6809` +are `Users` / `admin` / `Creator`. That is how a writer adding an object to a file it did not create gets the +SIDs right ([system-catalog §11](system-catalog.md)); the pair baked in below fits only the files this engine creates itself. -That mask is **bound to the exact millisecond-precise creation-date `double`** at `0x72`: a file with +The keystream is **bound to the exact millisecond-precise creation-date `double`** at `0x72`: a file with self-consistent SIDs but a *different* creation date is rejected with *"Record(s) cannot be read; no read permission on 'MSysObjects'/'MSysACEs'"* (Jet 3112). Grafting a real file's date **and** SIDs together opens -clean; either alone fails. There is **no known closed-form `date → mask` function** — word XOR/sum, CRC-16, -MSVCRT `rand`, the VBA LCG and multiplicative hashes do not fit, and files created in the same second have -unrelated masks; Access most likely draws both the mask and the sub-second creation bits from one -PRNG state, so they correlate but neither derives from the other. `DatabaseCreator` therefore **bakes one -verified `(SeedCreationDateBits, SidMask)` pair** (`0x40E68F1E8943D217` + `24-CC`) rather than computing it — -the from-scratch analogue of the account-SID constants. Limitations (deferred): every LibRed-created file -reports the same creation instant, and only the **default** workgroup is supported; per-file-random dates and -custom/secured workgroups both need the date↔mask coupling cracked (reading a custom `System.mdw` itself -works — §2.4). +clean; either alone fails. Dates within the same second give unrelated keystreams. + +How the keystream derives from the date is **not known**. It is not RC4 under a 4-byte key made from the date's +low half, high half, their XOR or its day count combined with any constant by XOR, addition or subtraction; nor +RC4, MD5 or SHA-1 keyed by the date (its `double` in either byte order, either half, its day, second or +millisecond count, its `FILETIME` or `SYSTEMTIME`, or the date as stored), by any span of page 0 decoded or raw, +or by the header's `C7 DA 39 6B` key combined with the date; nor MSVCRT `rand` or the VB `Rnd` generator seeded +from page 0. +`DatabaseCreator` therefore **bakes one verified `(SeedCreationDateBits, SidMask)` pair** (`0x40E68F1E8943D217` ++ `24-CC`) rather than computing it — the from-scratch analogue of the account-SID constants. Limitations +(deferred): every LibRed-created file reports the same creation instant, and only the **default** workgroup is +supported; per-file-random dates and custom/secured workgroups both need the date↔keystream derivation (reading +a custom `System.mdw` itself works — §2.4). ### 2.4 Legacy Jet 3/4 RC4 page encryption (verified) From eb6b9c522d05b58ff1423ce59079c8177c31454f Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sat, 26 Sep 2026 16:27:56 +0800 Subject: [PATCH 18/88] Read Access's bang, Yes/No, keyword members and non-ASCII names [Table]![Column] is Table.Column in every clause, and an unaliased one is named as ACE names it, text as written. A longer chain such as [Forms]![frmMenu]![txtCity] is one name, reachable as a parameter the query declares; a period between parts counts as a bang, as ACE binds them. A stored parameter declared that way now reads back as that name rather than with only its outer brackets stripped. Yes/On and No/Off are True and False, winning over a column of that name unless it is qualified. Any word, reserved or not, names a column after a period or bang, and an unbracketed name may use any script's letters. Every stored query in the example corpus that uses a bang now parses. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/JetCatalog.cs | 31 +- src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 | 47 +- .../Grammar/Generated/AccessSqlBaseVisitor.cs | 20 + .../Grammar/Generated/AccessSqlLexer.cs | 1156 +++--- .../Grammar/Generated/AccessSqlParser.cs | 3223 ++++++++++------- .../Grammar/Generated/AccessSqlVisitor.cs | 12 + src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs | 89 +- src/LibRed/README.md | 8 +- src/LibRed/docs/format/system-catalog.md | 8 + .../QueryNamesAccessTests.cs | 198 + test/LibRed.Engine.Tests/QueryNamesTests.cs | 165 + 11 files changed, 3072 insertions(+), 1885 deletions(-) create mode 100644 test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs create mode 100644 test/LibRed.Engine.Tests/QueryNamesTests.cs diff --git a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs index 7164640c..5c226663 100644 --- a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs +++ b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs @@ -359,11 +359,7 @@ private void EnsureStoredQueries() var (size, precision, scale) = type is { } t ? StoredQueryFormat.UnpackParameterFacets(t, r[lvExtra] as int?) : (null, null, null); - // ACE stores the name as declared, its brackets included ('[@firstName]'); the name bound to is - // inside them, as the parser reads the declaration. - string declared = (string)r[n1]!; - string name = declared is ['[', .., ']'] ? declared[1..^1] : declared; - return new StoredQueryParameter(name, type, size, precision, scale); + return new StoredQueryParameter(DeclaredParameterName((string)r[n1]!), type, size, precision, scale); }) .ToList(); if (parameters.Count > 0) _queryParameters[name] = parameters; @@ -546,6 +542,31 @@ static string Render((string Table, string? Alias, string? Sub) t) => return (from.ToString(), pending.Select(j => j.Cond).ToList()); } + /// The name a stored parameter binds to, from the name ACE stored — which is the name as declared, its + /// brackets included: [@firstName] binds as @firstName. A form control is declared as a chain, + /// [Forms]![frmSelector]![txtTo], and binds as its parts undelimited and joined by bangs, + /// Forms!frmSelector!txtTo, a period counting as a bang; the parser names a reference to it the same + /// way. A period or bang inside delimiters is part of the name. + private static string DeclaredParameterName(string declared) + { + var parts = new List(); + var part = new System.Text.StringBuilder(); + char close = '\0'; + foreach (char c in declared) + { + if (close != '\0') + { + if (c == close) close = '\0'; + else part.Append(c); + } + else if (c is '[' or '`') close = c == '[' ? ']' : '`'; + else if (c is '!' or '.') { parts.Add(part.ToString()); part.Clear(); } + else part.Append(c); + } + parts.Add(part.ToString()); + return string.Join('!', parts); + } + /// The leading PARAMETERS name Type, …; clause a query with declared parameters (its /// 0x02 rows: Name1=name, Flag=Jet type code) is rebuilt with, so the parser lowers references to /// those names in the body into engine parameters; empty for a query declaring none. An action query diff --git a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 index 7b7e1668..cba88002 100644 --- a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 +++ b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 @@ -5,7 +5,8 @@ // LibRed.Sql.Ast by AstBuilder, so the rest of the engine never sees these generated types. // // Dialect notes (vs ANSI): '&' string concat; MOD / '\' operators; TOP n (no OFFSET); -// #1/1/2020# date literals; [bracketed] and `backtick` identifiers; booleans -1/0. +// #1/1/2020# date literals; [bracketed] and `backtick` identifiers; Table!Column bang references; +// booleans -1/0. grammar AccessSql; @@ -98,9 +99,10 @@ procParamList : LPAREN procParam (COMMA procParam)* RPAREN | procParam (COMMA procParam)* ; -// A parameter name is an identifier or an @-prefixed parameter token (e.g. @Beginning_Date). +// A parameter name is an identifier or an @-prefixed parameter token (e.g. @Beginning_Date), or a chain naming a form +// control — `PARAMETERS [Forms]![frmSelector]![txtTo] DateTime`, the commonest thing an Access query declares. procParam : pname=procParamName dataType ; -procParamName : identifier | PARAM ; +procParamName : columnRef | PARAM ; // ALTER TABLE table { ADD [COLUMN] field type … | ADD CONSTRAINT … | ALTER COLUMN field type | DROP … } // (Access allows exactly one action per statement.) The CONSTRAINT clause reuses CREATE TABLE's @@ -496,7 +498,15 @@ functionCall // followed by '(' and `PARTITION BY` never is, so the two never collide. functionName : identifier | LEFT | RIGHT | ASC | FIRST | PARTITION ; -columnRef : (qualifier=identifier DOT)? name=identifier ; +// A name, or a chain of them joined by '.' or Access's bang '!'. Two parts are table and column whichever joins them +// (verified vs ACE: Customers!CustomerID reads as Customers.CustomerID in every clause). A longer chain names nothing +// a query can reach — a form control such as Forms!frmMenu!cmbGroup, or [Forms]![f]![sub].[Form]![ctl] — and is +// only ever a parameter the query declares. ACE refuses whitespace either side of a bang, which the builder checks. +// After a separator any word names a column, reserved or not (verified vs ACE: t.Key, t.Select, t.From, t.And and +// t.Mod all read the column; only Union and When are refused there, and after a bang the operator words as well, +// which LibRed accepts all the same). +columnRef : first=identifier (separators+=(DOT | BANG) rest+=memberName)* ; +memberName : identifier | reservedKeyword ; identifier : IDENTIFIER | BRACKET_ID | BACKTICK_ID | nonReservedKeyword ; @@ -507,8 +517,10 @@ literal | STRING_LITERAL # StringLiteral | DATE_LITERAL # DateLiteral | GUID_LITERAL # GuidLiteral - | TRUE # TrueLiteral - | FALSE # FalseLiteral + // Yes and On are True, No and Off False, all four even where a column has that name — only a qualified + // reference reaches the column (verified vs ACE). + | (TRUE | YES | ON) # TrueLiteral + | (FALSE | NO | OFF) # FalseLiteral | NULL # NullLiteral ; @@ -559,6 +571,20 @@ frameExclusion : CURRENT ROW | GROUP | TIES | NO OTHERS ; nonReservedKeyword : RANGE | GROUPS | UNBOUNDED | PRECEDING | FOLLOWING | CURRENT | EXCLUDE | TIES | OTHERS | WITHIN | LAST | RESPECT | NULLS | FILTER + | YES | OFF + ; + +// Every other keyword, which a name may be only after a separator — see columnRef. +reservedKeyword + : SELECT | FROM | WHERE | TOP | AS | AND | OR | NOT | XOR | EQV | IMP | BAND | BOR | BXOR | BNOT | LIKE | MOD + | INNER | LEFT | RIGHT | FULL | OUTER | JOIN | IN | ON | ORDER | GROUP | IS | BY | HAVING | EXISTS | IF | THEN + | DISTINCTROW | DISTINCT | PERCENT | CROSS | APPLY | OVER | PARTITION | CASE | WHEN | ELSE | END | OFFSET + | FETCH | NEXT | FIRST | ROWS | ROW | ONLY | 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 | CLUSTERED | IDENTITY | NONCLUSTERED | INDEX | TEMPORARY | WITH | COMPRESSION + | COMP | DISALLOW | IGNORE | CHECK | VIEW | PROCEDURE | PARAMETERS | EXECUTE | EXEC | ASC | DESC | TRUE + | FALSE | NULL ; // An aggregate's FILTER: only the rows for which the condition is true go into it. @@ -683,6 +709,9 @@ ASC : [Aa][Ss][Cc] ; DESC : [Dd][Ee][Ss][Cc] ; TRUE : [Tt][Rr][Uu][Ee] ; FALSE : [Ff][Aa][Ll][Ss][Ee] ; +// Access's other spellings of True and False — not reserved; see nonReservedKeyword. ON is already a keyword. +YES : [Yy][Ee][Ss] ; +OFF : [Oo][Ff][Ff] ; NULL : [Nn][Uu][Ll][Ll] ; // The window clauses' words — not reserved; see nonReservedKeyword. RANGE : [Rr][Aa][Nn][Gg][Ee] ; @@ -717,6 +746,8 @@ LPAREN : '(' ; RPAREN : ')' ; COMMA : ',' ; DOT : '.' ; +// Access's bang, between the parts of a name. '!=' still lexes as NEQ, the longer match. +BANG : '!' ; SEMI : ';' ; // A connection-scoped system variable: @@ROWCOUNT (rows affected by the last statement) and // @@IDENTITY (the last AutoNumber generated on this connection). Must precede PARAM so the '@@' @@ -744,7 +775,9 @@ BACKTICK_ID : '`' ~[`]+ '`' ; // A trailing '$' is allowed so VBA "$" string-function variants (Left$, UCase$, Chr$, …) lex as a single // identifier. Longest-match makes "Left$" an IDENTIFIER (5 chars) rather than the LEFT keyword (4); the // evaluator strips the '$' and dispatches to the base function. -IDENTIFIER : [A-Za-z_][A-Za-z_0-9]* '$'? ; +// Any script's letters, not just ASCII (verified vs ACE: Név, Номер, 名前, ΑΒΓ, straße and Ñandú all read unbracketed), +// with a combining mark allowed after the first, as Devanagari and Thai spell a letter. +IDENTIFIER : [\p{L}_][\p{L}\p{M}\p{Nd}_]* '$'? ; WS : [ \t\r\n]+ -> skip ; // SQL comments — EF Core query tags prepend a `-- tag` line comment to the statement; also block comments. diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs index c6d54165..ed1fb44a 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs @@ -1450,6 +1450,16 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result. public virtual Result VisitColumnRef([NotNull] AccessSqlParser.ColumnRefContext context) { return VisitChildren(context); } /// + /// Visit a parse tree produced by . + /// + /// The default implementation returns the result of calling + /// on . + /// + /// + /// The parse tree. + /// The visitor result. + public virtual Result VisitMemberName([NotNull] AccessSqlParser.MemberNameContext context) { return VisitChildren(context); } + /// /// Visit a parse tree produced by . /// /// The default implementation returns the result of calling @@ -1662,6 +1672,16 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result. public virtual Result VisitNonReservedKeyword([NotNull] AccessSqlParser.NonReservedKeywordContext context) { return VisitChildren(context); } /// + /// Visit a parse tree produced by . + /// + /// The default implementation returns the result of calling + /// on . + /// + /// + /// The parse tree. + /// The visitor result. + public virtual Result VisitReservedKeyword([NotNull] AccessSqlParser.ReservedKeywordContext 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 4631fd55..3dd4e265 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs @@ -48,15 +48,15 @@ public const int NO=85, UNIQUE=86, CLUSTERED=87, IDENTITY=88, NONCLUSTERED=89, INDEX=90, TEMPORARY=91, WITH=92, COMPRESSION=93, COMP=94, DISALLOW=95, IGNORE=96, CHECK=97, VIEW=98, PROCEDURE=99, PARAMETERS=100, EXECUTE=101, EXEC=102, - ASC=103, DESC=104, TRUE=105, FALSE=106, NULL=107, RANGE=108, GROUPS=109, - UNBOUNDED=110, PRECEDING=111, FOLLOWING=112, CURRENT=113, EXCLUDE=114, - TIES=115, OTHERS=116, WITHIN=117, LAST=118, RESPECT=119, NULLS=120, FILTER=121, - STAR=122, SLASH=123, BACKSLASH=124, CARET=125, PLUS=126, MINUS=127, AMP=128, - EQ=129, NEQ=130, LTE=131, GTE=132, LT=133, GT=134, LPAREN=135, RPAREN=136, - COMMA=137, DOT=138, SEMI=139, SYSVAR=140, PARAM=141, HEX_LITERAL=142, - INTEGER_LITERAL=143, NUMBER_LITERAL=144, STRING_LITERAL=145, DATE_LITERAL=146, - GUID_LITERAL=147, BRACKET_ID=148, BACKTICK_ID=149, IDENTIFIER=150, WS=151, - LINE_COMMENT=152, BLOCK_COMMENT=153; + ASC=103, DESC=104, TRUE=105, FALSE=106, YES=107, OFF=108, NULL=109, RANGE=110, + GROUPS=111, UNBOUNDED=112, PRECEDING=113, FOLLOWING=114, CURRENT=115, + EXCLUDE=116, TIES=117, OTHERS=118, WITHIN=119, LAST=120, RESPECT=121, + NULLS=122, FILTER=123, STAR=124, SLASH=125, BACKSLASH=126, CARET=127, + PLUS=128, MINUS=129, AMP=130, EQ=131, NEQ=132, LTE=133, GTE=134, LT=135, + GT=136, LPAREN=137, RPAREN=138, COMMA=139, DOT=140, BANG=141, SEMI=142, + SYSVAR=143, PARAM=144, HEX_LITERAL=145, INTEGER_LITERAL=146, NUMBER_LITERAL=147, + STRING_LITERAL=148, DATE_LITERAL=149, GUID_LITERAL=150, BRACKET_ID=151, + BACKTICK_ID=152, IDENTIFIER=153, WS=154, LINE_COMMENT=155, BLOCK_COMMENT=156; public static string[] channelNames = { "DEFAULT_TOKEN_CHANNEL", "HIDDEN" }; @@ -79,14 +79,14 @@ public const int "SET", "DEFAULT", "NO", "UNIQUE", "CLUSTERED", "IDENTITY", "NONCLUSTERED", "INDEX", "TEMPORARY", "WITH", "COMPRESSION", "COMP", "DISALLOW", "IGNORE", "CHECK", "VIEW", "PROCEDURE", "PARAMETERS", "EXECUTE", "EXEC", "ASC", - "DESC", "TRUE", "FALSE", "NULL", "RANGE", "GROUPS", "UNBOUNDED", "PRECEDING", - "FOLLOWING", "CURRENT", "EXCLUDE", "TIES", "OTHERS", "WITHIN", "LAST", - "RESPECT", "NULLS", "FILTER", "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", - "GUID_BODY", "HEXDIGIT", "BRACKET_ID", "BACKTICK_ID", "IDENTIFIER", "WS", - "LINE_COMMENT", "BLOCK_COMMENT" + "DESC", "TRUE", "FALSE", "YES", "OFF", "NULL", "RANGE", "GROUPS", "UNBOUNDED", + "PRECEDING", "FOLLOWING", "CURRENT", "EXCLUDE", "TIES", "OTHERS", "WITHIN", + "LAST", "RESPECT", "NULLS", "FILTER", "STAR", "SLASH", "BACKSLASH", "CARET", + "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", "LT", "GT", "LPAREN", + "RPAREN", "COMMA", "DOT", "BANG", "SEMI", "SYSVAR", "PARAM", "HEX_LITERAL", + "INTEGER_LITERAL", "NUMBER_LITERAL", "EXPONENT", "STRING_LITERAL", "DATE_LITERAL", + "GUID_LITERAL", "GUID_BODY", "HEXDIGIT", "BRACKET_ID", "BACKTICK_ID", + "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT" }; @@ -121,8 +121,9 @@ 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, "'*'", "'/'", "'\\'", "'^'", "'+'", "'-'", "'&'", + "'='", null, "'<='", "'>='", "'<'", "'>'", "'('", "')'", "','", "'.'", + "'!'", "';'" }; private static readonly string[] _SymbolicNames = { null, "SELECT", "FROM", "WHERE", "TOP", "AS", "AND", "OR", "NOT", "XOR", @@ -138,13 +139,14 @@ public AccessSqlLexer(ICharStream input, TextWriter output, TextWriter errorOutp "SET", "DEFAULT", "NO", "UNIQUE", "CLUSTERED", "IDENTITY", "NONCLUSTERED", "INDEX", "TEMPORARY", "WITH", "COMPRESSION", "COMP", "DISALLOW", "IGNORE", "CHECK", "VIEW", "PROCEDURE", "PARAMETERS", "EXECUTE", "EXEC", "ASC", - "DESC", "TRUE", "FALSE", "NULL", "RANGE", "GROUPS", "UNBOUNDED", "PRECEDING", - "FOLLOWING", "CURRENT", "EXCLUDE", "TIES", "OTHERS", "WITHIN", "LAST", - "RESPECT", "NULLS", "FILTER", "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", "STRING_LITERAL", "DATE_LITERAL", "GUID_LITERAL", "BRACKET_ID", - "BACKTICK_ID", "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT" + "DESC", "TRUE", "FALSE", "YES", "OFF", "NULL", "RANGE", "GROUPS", "UNBOUNDED", + "PRECEDING", "FOLLOWING", "CURRENT", "EXCLUDE", "TIES", "OTHERS", "WITHIN", + "LAST", "RESPECT", "NULLS", "FILTER", "STAR", "SLASH", "BACKSLASH", "CARET", + "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", "LT", "GT", "LPAREN", + "RPAREN", "COMMA", "DOT", "BANG", "SEMI", "SYSVAR", "PARAM", "HEX_LITERAL", + "INTEGER_LITERAL", "NUMBER_LITERAL", "STRING_LITERAL", "DATE_LITERAL", + "GUID_LITERAL", "BRACKET_ID", "BACKTICK_ID", "IDENTIFIER", "WS", "LINE_COMMENT", + "BLOCK_COMMENT" }; public static readonly IVocabulary DefaultVocabulary = new Vocabulary(_LiteralNames, _SymbolicNames); @@ -175,7 +177,7 @@ static AccessSqlLexer() { } public override bool Sempred(RuleContext _localctx, int ruleIndex, int predIndex) { switch (ruleIndex) { - case 154 : return LINE_COMMENT_sempred(_localctx, predIndex); + case 157 : return LINE_COMMENT_sempred(_localctx, predIndex); } return true; } @@ -187,7 +189,7 @@ private bool LINE_COMMENT_sempred(RuleContext _localctx, int predIndex) { } private static int[] _serializedATN = { - 4,0,153,1346,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,156,1362,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, @@ -210,438 +212,672 @@ private bool LINE_COMMENT_sempred(RuleContext _localctx, int predIndex) { 7,134,2,135,7,135,2,136,7,136,2,137,7,137,2,138,7,138,2,139,7,139,2,140, 7,140,2,141,7,141,2,142,7,142,2,143,7,143,2,144,7,144,2,145,7,145,2,146, 7,146,2,147,7,147,2,148,7,148,2,149,7,149,2,150,7,150,2,151,7,151,2,152, - 7,152,2,153,7,153,2,154,7,154,2,155,7,155,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,9,1,9, - 1,9,1,9,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,13,1,13,1,13,1,13,1,13,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,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,19,1,19,1,19,1,20,1,20,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,23,1,23,1,23,1, - 24,1,24,1,24,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,27,1,27,1,27,1,28,1,28,1,28,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,31,1,31,1,31,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,33,1,33,1,33,1,33,1,34,1, - 34,1,34,1,34,1,34,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,36,1,36,1,36,1,36,1,36,1,36,1,37,1,37,1,37,1,37,1,37,1,37,1, - 38,1,38,1,38,1,38,1,38,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1, - 39,1,40,1,40,1,40,1,40,1,40,1,41,1,41,1,41,1,41,1,41,1,42,1,42,1,42,1, - 42,1,42,1,43,1,43,1,43,1,43,1,44,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,46,1,46,1,46,1,46,1,46,1,47,1,47,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,50,1,50,1,50,1, - 50,1,50,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,52,1,52,1,52,1,52,1, - 52,1,52,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, - 54,1,54,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,57,1,57,1,57,1,57,1,57,1,57,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,60,1,61,1,61,1,61,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,63,1,63,1,63,1,63,1,63,1,63,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,67,1,67,1,67,1, - 67,1,67,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, - 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, - 72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,73,1,73,1,73,1,73,1,74,1,74,1, - 74,1,74,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,75,1,75,1,76,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,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,80,1, - 80,1,80,1,80,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,82,1,82,1,82,1,82,1, - 83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,84,1,84,1,84,1,85,1,85,1,85,1, - 85,1,85,1,85,1,85,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1, - 87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,88,1,88,1,88,1,88,1,88,1, - 88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,89,1,89,1,89,1,89,1,89,1,89,1, - 90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,91,1,91,1,91,1,91,1, - 91,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,93,1, - 93,1,93,1,93,1,93,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,95,1, - 95,1,95,1,95,1,95,1,95,1,95,1,96,1,96,1,96,1,96,1,96,1,96,1,97,1,97,1, - 97,1,97,1,97,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,99,1, - 99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,100,1,100,1,100,1,100, - 1,100,1,100,1,100,1,100,1,101,1,101,1,101,1,101,1,101,1,102,1,102,1,102, - 1,102,1,103,1,103,1,103,1,103,1,103,1,104,1,104,1,104,1,104,1,104,1,105, - 1,105,1,105,1,105,1,105,1,105,1,106,1,106,1,106,1,106,1,106,1,107,1,107, - 1,107,1,107,1,107,1,107,1,108,1,108,1,108,1,108,1,108,1,108,1,108,1,109, - 1,109,1,109,1,109,1,109,1,109,1,109,1,109,1,109,1,109,1,110,1,110,1,110, - 1,110,1,110,1,110,1,110,1,110,1,110,1,110,1,111,1,111,1,111,1,111,1,111, - 1,111,1,111,1,111,1,111,1,111,1,112,1,112,1,112,1,112,1,112,1,112,1,112, - 1,112,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,114,1,114,1,114, - 1,114,1,114,1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,116,1,116,1,116, - 1,116,1,116,1,116,1,116,1,117,1,117,1,117,1,117,1,117,1,118,1,118,1,118, - 1,118,1,118,1,118,1,118,1,118,1,119,1,119,1,119,1,119,1,119,1,119,1,120, - 1,120,1,120,1,120,1,120,1,120,1,120,1,121,1,121,1,122,1,122,1,123,1,123, - 1,124,1,124,1,125,1,125,1,126,1,126,1,127,1,127,1,128,1,128,1,129,1,129, - 1,129,1,129,3,129,1102,8,129,1,130,1,130,1,130,1,131,1,131,1,131,1,132, - 1,132,1,133,1,133,1,134,1,134,1,135,1,135,1,136,1,136,1,137,1,137,1,138, - 1,138,1,139,1,139,1,139,1,139,1,139,5,139,1129,8,139,10,139,12,139,1132, - 9,139,1,140,1,140,1,140,1,140,5,140,1138,8,140,10,140,12,140,1141,9,140, - 3,140,1143,8,140,1,141,1,141,1,141,4,141,1148,8,141,11,141,12,141,1149, - 1,142,4,142,1153,8,142,11,142,12,142,1154,1,143,4,143,1158,8,143,11,143, - 12,143,1159,1,143,1,143,5,143,1164,8,143,10,143,12,143,1167,9,143,1,143, - 3,143,1170,8,143,1,143,1,143,4,143,1174,8,143,11,143,12,143,1175,1,143, - 3,143,1179,8,143,1,143,4,143,1182,8,143,11,143,12,143,1183,1,143,3,143, - 1187,8,143,1,144,1,144,3,144,1191,8,144,1,144,4,144,1194,8,144,11,144, - 12,144,1195,1,145,1,145,1,145,1,145,5,145,1202,8,145,10,145,12,145,1205, - 9,145,1,145,1,145,1,145,1,145,1,145,5,145,1212,8,145,10,145,12,145,1215, - 9,145,1,145,3,145,1218,8,145,1,146,1,146,5,146,1222,8,146,10,146,12,146, - 1225,9,146,1,146,1,146,1,147,1,147,1,147,1,147,1,147,1,147,1,147,1,147, - 1,147,1,147,5,147,1239,8,147,10,147,12,147,1242,9,147,1,147,1,147,1,147, - 1,147,5,147,1248,8,147,10,147,12,147,1251,9,147,1,147,1,147,3,147,1255, - 8,147,1,148,4,148,1258,8,148,11,148,12,148,1259,1,148,1,148,4,148,1264, - 8,148,11,148,12,148,1265,1,148,1,148,4,148,1270,8,148,11,148,12,148,1271, - 1,148,1,148,4,148,1276,8,148,11,148,12,148,1277,1,148,1,148,4,148,1282, - 8,148,11,148,12,148,1283,1,149,1,149,1,150,1,150,4,150,1290,8,150,11,150, - 12,150,1291,1,150,1,150,1,151,1,151,4,151,1298,8,151,11,151,12,151,1299, - 1,151,1,151,1,152,1,152,5,152,1306,8,152,10,152,12,152,1309,9,152,1,152, - 3,152,1312,8,152,1,153,4,153,1315,8,153,11,153,12,153,1316,1,153,1,153, - 1,154,1,154,1,154,1,154,1,154,5,154,1326,8,154,10,154,12,154,1329,9,154, - 1,154,1,154,1,155,1,155,1,155,1,155,5,155,1337,8,155,10,155,12,155,1340, - 9,155,1,155,1,155,1,155,1,155,1,155,1,1338,0,156,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,110,221,111,223,112, - 225,113,227,114,229,115,231,116,233,117,235,118,237,119,239,120,241,121, - 243,122,245,123,247,124,249,125,251,126,253,127,255,128,257,129,259,130, - 261,131,263,132,265,133,267,134,269,135,271,136,273,137,275,138,277,139, - 279,140,281,141,283,142,285,143,287,144,289,0,291,145,293,146,295,147, - 297,0,299,0,301,148,303,149,305,150,307,151,309,152,311,153,1,0,38,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,88,88,120,120, - 2,0,81,81,113,113,2,0,86,86,118,118,2,0,73,73,105,105,2,0,66,66,98,98, - 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,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,2,0,9,9,32,32,1,0,93,93,1,0,96,96,3,0,9,10,13,13,32,32,2,0,10, - 10,13,13,1379,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,219,1,0,0,0,0,221,1,0,0,0,0,223,1,0,0,0,0,225,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,0,239,1,0,0,0,0,241,1,0,0,0,0,243,1,0,0,0,0,245,1,0,0,0,0,247,1, - 0,0,0,0,249,1,0,0,0,0,251,1,0,0,0,0,253,1,0,0,0,0,255,1,0,0,0,0,257,1, - 0,0,0,0,259,1,0,0,0,0,261,1,0,0,0,0,263,1,0,0,0,0,265,1,0,0,0,0,267,1, - 0,0,0,0,269,1,0,0,0,0,271,1,0,0,0,0,273,1,0,0,0,0,275,1,0,0,0,0,277,1, - 0,0,0,0,279,1,0,0,0,0,281,1,0,0,0,0,283,1,0,0,0,0,285,1,0,0,0,0,287,1, - 0,0,0,0,291,1,0,0,0,0,293,1,0,0,0,0,295,1,0,0,0,0,301,1,0,0,0,0,303,1, - 0,0,0,0,305,1,0,0,0,0,307,1,0,0,0,0,309,1,0,0,0,0,311,1,0,0,0,1,313,1, - 0,0,0,3,320,1,0,0,0,5,325,1,0,0,0,7,331,1,0,0,0,9,335,1,0,0,0,11,338,1, - 0,0,0,13,342,1,0,0,0,15,345,1,0,0,0,17,349,1,0,0,0,19,353,1,0,0,0,21,357, - 1,0,0,0,23,361,1,0,0,0,25,366,1,0,0,0,27,370,1,0,0,0,29,375,1,0,0,0,31, - 380,1,0,0,0,33,385,1,0,0,0,35,389,1,0,0,0,37,395,1,0,0,0,39,400,1,0,0, - 0,41,406,1,0,0,0,43,411,1,0,0,0,45,417,1,0,0,0,47,422,1,0,0,0,49,425,1, - 0,0,0,51,428,1,0,0,0,53,434,1,0,0,0,55,440,1,0,0,0,57,443,1,0,0,0,59,446, - 1,0,0,0,61,453,1,0,0,0,63,460,1,0,0,0,65,463,1,0,0,0,67,468,1,0,0,0,69, - 480,1,0,0,0,71,489,1,0,0,0,73,497,1,0,0,0,75,503,1,0,0,0,77,509,1,0,0, - 0,79,514,1,0,0,0,81,524,1,0,0,0,83,529,1,0,0,0,85,534,1,0,0,0,87,539,1, - 0,0,0,89,543,1,0,0,0,91,550,1,0,0,0,93,556,1,0,0,0,95,561,1,0,0,0,97,567, - 1,0,0,0,99,572,1,0,0,0,101,576,1,0,0,0,103,581,1,0,0,0,105,589,1,0,0,0, - 107,595,1,0,0,0,109,599,1,0,0,0,111,609,1,0,0,0,113,616,1,0,0,0,115,623, - 1,0,0,0,117,629,1,0,0,0,119,635,1,0,0,0,121,642,1,0,0,0,123,651,1,0,0, - 0,125,663,1,0,0,0,127,668,1,0,0,0,129,674,1,0,0,0,131,681,1,0,0,0,133, - 684,1,0,0,0,135,688,1,0,0,0,137,693,1,0,0,0,139,700,1,0,0,0,141,707,1, - 0,0,0,143,712,1,0,0,0,145,719,1,0,0,0,147,727,1,0,0,0,149,731,1,0,0,0, - 151,742,1,0,0,0,153,750,1,0,0,0,155,761,1,0,0,0,157,768,1,0,0,0,159,775, - 1,0,0,0,161,783,1,0,0,0,163,792,1,0,0,0,165,799,1,0,0,0,167,803,1,0,0, - 0,169,811,1,0,0,0,171,814,1,0,0,0,173,821,1,0,0,0,175,831,1,0,0,0,177, - 840,1,0,0,0,179,853,1,0,0,0,181,859,1,0,0,0,183,869,1,0,0,0,185,874,1, - 0,0,0,187,886,1,0,0,0,189,891,1,0,0,0,191,900,1,0,0,0,193,907,1,0,0,0, - 195,913,1,0,0,0,197,918,1,0,0,0,199,928,1,0,0,0,201,939,1,0,0,0,203,947, - 1,0,0,0,205,952,1,0,0,0,207,956,1,0,0,0,209,961,1,0,0,0,211,966,1,0,0, - 0,213,972,1,0,0,0,215,977,1,0,0,0,217,983,1,0,0,0,219,990,1,0,0,0,221, - 1000,1,0,0,0,223,1010,1,0,0,0,225,1020,1,0,0,0,227,1028,1,0,0,0,229,1036, - 1,0,0,0,231,1041,1,0,0,0,233,1048,1,0,0,0,235,1055,1,0,0,0,237,1060,1, - 0,0,0,239,1068,1,0,0,0,241,1074,1,0,0,0,243,1081,1,0,0,0,245,1083,1,0, - 0,0,247,1085,1,0,0,0,249,1087,1,0,0,0,251,1089,1,0,0,0,253,1091,1,0,0, - 0,255,1093,1,0,0,0,257,1095,1,0,0,0,259,1101,1,0,0,0,261,1103,1,0,0,0, - 263,1106,1,0,0,0,265,1109,1,0,0,0,267,1111,1,0,0,0,269,1113,1,0,0,0,271, - 1115,1,0,0,0,273,1117,1,0,0,0,275,1119,1,0,0,0,277,1121,1,0,0,0,279,1123, - 1,0,0,0,281,1142,1,0,0,0,283,1144,1,0,0,0,285,1152,1,0,0,0,287,1186,1, - 0,0,0,289,1188,1,0,0,0,291,1217,1,0,0,0,293,1219,1,0,0,0,295,1254,1,0, - 0,0,297,1257,1,0,0,0,299,1285,1,0,0,0,301,1287,1,0,0,0,303,1295,1,0,0, - 0,305,1303,1,0,0,0,307,1314,1,0,0,0,309,1320,1,0,0,0,311,1332,1,0,0,0, - 313,314,7,0,0,0,314,315,7,1,0,0,315,316,7,2,0,0,316,317,7,1,0,0,317,318, - 7,3,0,0,318,319,7,4,0,0,319,2,1,0,0,0,320,321,7,5,0,0,321,322,7,6,0,0, - 322,323,7,7,0,0,323,324,7,8,0,0,324,4,1,0,0,0,325,326,7,9,0,0,326,327, - 7,10,0,0,327,328,7,1,0,0,328,329,7,6,0,0,329,330,7,1,0,0,330,6,1,0,0,0, - 331,332,7,4,0,0,332,333,7,7,0,0,333,334,7,11,0,0,334,8,1,0,0,0,335,336, - 7,12,0,0,336,337,7,0,0,0,337,10,1,0,0,0,338,339,7,12,0,0,339,340,7,13, - 0,0,340,341,7,14,0,0,341,12,1,0,0,0,342,343,7,7,0,0,343,344,7,6,0,0,344, - 14,1,0,0,0,345,346,7,13,0,0,346,347,7,7,0,0,347,348,7,4,0,0,348,16,1,0, - 0,0,349,350,7,15,0,0,350,351,7,7,0,0,351,352,7,6,0,0,352,18,1,0,0,0,353, - 354,7,1,0,0,354,355,7,16,0,0,355,356,7,17,0,0,356,20,1,0,0,0,357,358,7, - 18,0,0,358,359,7,8,0,0,359,360,7,11,0,0,360,22,1,0,0,0,361,362,7,19,0, - 0,362,363,7,12,0,0,363,364,7,13,0,0,364,365,7,14,0,0,365,24,1,0,0,0,366, - 367,7,19,0,0,367,368,7,7,0,0,368,369,7,6,0,0,369,26,1,0,0,0,370,371,7, - 19,0,0,371,372,7,15,0,0,372,373,7,7,0,0,373,374,7,6,0,0,374,28,1,0,0,0, - 375,376,7,19,0,0,376,377,7,13,0,0,377,378,7,7,0,0,378,379,7,4,0,0,379, - 30,1,0,0,0,380,381,7,2,0,0,381,382,7,18,0,0,382,383,7,20,0,0,383,384,7, - 1,0,0,384,32,1,0,0,0,385,386,7,8,0,0,386,387,7,7,0,0,387,388,7,14,0,0, - 388,34,1,0,0,0,389,390,7,18,0,0,390,391,7,13,0,0,391,392,7,13,0,0,392, - 393,7,1,0,0,393,394,7,6,0,0,394,36,1,0,0,0,395,396,7,2,0,0,396,397,7,1, - 0,0,397,398,7,5,0,0,398,399,7,4,0,0,399,38,1,0,0,0,400,401,7,6,0,0,401, - 402,7,18,0,0,402,403,7,21,0,0,403,404,7,10,0,0,404,405,7,4,0,0,405,40, - 1,0,0,0,406,407,7,5,0,0,407,408,7,22,0,0,408,409,7,2,0,0,409,410,7,2,0, - 0,410,42,1,0,0,0,411,412,7,7,0,0,412,413,7,22,0,0,413,414,7,4,0,0,414, - 415,7,1,0,0,415,416,7,6,0,0,416,44,1,0,0,0,417,418,7,23,0,0,418,419,7, - 7,0,0,419,420,7,18,0,0,420,421,7,13,0,0,421,46,1,0,0,0,422,423,7,18,0, - 0,423,424,7,13,0,0,424,48,1,0,0,0,425,426,7,7,0,0,426,427,7,13,0,0,427, - 50,1,0,0,0,428,429,7,7,0,0,429,430,7,6,0,0,430,431,7,14,0,0,431,432,7, - 1,0,0,432,433,7,6,0,0,433,52,1,0,0,0,434,435,7,21,0,0,435,436,7,6,0,0, - 436,437,7,7,0,0,437,438,7,22,0,0,438,439,7,11,0,0,439,54,1,0,0,0,440,441, - 7,18,0,0,441,442,7,0,0,0,442,56,1,0,0,0,443,444,7,19,0,0,444,445,7,24, - 0,0,445,58,1,0,0,0,446,447,7,10,0,0,447,448,7,12,0,0,448,449,7,17,0,0, - 449,450,7,18,0,0,450,451,7,13,0,0,451,452,7,21,0,0,452,60,1,0,0,0,453, - 454,7,1,0,0,454,455,7,15,0,0,455,456,7,18,0,0,456,457,7,0,0,0,457,458, - 7,4,0,0,458,459,7,0,0,0,459,62,1,0,0,0,460,461,7,18,0,0,461,462,7,5,0, - 0,462,64,1,0,0,0,463,464,7,4,0,0,464,465,7,10,0,0,465,466,7,1,0,0,466, - 467,7,13,0,0,467,66,1,0,0,0,468,469,7,14,0,0,469,470,7,18,0,0,470,471, - 7,0,0,0,471,472,7,4,0,0,472,473,7,18,0,0,473,474,7,13,0,0,474,475,7,3, - 0,0,475,476,7,4,0,0,476,477,7,6,0,0,477,478,7,7,0,0,478,479,7,9,0,0,479, - 68,1,0,0,0,480,481,7,14,0,0,481,482,7,18,0,0,482,483,7,0,0,0,483,484,7, - 4,0,0,484,485,7,18,0,0,485,486,7,13,0,0,486,487,7,3,0,0,487,488,7,4,0, - 0,488,70,1,0,0,0,489,490,7,11,0,0,490,491,7,1,0,0,491,492,7,6,0,0,492, - 493,7,3,0,0,493,494,7,1,0,0,494,495,7,13,0,0,495,496,7,4,0,0,496,72,1, - 0,0,0,497,498,7,3,0,0,498,499,7,6,0,0,499,500,7,7,0,0,500,501,7,0,0,0, - 501,502,7,0,0,0,502,74,1,0,0,0,503,504,7,12,0,0,504,505,7,11,0,0,505,506, - 7,11,0,0,506,507,7,2,0,0,507,508,7,24,0,0,508,76,1,0,0,0,509,510,7,7,0, - 0,510,511,7,17,0,0,511,512,7,1,0,0,512,513,7,6,0,0,513,78,1,0,0,0,514, - 515,7,11,0,0,515,516,7,12,0,0,516,517,7,6,0,0,517,518,7,4,0,0,518,519, - 7,18,0,0,519,520,7,4,0,0,520,521,7,18,0,0,521,522,7,7,0,0,522,523,7,13, - 0,0,523,80,1,0,0,0,524,525,7,3,0,0,525,526,7,12,0,0,526,527,7,0,0,0,527, - 528,7,1,0,0,528,82,1,0,0,0,529,530,7,9,0,0,530,531,7,10,0,0,531,532,7, - 1,0,0,532,533,7,13,0,0,533,84,1,0,0,0,534,535,7,1,0,0,535,536,7,2,0,0, - 536,537,7,0,0,0,537,538,7,1,0,0,538,86,1,0,0,0,539,540,7,1,0,0,540,541, - 7,13,0,0,541,542,7,14,0,0,542,88,1,0,0,0,543,544,7,7,0,0,544,545,7,5,0, - 0,545,546,7,5,0,0,546,547,7,0,0,0,547,548,7,1,0,0,548,549,7,4,0,0,549, - 90,1,0,0,0,550,551,7,5,0,0,551,552,7,1,0,0,552,553,7,4,0,0,553,554,7,3, - 0,0,554,555,7,10,0,0,555,92,1,0,0,0,556,557,7,13,0,0,557,558,7,1,0,0,558, - 559,7,15,0,0,559,560,7,4,0,0,560,94,1,0,0,0,561,562,7,5,0,0,562,563,7, - 18,0,0,563,564,7,6,0,0,564,565,7,0,0,0,565,566,7,4,0,0,566,96,1,0,0,0, - 567,568,7,6,0,0,568,569,7,7,0,0,569,570,7,9,0,0,570,571,7,0,0,0,571,98, - 1,0,0,0,572,573,7,6,0,0,573,574,7,7,0,0,574,575,7,9,0,0,575,100,1,0,0, - 0,576,577,7,7,0,0,577,578,7,13,0,0,578,579,7,2,0,0,579,580,7,24,0,0,580, - 102,1,0,0,0,581,582,7,19,0,0,582,583,7,1,0,0,583,584,7,4,0,0,584,585,7, - 9,0,0,585,586,7,1,0,0,586,587,7,1,0,0,587,588,7,13,0,0,588,104,1,0,0,0, - 589,590,7,22,0,0,590,591,7,13,0,0,591,592,7,18,0,0,592,593,7,7,0,0,593, - 594,7,13,0,0,594,106,1,0,0,0,595,596,7,12,0,0,596,597,7,2,0,0,597,598, - 7,2,0,0,598,108,1,0,0,0,599,600,7,18,0,0,600,601,7,13,0,0,601,602,7,4, - 0,0,602,603,7,1,0,0,603,604,7,6,0,0,604,605,7,0,0,0,605,606,7,1,0,0,606, - 607,7,3,0,0,607,608,7,4,0,0,608,110,1,0,0,0,609,610,7,1,0,0,610,611,7, - 15,0,0,611,612,7,3,0,0,612,613,7,1,0,0,613,614,7,11,0,0,614,615,7,4,0, - 0,615,112,1,0,0,0,616,617,7,3,0,0,617,618,7,6,0,0,618,619,7,1,0,0,619, - 620,7,12,0,0,620,621,7,4,0,0,621,622,7,1,0,0,622,114,1,0,0,0,623,624,7, - 4,0,0,624,625,7,12,0,0,625,626,7,19,0,0,626,627,7,2,0,0,627,628,7,1,0, - 0,628,116,1,0,0,0,629,630,7,19,0,0,630,631,7,1,0,0,631,632,7,21,0,0,632, - 633,7,18,0,0,633,634,7,13,0,0,634,118,1,0,0,0,635,636,7,3,0,0,636,637, - 7,7,0,0,637,638,7,8,0,0,638,639,7,8,0,0,639,640,7,18,0,0,640,641,7,4,0, - 0,641,120,1,0,0,0,642,643,7,6,0,0,643,644,7,7,0,0,644,645,7,2,0,0,645, - 646,7,2,0,0,646,647,7,19,0,0,647,648,7,12,0,0,648,649,7,3,0,0,649,650, - 7,20,0,0,650,122,1,0,0,0,651,652,7,4,0,0,652,653,7,6,0,0,653,654,7,12, - 0,0,654,655,7,13,0,0,655,656,7,0,0,0,656,657,7,12,0,0,657,658,7,3,0,0, - 658,659,7,4,0,0,659,660,7,18,0,0,660,661,7,7,0,0,661,662,7,13,0,0,662, - 124,1,0,0,0,663,664,7,9,0,0,664,665,7,7,0,0,665,666,7,6,0,0,666,667,7, - 20,0,0,667,126,1,0,0,0,668,669,7,12,0,0,669,670,7,2,0,0,670,671,7,4,0, - 0,671,672,7,1,0,0,672,673,7,6,0,0,673,128,1,0,0,0,674,675,7,6,0,0,675, - 676,7,1,0,0,676,677,7,13,0,0,677,678,7,12,0,0,678,679,7,8,0,0,679,680, - 7,1,0,0,680,130,1,0,0,0,681,682,7,4,0,0,682,683,7,7,0,0,683,132,1,0,0, - 0,684,685,7,12,0,0,685,686,7,14,0,0,686,687,7,14,0,0,687,134,1,0,0,0,688, - 689,7,14,0,0,689,690,7,6,0,0,690,691,7,7,0,0,691,692,7,11,0,0,692,136, - 1,0,0,0,693,694,7,3,0,0,694,695,7,7,0,0,695,696,7,2,0,0,696,697,7,22,0, - 0,697,698,7,8,0,0,698,699,7,13,0,0,699,138,1,0,0,0,700,701,7,18,0,0,701, - 702,7,13,0,0,702,703,7,0,0,0,703,704,7,1,0,0,704,705,7,6,0,0,705,706,7, - 4,0,0,706,140,1,0,0,0,707,708,7,18,0,0,708,709,7,13,0,0,709,710,7,4,0, - 0,710,711,7,7,0,0,711,142,1,0,0,0,712,713,7,17,0,0,713,714,7,12,0,0,714, - 715,7,2,0,0,715,716,7,22,0,0,716,717,7,1,0,0,717,718,7,0,0,0,718,144,1, - 0,0,0,719,720,7,11,0,0,720,721,7,6,0,0,721,722,7,18,0,0,722,723,7,8,0, - 0,723,724,7,12,0,0,724,725,7,6,0,0,725,726,7,24,0,0,726,146,1,0,0,0,727, - 728,7,20,0,0,728,729,7,1,0,0,729,730,7,24,0,0,730,148,1,0,0,0,731,732, - 7,3,0,0,732,733,7,7,0,0,733,734,7,13,0,0,734,735,7,0,0,0,735,736,7,4,0, - 0,736,737,7,6,0,0,737,738,7,12,0,0,738,739,7,18,0,0,739,740,7,13,0,0,740, - 741,7,4,0,0,741,150,1,0,0,0,742,743,7,5,0,0,743,744,7,7,0,0,744,745,7, - 6,0,0,745,746,7,1,0,0,746,747,7,18,0,0,747,748,7,21,0,0,748,749,7,13,0, - 0,749,152,1,0,0,0,750,751,7,6,0,0,751,752,7,1,0,0,752,753,7,5,0,0,753, - 754,7,1,0,0,754,755,7,6,0,0,755,756,7,1,0,0,756,757,7,13,0,0,757,758,7, - 3,0,0,758,759,7,1,0,0,759,760,7,0,0,0,760,154,1,0,0,0,761,762,7,14,0,0, - 762,763,7,1,0,0,763,764,7,2,0,0,764,765,7,1,0,0,765,766,7,4,0,0,766,767, - 7,1,0,0,767,156,1,0,0,0,768,769,7,22,0,0,769,770,7,11,0,0,770,771,7,14, - 0,0,771,772,7,12,0,0,772,773,7,4,0,0,773,774,7,1,0,0,774,158,1,0,0,0,775, - 776,7,3,0,0,776,777,7,12,0,0,777,778,7,0,0,0,778,779,7,3,0,0,779,780,7, - 12,0,0,780,781,7,14,0,0,781,782,7,1,0,0,782,160,1,0,0,0,783,784,7,6,0, - 0,784,785,7,1,0,0,785,786,7,0,0,0,786,787,7,4,0,0,787,788,7,6,0,0,788, - 789,7,18,0,0,789,790,7,3,0,0,790,791,7,4,0,0,791,162,1,0,0,0,792,793,7, - 12,0,0,793,794,7,3,0,0,794,795,7,4,0,0,795,796,7,18,0,0,796,797,7,7,0, - 0,797,798,7,13,0,0,798,164,1,0,0,0,799,800,7,0,0,0,800,801,7,1,0,0,801, - 802,7,4,0,0,802,166,1,0,0,0,803,804,7,14,0,0,804,805,7,1,0,0,805,806,7, - 5,0,0,806,807,7,12,0,0,807,808,7,22,0,0,808,809,7,2,0,0,809,810,7,4,0, - 0,810,168,1,0,0,0,811,812,7,13,0,0,812,813,7,7,0,0,813,170,1,0,0,0,814, - 815,7,22,0,0,815,816,7,13,0,0,816,817,7,18,0,0,817,818,7,16,0,0,818,819, - 7,22,0,0,819,820,7,1,0,0,820,172,1,0,0,0,821,822,7,3,0,0,822,823,7,2,0, - 0,823,824,7,22,0,0,824,825,7,0,0,0,825,826,7,4,0,0,826,827,7,1,0,0,827, - 828,7,6,0,0,828,829,7,1,0,0,829,830,7,14,0,0,830,174,1,0,0,0,831,832,7, - 18,0,0,832,833,7,14,0,0,833,834,7,1,0,0,834,835,7,13,0,0,835,836,7,4,0, - 0,836,837,7,18,0,0,837,838,7,4,0,0,838,839,7,24,0,0,839,176,1,0,0,0,840, - 841,7,13,0,0,841,842,7,7,0,0,842,843,7,13,0,0,843,844,7,3,0,0,844,845, - 7,2,0,0,845,846,7,22,0,0,846,847,7,0,0,0,847,848,7,4,0,0,848,849,7,1,0, - 0,849,850,7,6,0,0,850,851,7,1,0,0,851,852,7,14,0,0,852,178,1,0,0,0,853, - 854,7,18,0,0,854,855,7,13,0,0,855,856,7,14,0,0,856,857,7,1,0,0,857,858, - 7,15,0,0,858,180,1,0,0,0,859,860,7,4,0,0,860,861,7,1,0,0,861,862,7,8,0, - 0,862,863,7,11,0,0,863,864,7,7,0,0,864,865,7,6,0,0,865,866,7,12,0,0,866, - 867,7,6,0,0,867,868,7,24,0,0,868,182,1,0,0,0,869,870,7,9,0,0,870,871,7, - 18,0,0,871,872,7,4,0,0,872,873,7,10,0,0,873,184,1,0,0,0,874,875,7,3,0, - 0,875,876,7,7,0,0,876,877,7,8,0,0,877,878,7,11,0,0,878,879,7,6,0,0,879, - 880,7,1,0,0,880,881,7,0,0,0,881,882,7,0,0,0,882,883,7,18,0,0,883,884,7, - 7,0,0,884,885,7,13,0,0,885,186,1,0,0,0,886,887,7,3,0,0,887,888,7,7,0,0, - 888,889,7,8,0,0,889,890,7,11,0,0,890,188,1,0,0,0,891,892,7,14,0,0,892, - 893,7,18,0,0,893,894,7,0,0,0,894,895,7,12,0,0,895,896,7,2,0,0,896,897, - 7,2,0,0,897,898,7,7,0,0,898,899,7,9,0,0,899,190,1,0,0,0,900,901,7,18,0, - 0,901,902,7,21,0,0,902,903,7,13,0,0,903,904,7,7,0,0,904,905,7,6,0,0,905, - 906,7,1,0,0,906,192,1,0,0,0,907,908,7,3,0,0,908,909,7,10,0,0,909,910,7, - 1,0,0,910,911,7,3,0,0,911,912,7,20,0,0,912,194,1,0,0,0,913,914,7,17,0, - 0,914,915,7,18,0,0,915,916,7,1,0,0,916,917,7,9,0,0,917,196,1,0,0,0,918, - 919,7,11,0,0,919,920,7,6,0,0,920,921,7,7,0,0,921,922,7,3,0,0,922,923,7, - 1,0,0,923,924,7,14,0,0,924,925,7,22,0,0,925,926,7,6,0,0,926,927,7,1,0, - 0,927,198,1,0,0,0,928,929,7,11,0,0,929,930,7,12,0,0,930,931,7,6,0,0,931, - 932,7,12,0,0,932,933,7,8,0,0,933,934,7,1,0,0,934,935,7,4,0,0,935,936,7, - 1,0,0,936,937,7,6,0,0,937,938,7,0,0,0,938,200,1,0,0,0,939,940,7,1,0,0, - 940,941,7,15,0,0,941,942,7,1,0,0,942,943,7,3,0,0,943,944,7,22,0,0,944, - 945,7,4,0,0,945,946,7,1,0,0,946,202,1,0,0,0,947,948,7,1,0,0,948,949,7, - 15,0,0,949,950,7,1,0,0,950,951,7,3,0,0,951,204,1,0,0,0,952,953,7,12,0, - 0,953,954,7,0,0,0,954,955,7,3,0,0,955,206,1,0,0,0,956,957,7,14,0,0,957, - 958,7,1,0,0,958,959,7,0,0,0,959,960,7,3,0,0,960,208,1,0,0,0,961,962,7, - 4,0,0,962,963,7,6,0,0,963,964,7,22,0,0,964,965,7,1,0,0,965,210,1,0,0,0, - 966,967,7,5,0,0,967,968,7,12,0,0,968,969,7,2,0,0,969,970,7,0,0,0,970,971, - 7,1,0,0,971,212,1,0,0,0,972,973,7,13,0,0,973,974,7,22,0,0,974,975,7,2, - 0,0,975,976,7,2,0,0,976,214,1,0,0,0,977,978,7,6,0,0,978,979,7,12,0,0,979, - 980,7,13,0,0,980,981,7,21,0,0,981,982,7,1,0,0,982,216,1,0,0,0,983,984, - 7,21,0,0,984,985,7,6,0,0,985,986,7,7,0,0,986,987,7,22,0,0,987,988,7,11, - 0,0,988,989,7,0,0,0,989,218,1,0,0,0,990,991,7,22,0,0,991,992,7,13,0,0, - 992,993,7,19,0,0,993,994,7,7,0,0,994,995,7,22,0,0,995,996,7,13,0,0,996, - 997,7,14,0,0,997,998,7,1,0,0,998,999,7,14,0,0,999,220,1,0,0,0,1000,1001, - 7,11,0,0,1001,1002,7,6,0,0,1002,1003,7,1,0,0,1003,1004,7,3,0,0,1004,1005, - 7,1,0,0,1005,1006,7,14,0,0,1006,1007,7,18,0,0,1007,1008,7,13,0,0,1008, - 1009,7,21,0,0,1009,222,1,0,0,0,1010,1011,7,5,0,0,1011,1012,7,7,0,0,1012, - 1013,7,2,0,0,1013,1014,7,2,0,0,1014,1015,7,7,0,0,1015,1016,7,9,0,0,1016, - 1017,7,18,0,0,1017,1018,7,13,0,0,1018,1019,7,21,0,0,1019,224,1,0,0,0,1020, - 1021,7,3,0,0,1021,1022,7,22,0,0,1022,1023,7,6,0,0,1023,1024,7,6,0,0,1024, - 1025,7,1,0,0,1025,1026,7,13,0,0,1026,1027,7,4,0,0,1027,226,1,0,0,0,1028, - 1029,7,1,0,0,1029,1030,7,15,0,0,1030,1031,7,3,0,0,1031,1032,7,2,0,0,1032, - 1033,7,22,0,0,1033,1034,7,14,0,0,1034,1035,7,1,0,0,1035,228,1,0,0,0,1036, - 1037,7,4,0,0,1037,1038,7,18,0,0,1038,1039,7,1,0,0,1039,1040,7,0,0,0,1040, - 230,1,0,0,0,1041,1042,7,7,0,0,1042,1043,7,4,0,0,1043,1044,7,10,0,0,1044, - 1045,7,1,0,0,1045,1046,7,6,0,0,1046,1047,7,0,0,0,1047,232,1,0,0,0,1048, - 1049,7,9,0,0,1049,1050,7,18,0,0,1050,1051,7,4,0,0,1051,1052,7,10,0,0,1052, - 1053,7,18,0,0,1053,1054,7,13,0,0,1054,234,1,0,0,0,1055,1056,7,2,0,0,1056, - 1057,7,12,0,0,1057,1058,7,0,0,0,1058,1059,7,4,0,0,1059,236,1,0,0,0,1060, - 1061,7,6,0,0,1061,1062,7,1,0,0,1062,1063,7,0,0,0,1063,1064,7,11,0,0,1064, - 1065,7,1,0,0,1065,1066,7,3,0,0,1066,1067,7,4,0,0,1067,238,1,0,0,0,1068, - 1069,7,13,0,0,1069,1070,7,22,0,0,1070,1071,7,2,0,0,1071,1072,7,2,0,0,1072, - 1073,7,0,0,0,1073,240,1,0,0,0,1074,1075,7,5,0,0,1075,1076,7,18,0,0,1076, - 1077,7,2,0,0,1077,1078,7,4,0,0,1078,1079,7,1,0,0,1079,1080,7,6,0,0,1080, - 242,1,0,0,0,1081,1082,5,42,0,0,1082,244,1,0,0,0,1083,1084,5,47,0,0,1084, - 246,1,0,0,0,1085,1086,5,92,0,0,1086,248,1,0,0,0,1087,1088,5,94,0,0,1088, - 250,1,0,0,0,1089,1090,5,43,0,0,1090,252,1,0,0,0,1091,1092,5,45,0,0,1092, - 254,1,0,0,0,1093,1094,5,38,0,0,1094,256,1,0,0,0,1095,1096,5,61,0,0,1096, - 258,1,0,0,0,1097,1098,5,60,0,0,1098,1102,5,62,0,0,1099,1100,5,33,0,0,1100, - 1102,5,61,0,0,1101,1097,1,0,0,0,1101,1099,1,0,0,0,1102,260,1,0,0,0,1103, - 1104,5,60,0,0,1104,1105,5,61,0,0,1105,262,1,0,0,0,1106,1107,5,62,0,0,1107, - 1108,5,61,0,0,1108,264,1,0,0,0,1109,1110,5,60,0,0,1110,266,1,0,0,0,1111, - 1112,5,62,0,0,1112,268,1,0,0,0,1113,1114,5,40,0,0,1114,270,1,0,0,0,1115, - 1116,5,41,0,0,1116,272,1,0,0,0,1117,1118,5,44,0,0,1118,274,1,0,0,0,1119, - 1120,5,46,0,0,1120,276,1,0,0,0,1121,1122,5,59,0,0,1122,278,1,0,0,0,1123, - 1124,5,64,0,0,1124,1125,5,64,0,0,1125,1126,1,0,0,0,1126,1130,7,25,0,0, - 1127,1129,7,26,0,0,1128,1127,1,0,0,0,1129,1132,1,0,0,0,1130,1128,1,0,0, - 0,1130,1131,1,0,0,0,1131,280,1,0,0,0,1132,1130,1,0,0,0,1133,1143,5,63, - 0,0,1134,1135,5,64,0,0,1135,1139,7,25,0,0,1136,1138,7,26,0,0,1137,1136, - 1,0,0,0,1138,1141,1,0,0,0,1139,1137,1,0,0,0,1139,1140,1,0,0,0,1140,1143, - 1,0,0,0,1141,1139,1,0,0,0,1142,1133,1,0,0,0,1142,1134,1,0,0,0,1143,282, - 1,0,0,0,1144,1145,5,48,0,0,1145,1147,7,15,0,0,1146,1148,7,27,0,0,1147, - 1146,1,0,0,0,1148,1149,1,0,0,0,1149,1147,1,0,0,0,1149,1150,1,0,0,0,1150, - 284,1,0,0,0,1151,1153,7,28,0,0,1152,1151,1,0,0,0,1153,1154,1,0,0,0,1154, - 1152,1,0,0,0,1154,1155,1,0,0,0,1155,286,1,0,0,0,1156,1158,7,28,0,0,1157, - 1156,1,0,0,0,1158,1159,1,0,0,0,1159,1157,1,0,0,0,1159,1160,1,0,0,0,1160, - 1161,1,0,0,0,1161,1165,5,46,0,0,1162,1164,7,28,0,0,1163,1162,1,0,0,0,1164, - 1167,1,0,0,0,1165,1163,1,0,0,0,1165,1166,1,0,0,0,1166,1169,1,0,0,0,1167, - 1165,1,0,0,0,1168,1170,3,289,144,0,1169,1168,1,0,0,0,1169,1170,1,0,0,0, - 1170,1187,1,0,0,0,1171,1173,5,46,0,0,1172,1174,7,28,0,0,1173,1172,1,0, - 0,0,1174,1175,1,0,0,0,1175,1173,1,0,0,0,1175,1176,1,0,0,0,1176,1178,1, - 0,0,0,1177,1179,3,289,144,0,1178,1177,1,0,0,0,1178,1179,1,0,0,0,1179,1187, - 1,0,0,0,1180,1182,7,28,0,0,1181,1180,1,0,0,0,1182,1183,1,0,0,0,1183,1181, - 1,0,0,0,1183,1184,1,0,0,0,1184,1185,1,0,0,0,1185,1187,3,289,144,0,1186, - 1157,1,0,0,0,1186,1171,1,0,0,0,1186,1181,1,0,0,0,1187,288,1,0,0,0,1188, - 1190,7,1,0,0,1189,1191,7,29,0,0,1190,1189,1,0,0,0,1190,1191,1,0,0,0,1191, - 1193,1,0,0,0,1192,1194,7,28,0,0,1193,1192,1,0,0,0,1194,1195,1,0,0,0,1195, - 1193,1,0,0,0,1195,1196,1,0,0,0,1196,290,1,0,0,0,1197,1203,5,34,0,0,1198, - 1202,8,30,0,0,1199,1200,5,34,0,0,1200,1202,5,34,0,0,1201,1198,1,0,0,0, - 1201,1199,1,0,0,0,1202,1205,1,0,0,0,1203,1201,1,0,0,0,1203,1204,1,0,0, - 0,1204,1206,1,0,0,0,1205,1203,1,0,0,0,1206,1218,5,34,0,0,1207,1213,5,39, - 0,0,1208,1212,8,31,0,0,1209,1210,5,39,0,0,1210,1212,5,39,0,0,1211,1208, - 1,0,0,0,1211,1209,1,0,0,0,1212,1215,1,0,0,0,1213,1211,1,0,0,0,1213,1214, - 1,0,0,0,1214,1216,1,0,0,0,1215,1213,1,0,0,0,1216,1218,5,39,0,0,1217,1197, - 1,0,0,0,1217,1207,1,0,0,0,1218,292,1,0,0,0,1219,1223,5,35,0,0,1220,1222, - 8,32,0,0,1221,1220,1,0,0,0,1222,1225,1,0,0,0,1223,1221,1,0,0,0,1223,1224, - 1,0,0,0,1224,1226,1,0,0,0,1225,1223,1,0,0,0,1226,1227,5,35,0,0,1227,294, - 1,0,0,0,1228,1229,5,123,0,0,1229,1230,3,297,148,0,1230,1231,5,125,0,0, - 1231,1255,1,0,0,0,1232,1233,5,123,0,0,1233,1234,7,21,0,0,1234,1235,7,22, - 0,0,1235,1236,7,18,0,0,1236,1240,7,14,0,0,1237,1239,7,33,0,0,1238,1237, - 1,0,0,0,1239,1242,1,0,0,0,1240,1238,1,0,0,0,1240,1241,1,0,0,0,1241,1243, - 1,0,0,0,1242,1240,1,0,0,0,1243,1244,5,123,0,0,1244,1245,3,297,148,0,1245, - 1249,5,125,0,0,1246,1248,7,33,0,0,1247,1246,1,0,0,0,1248,1251,1,0,0,0, - 1249,1247,1,0,0,0,1249,1250,1,0,0,0,1250,1252,1,0,0,0,1251,1249,1,0,0, - 0,1252,1253,5,125,0,0,1253,1255,1,0,0,0,1254,1228,1,0,0,0,1254,1232,1, - 0,0,0,1255,296,1,0,0,0,1256,1258,3,299,149,0,1257,1256,1,0,0,0,1258,1259, - 1,0,0,0,1259,1257,1,0,0,0,1259,1260,1,0,0,0,1260,1261,1,0,0,0,1261,1263, - 5,45,0,0,1262,1264,3,299,149,0,1263,1262,1,0,0,0,1264,1265,1,0,0,0,1265, - 1263,1,0,0,0,1265,1266,1,0,0,0,1266,1267,1,0,0,0,1267,1269,5,45,0,0,1268, - 1270,3,299,149,0,1269,1268,1,0,0,0,1270,1271,1,0,0,0,1271,1269,1,0,0,0, - 1271,1272,1,0,0,0,1272,1273,1,0,0,0,1273,1275,5,45,0,0,1274,1276,3,299, - 149,0,1275,1274,1,0,0,0,1276,1277,1,0,0,0,1277,1275,1,0,0,0,1277,1278, - 1,0,0,0,1278,1279,1,0,0,0,1279,1281,5,45,0,0,1280,1282,3,299,149,0,1281, - 1280,1,0,0,0,1282,1283,1,0,0,0,1283,1281,1,0,0,0,1283,1284,1,0,0,0,1284, - 298,1,0,0,0,1285,1286,7,27,0,0,1286,300,1,0,0,0,1287,1289,5,91,0,0,1288, - 1290,8,34,0,0,1289,1288,1,0,0,0,1290,1291,1,0,0,0,1291,1289,1,0,0,0,1291, - 1292,1,0,0,0,1292,1293,1,0,0,0,1293,1294,5,93,0,0,1294,302,1,0,0,0,1295, - 1297,5,96,0,0,1296,1298,8,35,0,0,1297,1296,1,0,0,0,1298,1299,1,0,0,0,1299, - 1297,1,0,0,0,1299,1300,1,0,0,0,1300,1301,1,0,0,0,1301,1302,5,96,0,0,1302, - 304,1,0,0,0,1303,1307,7,25,0,0,1304,1306,7,26,0,0,1305,1304,1,0,0,0,1306, - 1309,1,0,0,0,1307,1305,1,0,0,0,1307,1308,1,0,0,0,1308,1311,1,0,0,0,1309, - 1307,1,0,0,0,1310,1312,5,36,0,0,1311,1310,1,0,0,0,1311,1312,1,0,0,0,1312, - 306,1,0,0,0,1313,1315,7,36,0,0,1314,1313,1,0,0,0,1315,1316,1,0,0,0,1316, - 1314,1,0,0,0,1316,1317,1,0,0,0,1317,1318,1,0,0,0,1318,1319,6,153,0,0,1319, - 308,1,0,0,0,1320,1321,5,45,0,0,1321,1322,5,45,0,0,1322,1323,1,0,0,0,1323, - 1327,4,154,0,0,1324,1326,8,37,0,0,1325,1324,1,0,0,0,1326,1329,1,0,0,0, - 1327,1325,1,0,0,0,1327,1328,1,0,0,0,1328,1330,1,0,0,0,1329,1327,1,0,0, - 0,1330,1331,6,154,0,0,1331,310,1,0,0,0,1332,1333,5,47,0,0,1333,1334,5, - 42,0,0,1334,1338,1,0,0,0,1335,1337,9,0,0,0,1336,1335,1,0,0,0,1337,1340, - 1,0,0,0,1338,1339,1,0,0,0,1338,1336,1,0,0,0,1339,1341,1,0,0,0,1340,1338, - 1,0,0,0,1341,1342,5,42,0,0,1342,1343,5,47,0,0,1343,1344,1,0,0,0,1344,1345, - 6,155,0,0,1345,312,1,0,0,0,37,0,1101,1130,1139,1142,1149,1154,1159,1165, - 1169,1175,1178,1183,1186,1190,1195,1201,1203,1211,1213,1217,1223,1240, - 1249,1254,1259,1265,1271,1277,1283,1291,1299,1307,1311,1316,1327,1338, - 1,6,0,0 + 7,152,2,153,7,153,2,154,7,154,2,155,7,155,2,156,7,156,2,157,7,157,2,158, + 7,158,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,9,1,9,1,9,1,9,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,13,1,13,1,13,1,13,1,13,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,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,19,1,19, + 1,19,1,20,1,20,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,23,1,23,1,23,1,24,1,24,1,24,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,27,1,27,1,27,1,28,1,28,1,28,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,31, + 1,31,1,31,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,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34,1,34,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,36,1,36,1,36,1,36,1,36,1,36, + 1,37,1,37,1,37,1,37,1,37,1,37,1,38,1,38,1,38,1,38,1,38,1,39,1,39,1,39, + 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40,1,41,1,41, + 1,41,1,41,1,41,1,42,1,42,1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,44,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,46,1,46,1,46, + 1,46,1,46,1,47,1,47,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,50,1,50,1,50,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1,51, + 1,51,1,51,1,52,1,52,1,52,1,52,1,52,1,52,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,54,1,54,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,57,1,57,1,57,1,57,1,57,1,57, + 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,60,1,61,1,61,1,61,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,63,1,63,1,63, + 1,63,1,63,1,63,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,67,1,67,1,67,1,67,1,67,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,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,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72, + 1,73,1,73,1,73,1,73,1,74,1,74,1,74,1,74,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,75,1,75,1,76,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,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,80,1,80,1,80,1,80,1,81,1,81,1,81,1,81,1,81, + 1,81,1,81,1,82,1,82,1,82,1,82,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83, + 1,84,1,84,1,84,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,86,1,86,1,86,1,86, + 1,86,1,86,1,86,1,86,1,86,1,86,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87, + 1,87,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88, + 1,89,1,89,1,89,1,89,1,89,1,89,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90, + 1,90,1,90,1,91,1,91,1,91,1,91,1,91,1,92,1,92,1,92,1,92,1,92,1,92,1,92, + 1,92,1,92,1,92,1,92,1,92,1,93,1,93,1,93,1,93,1,93,1,94,1,94,1,94,1,94, + 1,94,1,94,1,94,1,94,1,94,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,96,1,96, + 1,96,1,96,1,96,1,96,1,97,1,97,1,97,1,97,1,97,1,98,1,98,1,98,1,98,1,98, + 1,98,1,98,1,98,1,98,1,98,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99, + 1,99,1,99,1,100,1,100,1,100,1,100,1,100,1,100,1,100,1,100,1,101,1,101, + 1,101,1,101,1,101,1,102,1,102,1,102,1,102,1,103,1,103,1,103,1,103,1,103, + 1,104,1,104,1,104,1,104,1,104,1,105,1,105,1,105,1,105,1,105,1,105,1,106, + 1,106,1,106,1,106,1,107,1,107,1,107,1,107,1,108,1,108,1,108,1,108,1,108, + 1,109,1,109,1,109,1,109,1,109,1,109,1,110,1,110,1,110,1,110,1,110,1,110, + 1,110,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,112, + 1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,113,1,113,1,113, + 1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,114,1,114,1,114,1,114,1,114, + 1,114,1,114,1,114,1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,116, + 1,116,1,116,1,116,1,116,1,117,1,117,1,117,1,117,1,117,1,117,1,117,1,118, + 1,118,1,118,1,118,1,118,1,118,1,118,1,119,1,119,1,119,1,119,1,119,1,120, + 1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,121,1,121,1,121,1,121,1,121, + 1,121,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,123,1,123,1,124,1,124, + 1,125,1,125,1,126,1,126,1,127,1,127,1,128,1,128,1,129,1,129,1,130,1,130, + 1,131,1,131,1,131,1,131,3,131,1116,8,131,1,132,1,132,1,132,1,133,1,133, + 1,133,1,134,1,134,1,135,1,135,1,136,1,136,1,137,1,137,1,138,1,138,1,139, + 1,139,1,140,1,140,1,141,1,141,1,142,1,142,1,142,1,142,1,142,5,142,1145, + 8,142,10,142,12,142,1148,9,142,1,143,1,143,1,143,1,143,5,143,1154,8,143, + 10,143,12,143,1157,9,143,3,143,1159,8,143,1,144,1,144,1,144,4,144,1164, + 8,144,11,144,12,144,1165,1,145,4,145,1169,8,145,11,145,12,145,1170,1,146, + 4,146,1174,8,146,11,146,12,146,1175,1,146,1,146,5,146,1180,8,146,10,146, + 12,146,1183,9,146,1,146,3,146,1186,8,146,1,146,1,146,4,146,1190,8,146, + 11,146,12,146,1191,1,146,3,146,1195,8,146,1,146,4,146,1198,8,146,11,146, + 12,146,1199,1,146,3,146,1203,8,146,1,147,1,147,3,147,1207,8,147,1,147, + 4,147,1210,8,147,11,147,12,147,1211,1,148,1,148,1,148,1,148,5,148,1218, + 8,148,10,148,12,148,1221,9,148,1,148,1,148,1,148,1,148,1,148,5,148,1228, + 8,148,10,148,12,148,1231,9,148,1,148,3,148,1234,8,148,1,149,1,149,5,149, + 1238,8,149,10,149,12,149,1241,9,149,1,149,1,149,1,150,1,150,1,150,1,150, + 1,150,1,150,1,150,1,150,1,150,1,150,5,150,1255,8,150,10,150,12,150,1258, + 9,150,1,150,1,150,1,150,1,150,5,150,1264,8,150,10,150,12,150,1267,9,150, + 1,150,1,150,3,150,1271,8,150,1,151,4,151,1274,8,151,11,151,12,151,1275, + 1,151,1,151,4,151,1280,8,151,11,151,12,151,1281,1,151,1,151,4,151,1286, + 8,151,11,151,12,151,1287,1,151,1,151,4,151,1292,8,151,11,151,12,151,1293, + 1,151,1,151,4,151,1298,8,151,11,151,12,151,1299,1,152,1,152,1,153,1,153, + 4,153,1306,8,153,11,153,12,153,1307,1,153,1,153,1,154,1,154,4,154,1314, + 8,154,11,154,12,154,1315,1,154,1,154,1,155,1,155,5,155,1322,8,155,10,155, + 12,155,1325,9,155,1,155,3,155,1328,8,155,1,156,4,156,1331,8,156,11,156, + 12,156,1332,1,156,1,156,1,157,1,157,1,157,1,157,1,157,5,157,1342,8,157, + 10,157,12,157,1345,9,157,1,157,1,157,1,158,1,158,1,158,1,158,5,158,1353, + 8,158,10,158,12,158,1356,9,158,1,158,1,158,1,158,1,158,1,158,1,1354,0, + 159,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,110,221,111,223,112,225,113,227,114,229,115,231,116,233,117,235, + 118,237,119,239,120,241,121,243,122,245,123,247,124,249,125,251,126,253, + 127,255,128,257,129,259,130,261,131,263,132,265,133,267,134,269,135,271, + 136,273,137,275,138,277,139,279,140,281,141,283,142,285,143,287,144,289, + 145,291,146,293,147,295,0,297,148,299,149,301,150,303,0,305,0,307,151, + 309,152,311,153,313,154,315,155,317,156,1,0,40,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,88,88,120,120,2,0,81,81,113,113,2,0,86, + 86,118,118,2,0,73,73,105,105,2,0,66,66,98,98,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,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,2,0,9,9,32,32,1, + 0,93,93,1,0,96,96,660,0,65,90,95,95,97,122,170,170,181,181,186,186,192, + 214,216,246,248,705,710,721,736,740,748,748,750,750,880,884,886,887,890, + 893,895,895,902,902,904,906,908,908,910,929,931,1013,1015,1153,1162,1327, + 1329,1366,1369,1369,1376,1416,1488,1514,1519,1522,1568,1610,1646,1647, + 1649,1747,1749,1749,1765,1766,1774,1775,1786,1788,1791,1791,1808,1808, + 1810,1839,1869,1957,1969,1969,1994,2026,2036,2037,2042,2042,2048,2069, + 2074,2074,2084,2084,2088,2088,2112,2136,2144,2154,2160,2183,2185,2190, + 2208,2249,2308,2361,2365,2365,2384,2384,2392,2401,2417,2432,2437,2444, + 2447,2448,2451,2472,2474,2480,2482,2482,2486,2489,2493,2493,2510,2510, + 2524,2525,2527,2529,2544,2545,2556,2556,2565,2570,2575,2576,2579,2600, + 2602,2608,2610,2611,2613,2614,2616,2617,2649,2652,2654,2654,2674,2676, + 2693,2701,2703,2705,2707,2728,2730,2736,2738,2739,2741,2745,2749,2749, + 2768,2768,2784,2785,2809,2809,2821,2828,2831,2832,2835,2856,2858,2864, + 2866,2867,2869,2873,2877,2877,2908,2909,2911,2913,2929,2929,2947,2947, + 2949,2954,2958,2960,2962,2965,2969,2970,2972,2972,2974,2975,2979,2980, + 2984,2986,2990,3001,3024,3024,3077,3084,3086,3088,3090,3112,3114,3129, + 3133,3133,3160,3162,3165,3165,3168,3169,3200,3200,3205,3212,3214,3216, + 3218,3240,3242,3251,3253,3257,3261,3261,3293,3294,3296,3297,3313,3314, + 3332,3340,3342,3344,3346,3386,3389,3389,3406,3406,3412,3414,3423,3425, + 3450,3455,3461,3478,3482,3505,3507,3515,3517,3517,3520,3526,3585,3632, + 3634,3635,3648,3654,3713,3714,3716,3716,3718,3722,3724,3747,3749,3749, + 3751,3760,3762,3763,3773,3773,3776,3780,3782,3782,3804,3807,3840,3840, + 3904,3911,3913,3948,3976,3980,4096,4138,4159,4159,4176,4181,4186,4189, + 4193,4193,4197,4198,4206,4208,4213,4225,4238,4238,4256,4293,4295,4295, + 4301,4301,4304,4346,4348,4680,4682,4685,4688,4694,4696,4696,4698,4701, + 4704,4744,4746,4749,4752,4784,4786,4789,4792,4798,4800,4800,4802,4805, + 4808,4822,4824,4880,4882,4885,4888,4954,4992,5007,5024,5109,5112,5117, + 5121,5740,5743,5759,5761,5786,5792,5866,5873,5880,5888,5905,5919,5937, + 5952,5969,5984,5996,5998,6000,6016,6067,6103,6103,6108,6108,6176,6264, + 6272,6276,6279,6312,6314,6314,6320,6389,6400,6430,6480,6509,6512,6516, + 6528,6571,6576,6601,6656,6678,6688,6740,6823,6823,6917,6963,6981,6988, + 7043,7072,7086,7087,7098,7141,7168,7203,7245,7247,7258,7293,7296,7304, + 7312,7354,7357,7359,7401,7404,7406,7411,7413,7414,7418,7418,7424,7615, + 7680,7957,7960,7965,7968,8005,8008,8013,8016,8023,8025,8025,8027,8027, + 8029,8029,8031,8061,8064,8116,8118,8124,8126,8126,8130,8132,8134,8140, + 8144,8147,8150,8155,8160,8172,8178,8180,8182,8188,8305,8305,8319,8319, + 8336,8348,8450,8450,8455,8455,8458,8467,8469,8469,8473,8477,8484,8484, + 8486,8486,8488,8488,8490,8493,8495,8505,8508,8511,8517,8521,8526,8526, + 8579,8580,11264,11492,11499,11502,11506,11507,11520,11557,11559,11559, + 11565,11565,11568,11623,11631,11631,11648,11670,11680,11686,11688,11694, + 11696,11702,11704,11710,11712,11718,11720,11726,11728,11734,11736,11742, + 11823,11823,12293,12294,12337,12341,12347,12348,12353,12438,12445,12447, + 12449,12538,12540,12543,12549,12591,12593,12686,12704,12735,12784,12799, + 13312,19903,19968,42124,42192,42237,42240,42508,42512,42527,42538,42539, + 42560,42606,42623,42653,42656,42725,42775,42783,42786,42888,42891,42954, + 42960,42961,42963,42963,42965,42969,42994,43009,43011,43013,43015,43018, + 43020,43042,43072,43123,43138,43187,43250,43255,43259,43259,43261,43262, + 43274,43301,43312,43334,43360,43388,43396,43442,43471,43471,43488,43492, + 43494,43503,43514,43518,43520,43560,43584,43586,43588,43595,43616,43638, + 43642,43642,43646,43695,43697,43697,43701,43702,43705,43709,43712,43712, + 43714,43714,43739,43741,43744,43754,43762,43764,43777,43782,43785,43790, + 43793,43798,43808,43814,43816,43822,43824,43866,43868,43881,43888,44002, + 44032,55203,55216,55238,55243,55291,63744,64109,64112,64217,64256,64262, + 64275,64279,64285,64285,64287,64296,64298,64310,64312,64316,64318,64318, + 64320,64321,64323,64324,64326,64433,64467,64829,64848,64911,64914,64967, + 65008,65019,65136,65140,65142,65276,65313,65338,65345,65370,65382,65470, + 65474,65479,65482,65487,65490,65495,65498,65500,65536,65547,65549,65574, + 65576,65594,65596,65597,65599,65613,65616,65629,65664,65786,66176,66204, + 66208,66256,66304,66335,66349,66368,66370,66377,66384,66421,66432,66461, + 66464,66499,66504,66511,66560,66717,66736,66771,66776,66811,66816,66855, + 66864,66915,66928,66938,66940,66954,66956,66962,66964,66965,66967,66977, + 66979,66993,66995,67001,67003,67004,67072,67382,67392,67413,67424,67431, + 67456,67461,67463,67504,67506,67514,67584,67589,67592,67592,67594,67637, + 67639,67640,67644,67644,67647,67669,67680,67702,67712,67742,67808,67826, + 67828,67829,67840,67861,67872,67897,67968,68023,68030,68031,68096,68096, + 68112,68115,68117,68119,68121,68149,68192,68220,68224,68252,68288,68295, + 68297,68324,68352,68405,68416,68437,68448,68466,68480,68497,68608,68680, + 68736,68786,68800,68850,68864,68899,69248,69289,69296,69297,69376,69404, + 69415,69415,69424,69445,69488,69505,69552,69572,69600,69622,69635,69687, + 69745,69746,69749,69749,69763,69807,69840,69864,69891,69926,69956,69956, + 69959,69959,69968,70002,70006,70006,70019,70066,70081,70084,70106,70106, + 70108,70108,70144,70161,70163,70187,70207,70208,70272,70278,70280,70280, + 70282,70285,70287,70301,70303,70312,70320,70366,70405,70412,70415,70416, + 70419,70440,70442,70448,70450,70451,70453,70457,70461,70461,70480,70480, + 70493,70497,70656,70708,70727,70730,70751,70753,70784,70831,70852,70853, + 70855,70855,71040,71086,71128,71131,71168,71215,71236,71236,71296,71338, + 71352,71352,71424,71450,71488,71494,71680,71723,71840,71903,71935,71942, + 71945,71945,71948,71955,71957,71958,71960,71983,71999,71999,72001,72001, + 72096,72103,72106,72144,72161,72161,72163,72163,72192,72192,72203,72242, + 72250,72250,72272,72272,72284,72329,72349,72349,72368,72440,72704,72712, + 72714,72750,72768,72768,72818,72847,72960,72966,72968,72969,72971,73008, + 73030,73030,73056,73061,73063,73064,73066,73097,73112,73112,73440,73458, + 73474,73474,73476,73488,73490,73523,73648,73648,73728,74649,74880,75075, + 77712,77808,77824,78895,78913,78918,82944,83526,92160,92728,92736,92766, + 92784,92862,92880,92909,92928,92975,92992,92995,93027,93047,93053,93071, + 93760,93823,93952,94026,94032,94032,94099,94111,94176,94177,94179,94179, + 94208,100343,100352,101589,101632,101640,110576,110579,110581,110587,110589, + 110590,110592,110882,110898,110898,110928,110930,110933,110933,110948, + 110951,110960,111355,113664,113770,113776,113788,113792,113800,113808, + 113817,119808,119892,119894,119964,119966,119967,119970,119970,119973, + 119974,119977,119980,119982,119993,119995,119995,119997,120003,120005, + 120069,120071,120074,120077,120084,120086,120092,120094,120121,120123, + 120126,120128,120132,120134,120134,120138,120144,120146,120485,120488, + 120512,120514,120538,120540,120570,120572,120596,120598,120628,120630, + 120654,120656,120686,120688,120712,120714,120744,120746,120770,120772, + 120779,122624,122654,122661,122666,122928,122989,123136,123180,123191, + 123197,123214,123214,123536,123565,123584,123627,124112,124139,124896, + 124902,124904,124907,124909,124910,124912,124926,124928,125124,125184, + 125251,125259,125259,126464,126467,126469,126495,126497,126498,126500, + 126500,126503,126503,126505,126514,126516,126519,126521,126521,126523, + 126523,126530,126530,126535,126535,126537,126537,126539,126539,126541, + 126543,126545,126546,126548,126548,126551,126551,126553,126553,126555, + 126555,126557,126557,126559,126559,126561,126562,126564,126564,126567, + 126570,126572,126578,126580,126583,126585,126588,126590,126590,126592, + 126601,126603,126619,126625,126627,126629,126633,126635,126651,131072, + 173791,173824,177977,177984,178205,178208,183969,183984,191456,194560, + 195101,196608,201546,201552,205743,760,0,48,57,65,90,95,95,97,122,170, + 170,181,181,186,186,192,214,216,246,248,705,710,721,736,740,748,748,750, + 750,768,884,886,887,890,893,895,895,902,902,904,906,908,908,910,929,931, + 1013,1015,1153,1155,1327,1329,1366,1369,1369,1376,1416,1425,1469,1471, + 1471,1473,1474,1476,1477,1479,1479,1488,1514,1519,1522,1552,1562,1568, + 1641,1646,1747,1749,1756,1759,1768,1770,1788,1791,1791,1808,1866,1869, + 1969,1984,2037,2042,2042,2045,2045,2048,2093,2112,2139,2144,2154,2160, + 2183,2185,2190,2200,2273,2275,2403,2406,2415,2417,2435,2437,2444,2447, + 2448,2451,2472,2474,2480,2482,2482,2486,2489,2492,2500,2503,2504,2507, + 2510,2519,2519,2524,2525,2527,2531,2534,2545,2556,2556,2558,2558,2561, + 2563,2565,2570,2575,2576,2579,2600,2602,2608,2610,2611,2613,2614,2616, + 2617,2620,2620,2622,2626,2631,2632,2635,2637,2641,2641,2649,2652,2654, + 2654,2662,2677,2689,2691,2693,2701,2703,2705,2707,2728,2730,2736,2738, + 2739,2741,2745,2748,2757,2759,2761,2763,2765,2768,2768,2784,2787,2790, + 2799,2809,2815,2817,2819,2821,2828,2831,2832,2835,2856,2858,2864,2866, + 2867,2869,2873,2876,2884,2887,2888,2891,2893,2901,2903,2908,2909,2911, + 2915,2918,2927,2929,2929,2946,2947,2949,2954,2958,2960,2962,2965,2969, + 2970,2972,2972,2974,2975,2979,2980,2984,2986,2990,3001,3006,3010,3014, + 3016,3018,3021,3024,3024,3031,3031,3046,3055,3072,3084,3086,3088,3090, + 3112,3114,3129,3132,3140,3142,3144,3146,3149,3157,3158,3160,3162,3165, + 3165,3168,3171,3174,3183,3200,3203,3205,3212,3214,3216,3218,3240,3242, + 3251,3253,3257,3260,3268,3270,3272,3274,3277,3285,3286,3293,3294,3296, + 3299,3302,3311,3313,3315,3328,3340,3342,3344,3346,3396,3398,3400,3402, + 3406,3412,3415,3423,3427,3430,3439,3450,3455,3457,3459,3461,3478,3482, + 3505,3507,3515,3517,3517,3520,3526,3530,3530,3535,3540,3542,3542,3544, + 3551,3558,3567,3570,3571,3585,3642,3648,3662,3664,3673,3713,3714,3716, + 3716,3718,3722,3724,3747,3749,3749,3751,3773,3776,3780,3782,3782,3784, + 3790,3792,3801,3804,3807,3840,3840,3864,3865,3872,3881,3893,3893,3895, + 3895,3897,3897,3902,3911,3913,3948,3953,3972,3974,3991,3993,4028,4038, + 4038,4096,4169,4176,4253,4256,4293,4295,4295,4301,4301,4304,4346,4348, + 4680,4682,4685,4688,4694,4696,4696,4698,4701,4704,4744,4746,4749,4752, + 4784,4786,4789,4792,4798,4800,4800,4802,4805,4808,4822,4824,4880,4882, + 4885,4888,4954,4957,4959,4992,5007,5024,5109,5112,5117,5121,5740,5743, + 5759,5761,5786,5792,5866,5873,5880,5888,5909,5919,5940,5952,5971,5984, + 5996,5998,6000,6002,6003,6016,6099,6103,6103,6108,6109,6112,6121,6155, + 6157,6159,6169,6176,6264,6272,6314,6320,6389,6400,6430,6432,6443,6448, + 6459,6470,6509,6512,6516,6528,6571,6576,6601,6608,6617,6656,6683,6688, + 6750,6752,6780,6783,6793,6800,6809,6823,6823,6832,6862,6912,6988,6992, + 7001,7019,7027,7040,7155,7168,7223,7232,7241,7245,7293,7296,7304,7312, + 7354,7357,7359,7376,7378,7380,7418,7424,7957,7960,7965,7968,8005,8008, + 8013,8016,8023,8025,8025,8027,8027,8029,8029,8031,8061,8064,8116,8118, + 8124,8126,8126,8130,8132,8134,8140,8144,8147,8150,8155,8160,8172,8178, + 8180,8182,8188,8305,8305,8319,8319,8336,8348,8400,8432,8450,8450,8455, + 8455,8458,8467,8469,8469,8473,8477,8484,8484,8486,8486,8488,8488,8490, + 8493,8495,8505,8508,8511,8517,8521,8526,8526,8579,8580,11264,11492,11499, + 11507,11520,11557,11559,11559,11565,11565,11568,11623,11631,11631,11647, + 11670,11680,11686,11688,11694,11696,11702,11704,11710,11712,11718,11720, + 11726,11728,11734,11736,11742,11744,11775,11823,11823,12293,12294,12330, + 12335,12337,12341,12347,12348,12353,12438,12441,12442,12445,12447,12449, + 12538,12540,12543,12549,12591,12593,12686,12704,12735,12784,12799,13312, + 19903,19968,42124,42192,42237,42240,42508,42512,42539,42560,42610,42612, + 42621,42623,42725,42736,42737,42775,42783,42786,42888,42891,42954,42960, + 42961,42963,42963,42965,42969,42994,43047,43052,43052,43072,43123,43136, + 43205,43216,43225,43232,43255,43259,43259,43261,43309,43312,43347,43360, + 43388,43392,43456,43471,43481,43488,43518,43520,43574,43584,43597,43600, + 43609,43616,43638,43642,43714,43739,43741,43744,43759,43762,43766,43777, + 43782,43785,43790,43793,43798,43808,43814,43816,43822,43824,43866,43868, + 43881,43888,44010,44012,44013,44016,44025,44032,55203,55216,55238,55243, + 55291,63744,64109,64112,64217,64256,64262,64275,64279,64285,64296,64298, + 64310,64312,64316,64318,64318,64320,64321,64323,64324,64326,64433,64467, + 64829,64848,64911,64914,64967,65008,65019,65024,65039,65056,65071,65136, + 65140,65142,65276,65296,65305,65313,65338,65345,65370,65382,65470,65474, + 65479,65482,65487,65490,65495,65498,65500,65536,65547,65549,65574,65576, + 65594,65596,65597,65599,65613,65616,65629,65664,65786,66045,66045,66176, + 66204,66208,66256,66272,66272,66304,66335,66349,66368,66370,66377,66384, + 66426,66432,66461,66464,66499,66504,66511,66560,66717,66720,66729,66736, + 66771,66776,66811,66816,66855,66864,66915,66928,66938,66940,66954,66956, + 66962,66964,66965,66967,66977,66979,66993,66995,67001,67003,67004,67072, + 67382,67392,67413,67424,67431,67456,67461,67463,67504,67506,67514,67584, + 67589,67592,67592,67594,67637,67639,67640,67644,67644,67647,67669,67680, + 67702,67712,67742,67808,67826,67828,67829,67840,67861,67872,67897,67968, + 68023,68030,68031,68096,68099,68101,68102,68108,68115,68117,68119,68121, + 68149,68152,68154,68159,68159,68192,68220,68224,68252,68288,68295,68297, + 68326,68352,68405,68416,68437,68448,68466,68480,68497,68608,68680,68736, + 68786,68800,68850,68864,68903,68912,68921,69248,69289,69291,69292,69296, + 69297,69373,69404,69415,69415,69424,69456,69488,69509,69552,69572,69600, + 69622,69632,69702,69734,69749,69759,69818,69826,69826,69840,69864,69872, + 69881,69888,69940,69942,69951,69956,69959,69968,70003,70006,70006,70016, + 70084,70089,70092,70094,70106,70108,70108,70144,70161,70163,70199,70206, + 70209,70272,70278,70280,70280,70282,70285,70287,70301,70303,70312,70320, + 70378,70384,70393,70400,70403,70405,70412,70415,70416,70419,70440,70442, + 70448,70450,70451,70453,70457,70459,70468,70471,70472,70475,70477,70480, + 70480,70487,70487,70493,70499,70502,70508,70512,70516,70656,70730,70736, + 70745,70750,70753,70784,70853,70855,70855,70864,70873,71040,71093,71096, + 71104,71128,71133,71168,71232,71236,71236,71248,71257,71296,71352,71360, + 71369,71424,71450,71453,71467,71472,71481,71488,71494,71680,71738,71840, + 71913,71935,71942,71945,71945,71948,71955,71957,71958,71960,71989,71991, + 71992,71995,72003,72016,72025,72096,72103,72106,72151,72154,72161,72163, + 72164,72192,72254,72263,72263,72272,72345,72349,72349,72368,72440,72704, + 72712,72714,72758,72760,72768,72784,72793,72818,72847,72850,72871,72873, + 72886,72960,72966,72968,72969,72971,73014,73018,73018,73020,73021,73023, + 73031,73040,73049,73056,73061,73063,73064,73066,73102,73104,73105,73107, + 73112,73120,73129,73440,73462,73472,73488,73490,73530,73534,73538,73552, + 73561,73648,73648,73728,74649,74880,75075,77712,77808,77824,78895,78912, + 78933,82944,83526,92160,92728,92736,92766,92768,92777,92784,92862,92864, + 92873,92880,92909,92912,92916,92928,92982,92992,92995,93008,93017,93027, + 93047,93053,93071,93760,93823,93952,94026,94031,94087,94095,94111,94176, + 94177,94179,94180,94192,94193,94208,100343,100352,101589,101632,101640, + 110576,110579,110581,110587,110589,110590,110592,110882,110898,110898, + 110928,110930,110933,110933,110948,110951,110960,111355,113664,113770, + 113776,113788,113792,113800,113808,113817,113821,113822,118528,118573, + 118576,118598,119141,119145,119149,119154,119163,119170,119173,119179, + 119210,119213,119362,119364,119808,119892,119894,119964,119966,119967, + 119970,119970,119973,119974,119977,119980,119982,119993,119995,119995, + 119997,120003,120005,120069,120071,120074,120077,120084,120086,120092, + 120094,120121,120123,120126,120128,120132,120134,120134,120138,120144, + 120146,120485,120488,120512,120514,120538,120540,120570,120572,120596, + 120598,120628,120630,120654,120656,120686,120688,120712,120714,120744, + 120746,120770,120772,120779,120782,120831,121344,121398,121403,121452, + 121461,121461,121476,121476,121499,121503,121505,121519,122624,122654, + 122661,122666,122880,122886,122888,122904,122907,122913,122915,122916, + 122918,122922,122928,122989,123023,123023,123136,123180,123184,123197, + 123200,123209,123214,123214,123536,123566,123584,123641,124112,124153, + 124896,124902,124904,124907,124909,124910,124912,124926,124928,125124, + 125136,125142,125184,125259,125264,125273,126464,126467,126469,126495, + 126497,126498,126500,126500,126503,126503,126505,126514,126516,126519, + 126521,126521,126523,126523,126530,126530,126535,126535,126537,126537, + 126539,126539,126541,126543,126545,126546,126548,126548,126551,126551, + 126553,126553,126555,126555,126557,126557,126559,126559,126561,126562, + 126564,126564,126567,126570,126572,126578,126580,126583,126585,126588, + 126590,126590,126592,126601,126603,126619,126625,126627,126629,126633, + 126635,126651,130032,130041,131072,173791,173824,177977,177984,178205, + 178208,183969,183984,191456,194560,195101,196608,201546,201552,205743, + 917760,917999,3,0,9,10,13,13,32,32,2,0,10,10,13,13,1395,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,219,1,0,0,0,0,221,1,0, + 0,0,0,223,1,0,0,0,0,225,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,0,239,1,0,0,0,0,241,1,0, + 0,0,0,243,1,0,0,0,0,245,1,0,0,0,0,247,1,0,0,0,0,249,1,0,0,0,0,251,1,0, + 0,0,0,253,1,0,0,0,0,255,1,0,0,0,0,257,1,0,0,0,0,259,1,0,0,0,0,261,1,0, + 0,0,0,263,1,0,0,0,0,265,1,0,0,0,0,267,1,0,0,0,0,269,1,0,0,0,0,271,1,0, + 0,0,0,273,1,0,0,0,0,275,1,0,0,0,0,277,1,0,0,0,0,279,1,0,0,0,0,281,1,0, + 0,0,0,283,1,0,0,0,0,285,1,0,0,0,0,287,1,0,0,0,0,289,1,0,0,0,0,291,1,0, + 0,0,0,293,1,0,0,0,0,297,1,0,0,0,0,299,1,0,0,0,0,301,1,0,0,0,0,307,1,0, + 0,0,0,309,1,0,0,0,0,311,1,0,0,0,0,313,1,0,0,0,0,315,1,0,0,0,0,317,1,0, + 0,0,1,319,1,0,0,0,3,326,1,0,0,0,5,331,1,0,0,0,7,337,1,0,0,0,9,341,1,0, + 0,0,11,344,1,0,0,0,13,348,1,0,0,0,15,351,1,0,0,0,17,355,1,0,0,0,19,359, + 1,0,0,0,21,363,1,0,0,0,23,367,1,0,0,0,25,372,1,0,0,0,27,376,1,0,0,0,29, + 381,1,0,0,0,31,386,1,0,0,0,33,391,1,0,0,0,35,395,1,0,0,0,37,401,1,0,0, + 0,39,406,1,0,0,0,41,412,1,0,0,0,43,417,1,0,0,0,45,423,1,0,0,0,47,428,1, + 0,0,0,49,431,1,0,0,0,51,434,1,0,0,0,53,440,1,0,0,0,55,446,1,0,0,0,57,449, + 1,0,0,0,59,452,1,0,0,0,61,459,1,0,0,0,63,466,1,0,0,0,65,469,1,0,0,0,67, + 474,1,0,0,0,69,486,1,0,0,0,71,495,1,0,0,0,73,503,1,0,0,0,75,509,1,0,0, + 0,77,515,1,0,0,0,79,520,1,0,0,0,81,530,1,0,0,0,83,535,1,0,0,0,85,540,1, + 0,0,0,87,545,1,0,0,0,89,549,1,0,0,0,91,556,1,0,0,0,93,562,1,0,0,0,95,567, + 1,0,0,0,97,573,1,0,0,0,99,578,1,0,0,0,101,582,1,0,0,0,103,587,1,0,0,0, + 105,595,1,0,0,0,107,601,1,0,0,0,109,605,1,0,0,0,111,615,1,0,0,0,113,622, + 1,0,0,0,115,629,1,0,0,0,117,635,1,0,0,0,119,641,1,0,0,0,121,648,1,0,0, + 0,123,657,1,0,0,0,125,669,1,0,0,0,127,674,1,0,0,0,129,680,1,0,0,0,131, + 687,1,0,0,0,133,690,1,0,0,0,135,694,1,0,0,0,137,699,1,0,0,0,139,706,1, + 0,0,0,141,713,1,0,0,0,143,718,1,0,0,0,145,725,1,0,0,0,147,733,1,0,0,0, + 149,737,1,0,0,0,151,748,1,0,0,0,153,756,1,0,0,0,155,767,1,0,0,0,157,774, + 1,0,0,0,159,781,1,0,0,0,161,789,1,0,0,0,163,798,1,0,0,0,165,805,1,0,0, + 0,167,809,1,0,0,0,169,817,1,0,0,0,171,820,1,0,0,0,173,827,1,0,0,0,175, + 837,1,0,0,0,177,846,1,0,0,0,179,859,1,0,0,0,181,865,1,0,0,0,183,875,1, + 0,0,0,185,880,1,0,0,0,187,892,1,0,0,0,189,897,1,0,0,0,191,906,1,0,0,0, + 193,913,1,0,0,0,195,919,1,0,0,0,197,924,1,0,0,0,199,934,1,0,0,0,201,945, + 1,0,0,0,203,953,1,0,0,0,205,958,1,0,0,0,207,962,1,0,0,0,209,967,1,0,0, + 0,211,972,1,0,0,0,213,978,1,0,0,0,215,982,1,0,0,0,217,986,1,0,0,0,219, + 991,1,0,0,0,221,997,1,0,0,0,223,1004,1,0,0,0,225,1014,1,0,0,0,227,1024, + 1,0,0,0,229,1034,1,0,0,0,231,1042,1,0,0,0,233,1050,1,0,0,0,235,1055,1, + 0,0,0,237,1062,1,0,0,0,239,1069,1,0,0,0,241,1074,1,0,0,0,243,1082,1,0, + 0,0,245,1088,1,0,0,0,247,1095,1,0,0,0,249,1097,1,0,0,0,251,1099,1,0,0, + 0,253,1101,1,0,0,0,255,1103,1,0,0,0,257,1105,1,0,0,0,259,1107,1,0,0,0, + 261,1109,1,0,0,0,263,1115,1,0,0,0,265,1117,1,0,0,0,267,1120,1,0,0,0,269, + 1123,1,0,0,0,271,1125,1,0,0,0,273,1127,1,0,0,0,275,1129,1,0,0,0,277,1131, + 1,0,0,0,279,1133,1,0,0,0,281,1135,1,0,0,0,283,1137,1,0,0,0,285,1139,1, + 0,0,0,287,1158,1,0,0,0,289,1160,1,0,0,0,291,1168,1,0,0,0,293,1202,1,0, + 0,0,295,1204,1,0,0,0,297,1233,1,0,0,0,299,1235,1,0,0,0,301,1270,1,0,0, + 0,303,1273,1,0,0,0,305,1301,1,0,0,0,307,1303,1,0,0,0,309,1311,1,0,0,0, + 311,1319,1,0,0,0,313,1330,1,0,0,0,315,1336,1,0,0,0,317,1348,1,0,0,0,319, + 320,7,0,0,0,320,321,7,1,0,0,321,322,7,2,0,0,322,323,7,1,0,0,323,324,7, + 3,0,0,324,325,7,4,0,0,325,2,1,0,0,0,326,327,7,5,0,0,327,328,7,6,0,0,328, + 329,7,7,0,0,329,330,7,8,0,0,330,4,1,0,0,0,331,332,7,9,0,0,332,333,7,10, + 0,0,333,334,7,1,0,0,334,335,7,6,0,0,335,336,7,1,0,0,336,6,1,0,0,0,337, + 338,7,4,0,0,338,339,7,7,0,0,339,340,7,11,0,0,340,8,1,0,0,0,341,342,7,12, + 0,0,342,343,7,0,0,0,343,10,1,0,0,0,344,345,7,12,0,0,345,346,7,13,0,0,346, + 347,7,14,0,0,347,12,1,0,0,0,348,349,7,7,0,0,349,350,7,6,0,0,350,14,1,0, + 0,0,351,352,7,13,0,0,352,353,7,7,0,0,353,354,7,4,0,0,354,16,1,0,0,0,355, + 356,7,15,0,0,356,357,7,7,0,0,357,358,7,6,0,0,358,18,1,0,0,0,359,360,7, + 1,0,0,360,361,7,16,0,0,361,362,7,17,0,0,362,20,1,0,0,0,363,364,7,18,0, + 0,364,365,7,8,0,0,365,366,7,11,0,0,366,22,1,0,0,0,367,368,7,19,0,0,368, + 369,7,12,0,0,369,370,7,13,0,0,370,371,7,14,0,0,371,24,1,0,0,0,372,373, + 7,19,0,0,373,374,7,7,0,0,374,375,7,6,0,0,375,26,1,0,0,0,376,377,7,19,0, + 0,377,378,7,15,0,0,378,379,7,7,0,0,379,380,7,6,0,0,380,28,1,0,0,0,381, + 382,7,19,0,0,382,383,7,13,0,0,383,384,7,7,0,0,384,385,7,4,0,0,385,30,1, + 0,0,0,386,387,7,2,0,0,387,388,7,18,0,0,388,389,7,20,0,0,389,390,7,1,0, + 0,390,32,1,0,0,0,391,392,7,8,0,0,392,393,7,7,0,0,393,394,7,14,0,0,394, + 34,1,0,0,0,395,396,7,18,0,0,396,397,7,13,0,0,397,398,7,13,0,0,398,399, + 7,1,0,0,399,400,7,6,0,0,400,36,1,0,0,0,401,402,7,2,0,0,402,403,7,1,0,0, + 403,404,7,5,0,0,404,405,7,4,0,0,405,38,1,0,0,0,406,407,7,6,0,0,407,408, + 7,18,0,0,408,409,7,21,0,0,409,410,7,10,0,0,410,411,7,4,0,0,411,40,1,0, + 0,0,412,413,7,5,0,0,413,414,7,22,0,0,414,415,7,2,0,0,415,416,7,2,0,0,416, + 42,1,0,0,0,417,418,7,7,0,0,418,419,7,22,0,0,419,420,7,4,0,0,420,421,7, + 1,0,0,421,422,7,6,0,0,422,44,1,0,0,0,423,424,7,23,0,0,424,425,7,7,0,0, + 425,426,7,18,0,0,426,427,7,13,0,0,427,46,1,0,0,0,428,429,7,18,0,0,429, + 430,7,13,0,0,430,48,1,0,0,0,431,432,7,7,0,0,432,433,7,13,0,0,433,50,1, + 0,0,0,434,435,7,7,0,0,435,436,7,6,0,0,436,437,7,14,0,0,437,438,7,1,0,0, + 438,439,7,6,0,0,439,52,1,0,0,0,440,441,7,21,0,0,441,442,7,6,0,0,442,443, + 7,7,0,0,443,444,7,22,0,0,444,445,7,11,0,0,445,54,1,0,0,0,446,447,7,18, + 0,0,447,448,7,0,0,0,448,56,1,0,0,0,449,450,7,19,0,0,450,451,7,24,0,0,451, + 58,1,0,0,0,452,453,7,10,0,0,453,454,7,12,0,0,454,455,7,17,0,0,455,456, + 7,18,0,0,456,457,7,13,0,0,457,458,7,21,0,0,458,60,1,0,0,0,459,460,7,1, + 0,0,460,461,7,15,0,0,461,462,7,18,0,0,462,463,7,0,0,0,463,464,7,4,0,0, + 464,465,7,0,0,0,465,62,1,0,0,0,466,467,7,18,0,0,467,468,7,5,0,0,468,64, + 1,0,0,0,469,470,7,4,0,0,470,471,7,10,0,0,471,472,7,1,0,0,472,473,7,13, + 0,0,473,66,1,0,0,0,474,475,7,14,0,0,475,476,7,18,0,0,476,477,7,0,0,0,477, + 478,7,4,0,0,478,479,7,18,0,0,479,480,7,13,0,0,480,481,7,3,0,0,481,482, + 7,4,0,0,482,483,7,6,0,0,483,484,7,7,0,0,484,485,7,9,0,0,485,68,1,0,0,0, + 486,487,7,14,0,0,487,488,7,18,0,0,488,489,7,0,0,0,489,490,7,4,0,0,490, + 491,7,18,0,0,491,492,7,13,0,0,492,493,7,3,0,0,493,494,7,4,0,0,494,70,1, + 0,0,0,495,496,7,11,0,0,496,497,7,1,0,0,497,498,7,6,0,0,498,499,7,3,0,0, + 499,500,7,1,0,0,500,501,7,13,0,0,501,502,7,4,0,0,502,72,1,0,0,0,503,504, + 7,3,0,0,504,505,7,6,0,0,505,506,7,7,0,0,506,507,7,0,0,0,507,508,7,0,0, + 0,508,74,1,0,0,0,509,510,7,12,0,0,510,511,7,11,0,0,511,512,7,11,0,0,512, + 513,7,2,0,0,513,514,7,24,0,0,514,76,1,0,0,0,515,516,7,7,0,0,516,517,7, + 17,0,0,517,518,7,1,0,0,518,519,7,6,0,0,519,78,1,0,0,0,520,521,7,11,0,0, + 521,522,7,12,0,0,522,523,7,6,0,0,523,524,7,4,0,0,524,525,7,18,0,0,525, + 526,7,4,0,0,526,527,7,18,0,0,527,528,7,7,0,0,528,529,7,13,0,0,529,80,1, + 0,0,0,530,531,7,3,0,0,531,532,7,12,0,0,532,533,7,0,0,0,533,534,7,1,0,0, + 534,82,1,0,0,0,535,536,7,9,0,0,536,537,7,10,0,0,537,538,7,1,0,0,538,539, + 7,13,0,0,539,84,1,0,0,0,540,541,7,1,0,0,541,542,7,2,0,0,542,543,7,0,0, + 0,543,544,7,1,0,0,544,86,1,0,0,0,545,546,7,1,0,0,546,547,7,13,0,0,547, + 548,7,14,0,0,548,88,1,0,0,0,549,550,7,7,0,0,550,551,7,5,0,0,551,552,7, + 5,0,0,552,553,7,0,0,0,553,554,7,1,0,0,554,555,7,4,0,0,555,90,1,0,0,0,556, + 557,7,5,0,0,557,558,7,1,0,0,558,559,7,4,0,0,559,560,7,3,0,0,560,561,7, + 10,0,0,561,92,1,0,0,0,562,563,7,13,0,0,563,564,7,1,0,0,564,565,7,15,0, + 0,565,566,7,4,0,0,566,94,1,0,0,0,567,568,7,5,0,0,568,569,7,18,0,0,569, + 570,7,6,0,0,570,571,7,0,0,0,571,572,7,4,0,0,572,96,1,0,0,0,573,574,7,6, + 0,0,574,575,7,7,0,0,575,576,7,9,0,0,576,577,7,0,0,0,577,98,1,0,0,0,578, + 579,7,6,0,0,579,580,7,7,0,0,580,581,7,9,0,0,581,100,1,0,0,0,582,583,7, + 7,0,0,583,584,7,13,0,0,584,585,7,2,0,0,585,586,7,24,0,0,586,102,1,0,0, + 0,587,588,7,19,0,0,588,589,7,1,0,0,589,590,7,4,0,0,590,591,7,9,0,0,591, + 592,7,1,0,0,592,593,7,1,0,0,593,594,7,13,0,0,594,104,1,0,0,0,595,596,7, + 22,0,0,596,597,7,13,0,0,597,598,7,18,0,0,598,599,7,7,0,0,599,600,7,13, + 0,0,600,106,1,0,0,0,601,602,7,12,0,0,602,603,7,2,0,0,603,604,7,2,0,0,604, + 108,1,0,0,0,605,606,7,18,0,0,606,607,7,13,0,0,607,608,7,4,0,0,608,609, + 7,1,0,0,609,610,7,6,0,0,610,611,7,0,0,0,611,612,7,1,0,0,612,613,7,3,0, + 0,613,614,7,4,0,0,614,110,1,0,0,0,615,616,7,1,0,0,616,617,7,15,0,0,617, + 618,7,3,0,0,618,619,7,1,0,0,619,620,7,11,0,0,620,621,7,4,0,0,621,112,1, + 0,0,0,622,623,7,3,0,0,623,624,7,6,0,0,624,625,7,1,0,0,625,626,7,12,0,0, + 626,627,7,4,0,0,627,628,7,1,0,0,628,114,1,0,0,0,629,630,7,4,0,0,630,631, + 7,12,0,0,631,632,7,19,0,0,632,633,7,2,0,0,633,634,7,1,0,0,634,116,1,0, + 0,0,635,636,7,19,0,0,636,637,7,1,0,0,637,638,7,21,0,0,638,639,7,18,0,0, + 639,640,7,13,0,0,640,118,1,0,0,0,641,642,7,3,0,0,642,643,7,7,0,0,643,644, + 7,8,0,0,644,645,7,8,0,0,645,646,7,18,0,0,646,647,7,4,0,0,647,120,1,0,0, + 0,648,649,7,6,0,0,649,650,7,7,0,0,650,651,7,2,0,0,651,652,7,2,0,0,652, + 653,7,19,0,0,653,654,7,12,0,0,654,655,7,3,0,0,655,656,7,20,0,0,656,122, + 1,0,0,0,657,658,7,4,0,0,658,659,7,6,0,0,659,660,7,12,0,0,660,661,7,13, + 0,0,661,662,7,0,0,0,662,663,7,12,0,0,663,664,7,3,0,0,664,665,7,4,0,0,665, + 666,7,18,0,0,666,667,7,7,0,0,667,668,7,13,0,0,668,124,1,0,0,0,669,670, + 7,9,0,0,670,671,7,7,0,0,671,672,7,6,0,0,672,673,7,20,0,0,673,126,1,0,0, + 0,674,675,7,12,0,0,675,676,7,2,0,0,676,677,7,4,0,0,677,678,7,1,0,0,678, + 679,7,6,0,0,679,128,1,0,0,0,680,681,7,6,0,0,681,682,7,1,0,0,682,683,7, + 13,0,0,683,684,7,12,0,0,684,685,7,8,0,0,685,686,7,1,0,0,686,130,1,0,0, + 0,687,688,7,4,0,0,688,689,7,7,0,0,689,132,1,0,0,0,690,691,7,12,0,0,691, + 692,7,14,0,0,692,693,7,14,0,0,693,134,1,0,0,0,694,695,7,14,0,0,695,696, + 7,6,0,0,696,697,7,7,0,0,697,698,7,11,0,0,698,136,1,0,0,0,699,700,7,3,0, + 0,700,701,7,7,0,0,701,702,7,2,0,0,702,703,7,22,0,0,703,704,7,8,0,0,704, + 705,7,13,0,0,705,138,1,0,0,0,706,707,7,18,0,0,707,708,7,13,0,0,708,709, + 7,0,0,0,709,710,7,1,0,0,710,711,7,6,0,0,711,712,7,4,0,0,712,140,1,0,0, + 0,713,714,7,18,0,0,714,715,7,13,0,0,715,716,7,4,0,0,716,717,7,7,0,0,717, + 142,1,0,0,0,718,719,7,17,0,0,719,720,7,12,0,0,720,721,7,2,0,0,721,722, + 7,22,0,0,722,723,7,1,0,0,723,724,7,0,0,0,724,144,1,0,0,0,725,726,7,11, + 0,0,726,727,7,6,0,0,727,728,7,18,0,0,728,729,7,8,0,0,729,730,7,12,0,0, + 730,731,7,6,0,0,731,732,7,24,0,0,732,146,1,0,0,0,733,734,7,20,0,0,734, + 735,7,1,0,0,735,736,7,24,0,0,736,148,1,0,0,0,737,738,7,3,0,0,738,739,7, + 7,0,0,739,740,7,13,0,0,740,741,7,0,0,0,741,742,7,4,0,0,742,743,7,6,0,0, + 743,744,7,12,0,0,744,745,7,18,0,0,745,746,7,13,0,0,746,747,7,4,0,0,747, + 150,1,0,0,0,748,749,7,5,0,0,749,750,7,7,0,0,750,751,7,6,0,0,751,752,7, + 1,0,0,752,753,7,18,0,0,753,754,7,21,0,0,754,755,7,13,0,0,755,152,1,0,0, + 0,756,757,7,6,0,0,757,758,7,1,0,0,758,759,7,5,0,0,759,760,7,1,0,0,760, + 761,7,6,0,0,761,762,7,1,0,0,762,763,7,13,0,0,763,764,7,3,0,0,764,765,7, + 1,0,0,765,766,7,0,0,0,766,154,1,0,0,0,767,768,7,14,0,0,768,769,7,1,0,0, + 769,770,7,2,0,0,770,771,7,1,0,0,771,772,7,4,0,0,772,773,7,1,0,0,773,156, + 1,0,0,0,774,775,7,22,0,0,775,776,7,11,0,0,776,777,7,14,0,0,777,778,7,12, + 0,0,778,779,7,4,0,0,779,780,7,1,0,0,780,158,1,0,0,0,781,782,7,3,0,0,782, + 783,7,12,0,0,783,784,7,0,0,0,784,785,7,3,0,0,785,786,7,12,0,0,786,787, + 7,14,0,0,787,788,7,1,0,0,788,160,1,0,0,0,789,790,7,6,0,0,790,791,7,1,0, + 0,791,792,7,0,0,0,792,793,7,4,0,0,793,794,7,6,0,0,794,795,7,18,0,0,795, + 796,7,3,0,0,796,797,7,4,0,0,797,162,1,0,0,0,798,799,7,12,0,0,799,800,7, + 3,0,0,800,801,7,4,0,0,801,802,7,18,0,0,802,803,7,7,0,0,803,804,7,13,0, + 0,804,164,1,0,0,0,805,806,7,0,0,0,806,807,7,1,0,0,807,808,7,4,0,0,808, + 166,1,0,0,0,809,810,7,14,0,0,810,811,7,1,0,0,811,812,7,5,0,0,812,813,7, + 12,0,0,813,814,7,22,0,0,814,815,7,2,0,0,815,816,7,4,0,0,816,168,1,0,0, + 0,817,818,7,13,0,0,818,819,7,7,0,0,819,170,1,0,0,0,820,821,7,22,0,0,821, + 822,7,13,0,0,822,823,7,18,0,0,823,824,7,16,0,0,824,825,7,22,0,0,825,826, + 7,1,0,0,826,172,1,0,0,0,827,828,7,3,0,0,828,829,7,2,0,0,829,830,7,22,0, + 0,830,831,7,0,0,0,831,832,7,4,0,0,832,833,7,1,0,0,833,834,7,6,0,0,834, + 835,7,1,0,0,835,836,7,14,0,0,836,174,1,0,0,0,837,838,7,18,0,0,838,839, + 7,14,0,0,839,840,7,1,0,0,840,841,7,13,0,0,841,842,7,4,0,0,842,843,7,18, + 0,0,843,844,7,4,0,0,844,845,7,24,0,0,845,176,1,0,0,0,846,847,7,13,0,0, + 847,848,7,7,0,0,848,849,7,13,0,0,849,850,7,3,0,0,850,851,7,2,0,0,851,852, + 7,22,0,0,852,853,7,0,0,0,853,854,7,4,0,0,854,855,7,1,0,0,855,856,7,6,0, + 0,856,857,7,1,0,0,857,858,7,14,0,0,858,178,1,0,0,0,859,860,7,18,0,0,860, + 861,7,13,0,0,861,862,7,14,0,0,862,863,7,1,0,0,863,864,7,15,0,0,864,180, + 1,0,0,0,865,866,7,4,0,0,866,867,7,1,0,0,867,868,7,8,0,0,868,869,7,11,0, + 0,869,870,7,7,0,0,870,871,7,6,0,0,871,872,7,12,0,0,872,873,7,6,0,0,873, + 874,7,24,0,0,874,182,1,0,0,0,875,876,7,9,0,0,876,877,7,18,0,0,877,878, + 7,4,0,0,878,879,7,10,0,0,879,184,1,0,0,0,880,881,7,3,0,0,881,882,7,7,0, + 0,882,883,7,8,0,0,883,884,7,11,0,0,884,885,7,6,0,0,885,886,7,1,0,0,886, + 887,7,0,0,0,887,888,7,0,0,0,888,889,7,18,0,0,889,890,7,7,0,0,890,891,7, + 13,0,0,891,186,1,0,0,0,892,893,7,3,0,0,893,894,7,7,0,0,894,895,7,8,0,0, + 895,896,7,11,0,0,896,188,1,0,0,0,897,898,7,14,0,0,898,899,7,18,0,0,899, + 900,7,0,0,0,900,901,7,12,0,0,901,902,7,2,0,0,902,903,7,2,0,0,903,904,7, + 7,0,0,904,905,7,9,0,0,905,190,1,0,0,0,906,907,7,18,0,0,907,908,7,21,0, + 0,908,909,7,13,0,0,909,910,7,7,0,0,910,911,7,6,0,0,911,912,7,1,0,0,912, + 192,1,0,0,0,913,914,7,3,0,0,914,915,7,10,0,0,915,916,7,1,0,0,916,917,7, + 3,0,0,917,918,7,20,0,0,918,194,1,0,0,0,919,920,7,17,0,0,920,921,7,18,0, + 0,921,922,7,1,0,0,922,923,7,9,0,0,923,196,1,0,0,0,924,925,7,11,0,0,925, + 926,7,6,0,0,926,927,7,7,0,0,927,928,7,3,0,0,928,929,7,1,0,0,929,930,7, + 14,0,0,930,931,7,22,0,0,931,932,7,6,0,0,932,933,7,1,0,0,933,198,1,0,0, + 0,934,935,7,11,0,0,935,936,7,12,0,0,936,937,7,6,0,0,937,938,7,12,0,0,938, + 939,7,8,0,0,939,940,7,1,0,0,940,941,7,4,0,0,941,942,7,1,0,0,942,943,7, + 6,0,0,943,944,7,0,0,0,944,200,1,0,0,0,945,946,7,1,0,0,946,947,7,15,0,0, + 947,948,7,1,0,0,948,949,7,3,0,0,949,950,7,22,0,0,950,951,7,4,0,0,951,952, + 7,1,0,0,952,202,1,0,0,0,953,954,7,1,0,0,954,955,7,15,0,0,955,956,7,1,0, + 0,956,957,7,3,0,0,957,204,1,0,0,0,958,959,7,12,0,0,959,960,7,0,0,0,960, + 961,7,3,0,0,961,206,1,0,0,0,962,963,7,14,0,0,963,964,7,1,0,0,964,965,7, + 0,0,0,965,966,7,3,0,0,966,208,1,0,0,0,967,968,7,4,0,0,968,969,7,6,0,0, + 969,970,7,22,0,0,970,971,7,1,0,0,971,210,1,0,0,0,972,973,7,5,0,0,973,974, + 7,12,0,0,974,975,7,2,0,0,975,976,7,0,0,0,976,977,7,1,0,0,977,212,1,0,0, + 0,978,979,7,24,0,0,979,980,7,1,0,0,980,981,7,0,0,0,981,214,1,0,0,0,982, + 983,7,7,0,0,983,984,7,5,0,0,984,985,7,5,0,0,985,216,1,0,0,0,986,987,7, + 13,0,0,987,988,7,22,0,0,988,989,7,2,0,0,989,990,7,2,0,0,990,218,1,0,0, + 0,991,992,7,6,0,0,992,993,7,12,0,0,993,994,7,13,0,0,994,995,7,21,0,0,995, + 996,7,1,0,0,996,220,1,0,0,0,997,998,7,21,0,0,998,999,7,6,0,0,999,1000, + 7,7,0,0,1000,1001,7,22,0,0,1001,1002,7,11,0,0,1002,1003,7,0,0,0,1003,222, + 1,0,0,0,1004,1005,7,22,0,0,1005,1006,7,13,0,0,1006,1007,7,19,0,0,1007, + 1008,7,7,0,0,1008,1009,7,22,0,0,1009,1010,7,13,0,0,1010,1011,7,14,0,0, + 1011,1012,7,1,0,0,1012,1013,7,14,0,0,1013,224,1,0,0,0,1014,1015,7,11,0, + 0,1015,1016,7,6,0,0,1016,1017,7,1,0,0,1017,1018,7,3,0,0,1018,1019,7,1, + 0,0,1019,1020,7,14,0,0,1020,1021,7,18,0,0,1021,1022,7,13,0,0,1022,1023, + 7,21,0,0,1023,226,1,0,0,0,1024,1025,7,5,0,0,1025,1026,7,7,0,0,1026,1027, + 7,2,0,0,1027,1028,7,2,0,0,1028,1029,7,7,0,0,1029,1030,7,9,0,0,1030,1031, + 7,18,0,0,1031,1032,7,13,0,0,1032,1033,7,21,0,0,1033,228,1,0,0,0,1034,1035, + 7,3,0,0,1035,1036,7,22,0,0,1036,1037,7,6,0,0,1037,1038,7,6,0,0,1038,1039, + 7,1,0,0,1039,1040,7,13,0,0,1040,1041,7,4,0,0,1041,230,1,0,0,0,1042,1043, + 7,1,0,0,1043,1044,7,15,0,0,1044,1045,7,3,0,0,1045,1046,7,2,0,0,1046,1047, + 7,22,0,0,1047,1048,7,14,0,0,1048,1049,7,1,0,0,1049,232,1,0,0,0,1050,1051, + 7,4,0,0,1051,1052,7,18,0,0,1052,1053,7,1,0,0,1053,1054,7,0,0,0,1054,234, + 1,0,0,0,1055,1056,7,7,0,0,1056,1057,7,4,0,0,1057,1058,7,10,0,0,1058,1059, + 7,1,0,0,1059,1060,7,6,0,0,1060,1061,7,0,0,0,1061,236,1,0,0,0,1062,1063, + 7,9,0,0,1063,1064,7,18,0,0,1064,1065,7,4,0,0,1065,1066,7,10,0,0,1066,1067, + 7,18,0,0,1067,1068,7,13,0,0,1068,238,1,0,0,0,1069,1070,7,2,0,0,1070,1071, + 7,12,0,0,1071,1072,7,0,0,0,1072,1073,7,4,0,0,1073,240,1,0,0,0,1074,1075, + 7,6,0,0,1075,1076,7,1,0,0,1076,1077,7,0,0,0,1077,1078,7,11,0,0,1078,1079, + 7,1,0,0,1079,1080,7,3,0,0,1080,1081,7,4,0,0,1081,242,1,0,0,0,1082,1083, + 7,13,0,0,1083,1084,7,22,0,0,1084,1085,7,2,0,0,1085,1086,7,2,0,0,1086,1087, + 7,0,0,0,1087,244,1,0,0,0,1088,1089,7,5,0,0,1089,1090,7,18,0,0,1090,1091, + 7,2,0,0,1091,1092,7,4,0,0,1092,1093,7,1,0,0,1093,1094,7,6,0,0,1094,246, + 1,0,0,0,1095,1096,5,42,0,0,1096,248,1,0,0,0,1097,1098,5,47,0,0,1098,250, + 1,0,0,0,1099,1100,5,92,0,0,1100,252,1,0,0,0,1101,1102,5,94,0,0,1102,254, + 1,0,0,0,1103,1104,5,43,0,0,1104,256,1,0,0,0,1105,1106,5,45,0,0,1106,258, + 1,0,0,0,1107,1108,5,38,0,0,1108,260,1,0,0,0,1109,1110,5,61,0,0,1110,262, + 1,0,0,0,1111,1112,5,60,0,0,1112,1116,5,62,0,0,1113,1114,5,33,0,0,1114, + 1116,5,61,0,0,1115,1111,1,0,0,0,1115,1113,1,0,0,0,1116,264,1,0,0,0,1117, + 1118,5,60,0,0,1118,1119,5,61,0,0,1119,266,1,0,0,0,1120,1121,5,62,0,0,1121, + 1122,5,61,0,0,1122,268,1,0,0,0,1123,1124,5,60,0,0,1124,270,1,0,0,0,1125, + 1126,5,62,0,0,1126,272,1,0,0,0,1127,1128,5,40,0,0,1128,274,1,0,0,0,1129, + 1130,5,41,0,0,1130,276,1,0,0,0,1131,1132,5,44,0,0,1132,278,1,0,0,0,1133, + 1134,5,46,0,0,1134,280,1,0,0,0,1135,1136,5,33,0,0,1136,282,1,0,0,0,1137, + 1138,5,59,0,0,1138,284,1,0,0,0,1139,1140,5,64,0,0,1140,1141,5,64,0,0,1141, + 1142,1,0,0,0,1142,1146,7,25,0,0,1143,1145,7,26,0,0,1144,1143,1,0,0,0,1145, + 1148,1,0,0,0,1146,1144,1,0,0,0,1146,1147,1,0,0,0,1147,286,1,0,0,0,1148, + 1146,1,0,0,0,1149,1159,5,63,0,0,1150,1151,5,64,0,0,1151,1155,7,25,0,0, + 1152,1154,7,26,0,0,1153,1152,1,0,0,0,1154,1157,1,0,0,0,1155,1153,1,0,0, + 0,1155,1156,1,0,0,0,1156,1159,1,0,0,0,1157,1155,1,0,0,0,1158,1149,1,0, + 0,0,1158,1150,1,0,0,0,1159,288,1,0,0,0,1160,1161,5,48,0,0,1161,1163,7, + 15,0,0,1162,1164,7,27,0,0,1163,1162,1,0,0,0,1164,1165,1,0,0,0,1165,1163, + 1,0,0,0,1165,1166,1,0,0,0,1166,290,1,0,0,0,1167,1169,7,28,0,0,1168,1167, + 1,0,0,0,1169,1170,1,0,0,0,1170,1168,1,0,0,0,1170,1171,1,0,0,0,1171,292, + 1,0,0,0,1172,1174,7,28,0,0,1173,1172,1,0,0,0,1174,1175,1,0,0,0,1175,1173, + 1,0,0,0,1175,1176,1,0,0,0,1176,1177,1,0,0,0,1177,1181,5,46,0,0,1178,1180, + 7,28,0,0,1179,1178,1,0,0,0,1180,1183,1,0,0,0,1181,1179,1,0,0,0,1181,1182, + 1,0,0,0,1182,1185,1,0,0,0,1183,1181,1,0,0,0,1184,1186,3,295,147,0,1185, + 1184,1,0,0,0,1185,1186,1,0,0,0,1186,1203,1,0,0,0,1187,1189,5,46,0,0,1188, + 1190,7,28,0,0,1189,1188,1,0,0,0,1190,1191,1,0,0,0,1191,1189,1,0,0,0,1191, + 1192,1,0,0,0,1192,1194,1,0,0,0,1193,1195,3,295,147,0,1194,1193,1,0,0,0, + 1194,1195,1,0,0,0,1195,1203,1,0,0,0,1196,1198,7,28,0,0,1197,1196,1,0,0, + 0,1198,1199,1,0,0,0,1199,1197,1,0,0,0,1199,1200,1,0,0,0,1200,1201,1,0, + 0,0,1201,1203,3,295,147,0,1202,1173,1,0,0,0,1202,1187,1,0,0,0,1202,1197, + 1,0,0,0,1203,294,1,0,0,0,1204,1206,7,1,0,0,1205,1207,7,29,0,0,1206,1205, + 1,0,0,0,1206,1207,1,0,0,0,1207,1209,1,0,0,0,1208,1210,7,28,0,0,1209,1208, + 1,0,0,0,1210,1211,1,0,0,0,1211,1209,1,0,0,0,1211,1212,1,0,0,0,1212,296, + 1,0,0,0,1213,1219,5,34,0,0,1214,1218,8,30,0,0,1215,1216,5,34,0,0,1216, + 1218,5,34,0,0,1217,1214,1,0,0,0,1217,1215,1,0,0,0,1218,1221,1,0,0,0,1219, + 1217,1,0,0,0,1219,1220,1,0,0,0,1220,1222,1,0,0,0,1221,1219,1,0,0,0,1222, + 1234,5,34,0,0,1223,1229,5,39,0,0,1224,1228,8,31,0,0,1225,1226,5,39,0,0, + 1226,1228,5,39,0,0,1227,1224,1,0,0,0,1227,1225,1,0,0,0,1228,1231,1,0,0, + 0,1229,1227,1,0,0,0,1229,1230,1,0,0,0,1230,1232,1,0,0,0,1231,1229,1,0, + 0,0,1232,1234,5,39,0,0,1233,1213,1,0,0,0,1233,1223,1,0,0,0,1234,298,1, + 0,0,0,1235,1239,5,35,0,0,1236,1238,8,32,0,0,1237,1236,1,0,0,0,1238,1241, + 1,0,0,0,1239,1237,1,0,0,0,1239,1240,1,0,0,0,1240,1242,1,0,0,0,1241,1239, + 1,0,0,0,1242,1243,5,35,0,0,1243,300,1,0,0,0,1244,1245,5,123,0,0,1245,1246, + 3,303,151,0,1246,1247,5,125,0,0,1247,1271,1,0,0,0,1248,1249,5,123,0,0, + 1249,1250,7,21,0,0,1250,1251,7,22,0,0,1251,1252,7,18,0,0,1252,1256,7,14, + 0,0,1253,1255,7,33,0,0,1254,1253,1,0,0,0,1255,1258,1,0,0,0,1256,1254,1, + 0,0,0,1256,1257,1,0,0,0,1257,1259,1,0,0,0,1258,1256,1,0,0,0,1259,1260, + 5,123,0,0,1260,1261,3,303,151,0,1261,1265,5,125,0,0,1262,1264,7,33,0,0, + 1263,1262,1,0,0,0,1264,1267,1,0,0,0,1265,1263,1,0,0,0,1265,1266,1,0,0, + 0,1266,1268,1,0,0,0,1267,1265,1,0,0,0,1268,1269,5,125,0,0,1269,1271,1, + 0,0,0,1270,1244,1,0,0,0,1270,1248,1,0,0,0,1271,302,1,0,0,0,1272,1274,3, + 305,152,0,1273,1272,1,0,0,0,1274,1275,1,0,0,0,1275,1273,1,0,0,0,1275,1276, + 1,0,0,0,1276,1277,1,0,0,0,1277,1279,5,45,0,0,1278,1280,3,305,152,0,1279, + 1278,1,0,0,0,1280,1281,1,0,0,0,1281,1279,1,0,0,0,1281,1282,1,0,0,0,1282, + 1283,1,0,0,0,1283,1285,5,45,0,0,1284,1286,3,305,152,0,1285,1284,1,0,0, + 0,1286,1287,1,0,0,0,1287,1285,1,0,0,0,1287,1288,1,0,0,0,1288,1289,1,0, + 0,0,1289,1291,5,45,0,0,1290,1292,3,305,152,0,1291,1290,1,0,0,0,1292,1293, + 1,0,0,0,1293,1291,1,0,0,0,1293,1294,1,0,0,0,1294,1295,1,0,0,0,1295,1297, + 5,45,0,0,1296,1298,3,305,152,0,1297,1296,1,0,0,0,1298,1299,1,0,0,0,1299, + 1297,1,0,0,0,1299,1300,1,0,0,0,1300,304,1,0,0,0,1301,1302,7,27,0,0,1302, + 306,1,0,0,0,1303,1305,5,91,0,0,1304,1306,8,34,0,0,1305,1304,1,0,0,0,1306, + 1307,1,0,0,0,1307,1305,1,0,0,0,1307,1308,1,0,0,0,1308,1309,1,0,0,0,1309, + 1310,5,93,0,0,1310,308,1,0,0,0,1311,1313,5,96,0,0,1312,1314,8,35,0,0,1313, + 1312,1,0,0,0,1314,1315,1,0,0,0,1315,1313,1,0,0,0,1315,1316,1,0,0,0,1316, + 1317,1,0,0,0,1317,1318,5,96,0,0,1318,310,1,0,0,0,1319,1323,7,36,0,0,1320, + 1322,7,37,0,0,1321,1320,1,0,0,0,1322,1325,1,0,0,0,1323,1321,1,0,0,0,1323, + 1324,1,0,0,0,1324,1327,1,0,0,0,1325,1323,1,0,0,0,1326,1328,5,36,0,0,1327, + 1326,1,0,0,0,1327,1328,1,0,0,0,1328,312,1,0,0,0,1329,1331,7,38,0,0,1330, + 1329,1,0,0,0,1331,1332,1,0,0,0,1332,1330,1,0,0,0,1332,1333,1,0,0,0,1333, + 1334,1,0,0,0,1334,1335,6,156,0,0,1335,314,1,0,0,0,1336,1337,5,45,0,0,1337, + 1338,5,45,0,0,1338,1339,1,0,0,0,1339,1343,4,157,0,0,1340,1342,8,39,0,0, + 1341,1340,1,0,0,0,1342,1345,1,0,0,0,1343,1341,1,0,0,0,1343,1344,1,0,0, + 0,1344,1346,1,0,0,0,1345,1343,1,0,0,0,1346,1347,6,157,0,0,1347,316,1,0, + 0,0,1348,1349,5,47,0,0,1349,1350,5,42,0,0,1350,1354,1,0,0,0,1351,1353, + 9,0,0,0,1352,1351,1,0,0,0,1353,1356,1,0,0,0,1354,1355,1,0,0,0,1354,1352, + 1,0,0,0,1355,1357,1,0,0,0,1356,1354,1,0,0,0,1357,1358,5,42,0,0,1358,1359, + 5,47,0,0,1359,1360,1,0,0,0,1360,1361,6,158,0,0,1361,318,1,0,0,0,37,0,1115, + 1146,1155,1158,1165,1170,1175,1181,1185,1191,1194,1199,1202,1206,1211, + 1217,1219,1227,1229,1233,1239,1256,1265,1270,1275,1281,1287,1293,1299, + 1307,1315,1323,1327,1332,1343,1354,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 bdab9dcf..fd9f72e6 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs @@ -51,15 +51,15 @@ public const int NO=85, UNIQUE=86, CLUSTERED=87, IDENTITY=88, NONCLUSTERED=89, INDEX=90, TEMPORARY=91, WITH=92, COMPRESSION=93, COMP=94, DISALLOW=95, IGNORE=96, CHECK=97, VIEW=98, PROCEDURE=99, PARAMETERS=100, EXECUTE=101, EXEC=102, - ASC=103, DESC=104, TRUE=105, FALSE=106, NULL=107, RANGE=108, GROUPS=109, - UNBOUNDED=110, PRECEDING=111, FOLLOWING=112, CURRENT=113, EXCLUDE=114, - TIES=115, OTHERS=116, WITHIN=117, LAST=118, RESPECT=119, NULLS=120, FILTER=121, - STAR=122, SLASH=123, BACKSLASH=124, CARET=125, PLUS=126, MINUS=127, AMP=128, - EQ=129, NEQ=130, LTE=131, GTE=132, LT=133, GT=134, LPAREN=135, RPAREN=136, - COMMA=137, DOT=138, SEMI=139, SYSVAR=140, PARAM=141, HEX_LITERAL=142, - INTEGER_LITERAL=143, NUMBER_LITERAL=144, STRING_LITERAL=145, DATE_LITERAL=146, - GUID_LITERAL=147, BRACKET_ID=148, BACKTICK_ID=149, IDENTIFIER=150, WS=151, - LINE_COMMENT=152, BLOCK_COMMENT=153; + ASC=103, DESC=104, TRUE=105, FALSE=106, YES=107, OFF=108, NULL=109, RANGE=110, + GROUPS=111, UNBOUNDED=112, PRECEDING=113, FOLLOWING=114, CURRENT=115, + EXCLUDE=116, TIES=117, OTHERS=118, WITHIN=119, LAST=120, RESPECT=121, + NULLS=122, FILTER=123, STAR=124, SLASH=125, BACKSLASH=126, CARET=127, + PLUS=128, MINUS=129, AMP=130, EQ=131, NEQ=132, LTE=133, GTE=134, LT=135, + GT=136, LPAREN=137, RPAREN=138, COMMA=139, DOT=140, BANG=141, SEMI=142, + SYSVAR=143, PARAM=144, HEX_LITERAL=145, INTEGER_LITERAL=146, NUMBER_LITERAL=147, + STRING_LITERAL=148, DATE_LITERAL=149, GUID_LITERAL=150, BRACKET_ID=151, + BACKTICK_ID=152, IDENTIFIER=153, WS=154, LINE_COMMENT=155, BLOCK_COMMENT=156; public const int RULE_statement = 0, RULE_ifThenStatement = 1, RULE_thenBody = 2, RULE_executeStatement = 3, RULE_updateStatement = 4, RULE_assignment = 5, RULE_deleteStatement = 6, @@ -79,11 +79,12 @@ public const int RULE_tableSource = 49, RULE_tablePrimary = 50, RULE_joinClause = 51, RULE_joinType = 52, RULE_whereClause = 53, RULE_orderByClause = 54, RULE_orderByItem = 55, RULE_expression = 56, RULE_primary = 57, RULE_caseExpression = 58, RULE_caseWhen = 59, - RULE_functionCall = 60, RULE_functionName = 61, RULE_columnRef = 62, RULE_identifier = 63, - RULE_literal = 64, RULE_transactionStatement = 65, RULE_standaloneExpression = 66, - RULE_windowSpecification = 67, RULE_clusteredOption = 68, RULE_windowFrame = 69, - RULE_frameBound = 70, RULE_frameExclusion = 71, RULE_nonReservedKeyword = 72, - RULE_filterClause = 73, RULE_withinGroup = 74, RULE_nthRowFrom = 75, RULE_nullTreatment = 76; + RULE_functionCall = 60, RULE_functionName = 61, RULE_columnRef = 62, RULE_memberName = 63, + RULE_identifier = 64, RULE_literal = 65, RULE_transactionStatement = 66, + RULE_standaloneExpression = 67, RULE_windowSpecification = 68, RULE_clusteredOption = 69, + RULE_windowFrame = 70, RULE_frameBound = 71, RULE_frameExclusion = 72, + RULE_nonReservedKeyword = 73, RULE_reservedKeyword = 74, RULE_filterClause = 75, + RULE_withinGroup = 76, RULE_nthRowFrom = 77, RULE_nullTreatment = 78; public static readonly string[] ruleNames = { "statement", "ifThenStatement", "thenBody", "executeStatement", "updateStatement", "assignment", "deleteStatement", "systemVariableSelect", "sysVarItem", @@ -98,10 +99,10 @@ public const int "selectList", "selectItem", "fromClause", "tableSource", "tablePrimary", "joinClause", "joinType", "whereClause", "orderByClause", "orderByItem", "expression", "primary", "caseExpression", "caseWhen", "functionCall", - "functionName", "columnRef", "identifier", "literal", "transactionStatement", + "functionName", "columnRef", "memberName", "identifier", "literal", "transactionStatement", "standaloneExpression", "windowSpecification", "clusteredOption", "windowFrame", - "frameBound", "frameExclusion", "nonReservedKeyword", "filterClause", - "withinGroup", "nthRowFrom", "nullTreatment" + "frameBound", "frameExclusion", "nonReservedKeyword", "reservedKeyword", + "filterClause", "withinGroup", "nthRowFrom", "nullTreatment" }; private static readonly string[] _LiteralNames = { @@ -115,8 +116,9 @@ 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, "'*'", "'/'", "'\\'", "'^'", "'+'", "'-'", "'&'", + "'='", null, "'<='", "'>='", "'<'", "'>'", "'('", "')'", "','", "'.'", + "'!'", "';'" }; private static readonly string[] _SymbolicNames = { null, "SELECT", "FROM", "WHERE", "TOP", "AS", "AND", "OR", "NOT", "XOR", @@ -132,13 +134,14 @@ public const int "SET", "DEFAULT", "NO", "UNIQUE", "CLUSTERED", "IDENTITY", "NONCLUSTERED", "INDEX", "TEMPORARY", "WITH", "COMPRESSION", "COMP", "DISALLOW", "IGNORE", "CHECK", "VIEW", "PROCEDURE", "PARAMETERS", "EXECUTE", "EXEC", "ASC", - "DESC", "TRUE", "FALSE", "NULL", "RANGE", "GROUPS", "UNBOUNDED", "PRECEDING", - "FOLLOWING", "CURRENT", "EXCLUDE", "TIES", "OTHERS", "WITHIN", "LAST", - "RESPECT", "NULLS", "FILTER", "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", "STRING_LITERAL", "DATE_LITERAL", "GUID_LITERAL", "BRACKET_ID", - "BACKTICK_ID", "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT" + "DESC", "TRUE", "FALSE", "YES", "OFF", "NULL", "RANGE", "GROUPS", "UNBOUNDED", + "PRECEDING", "FOLLOWING", "CURRENT", "EXCLUDE", "TIES", "OTHERS", "WITHIN", + "LAST", "RESPECT", "NULLS", "FILTER", "STAR", "SLASH", "BACKSLASH", "CARET", + "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", "LT", "GT", "LPAREN", + "RPAREN", "COMMA", "DOT", "BANG", "SEMI", "SYSVAR", "PARAM", "HEX_LITERAL", + "INTEGER_LITERAL", "NUMBER_LITERAL", "STRING_LITERAL", "DATE_LITERAL", + "GUID_LITERAL", "BRACKET_ID", "BACKTICK_ID", "IDENTIFIER", "WS", "LINE_COMMENT", + "BLOCK_COMMENT" }; public static readonly IVocabulary DefaultVocabulary = new Vocabulary(_LiteralNames, _SymbolicNames); @@ -253,115 +256,115 @@ public StatementContext statement() { try { EnterOuterAlt(_localctx, 1); { - State = 155; + State = 159; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PARAMETERS) { { - State = 154; + State = 158; parametersClause(); } } - State = 171; + State = 175; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,1,Context) ) { case 1: { - State = 157; + State = 161; ifThenStatement(); } break; case 2: { - State = 158; + State = 162; createTableStatement(); } break; case 3: { - State = 159; + State = 163; createIndexStatement(); } break; case 4: { - State = 160; + State = 164; createViewStatement(); } break; case 5: { - State = 161; + State = 165; createProcedureStatement(); } break; case 6: { - State = 162; + State = 166; alterTableStatement(); } break; case 7: { - State = 163; + State = 167; dropStatement(); } break; case 8: { - State = 164; + State = 168; insertStatement(); } break; case 9: { - State = 165; + State = 169; updateStatement(); } break; case 10: { - State = 166; + State = 170; deleteStatement(); } break; case 11: { - State = 167; + State = 171; transactionStatement(); } break; case 12: { - State = 168; + State = 172; executeStatement(); } break; case 13: { - State = 169; + State = 173; systemVariableSelect(); } break; case 14: { - State = 170; + State = 174; queryExpression(); } break; } - State = 174; + State = 178; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==SEMI) { { - State = 173; + State = 177; Match(SEMI); } } - State = 176; + State = 180; Match(Eof); } } @@ -411,29 +414,29 @@ public IfThenStatementContext ifThenStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 178; + State = 182; Match(IF); - State = 180; + State = 184; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 179; + State = 183; _localctx.not = Match(NOT); } } - State = 182; + State = 186; Match(EXISTS); - State = 183; + State = 187; Match(LPAREN); - State = 184; + State = 188; queryExpression(); - State = 185; + State = 189; Match(RPAREN); - State = 186; + State = 190; Match(THEN); - State = 187; + State = 191; thenBody(); } } @@ -500,83 +503,83 @@ public ThenBodyContext thenBody() { ThenBodyContext _localctx = new ThenBodyContext(Context, State); EnterRule(_localctx, 4, RULE_thenBody); try { - State = 200; + State = 204; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,4,Context) ) { case 1: EnterOuterAlt(_localctx, 1); { - State = 189; + State = 193; createTableStatement(); } break; case 2: EnterOuterAlt(_localctx, 2); { - State = 190; + State = 194; createIndexStatement(); } break; case 3: EnterOuterAlt(_localctx, 3); { - State = 191; + State = 195; createViewStatement(); } break; case 4: EnterOuterAlt(_localctx, 4); { - State = 192; + State = 196; createProcedureStatement(); } break; case 5: EnterOuterAlt(_localctx, 5); { - State = 193; + State = 197; alterTableStatement(); } break; case 6: EnterOuterAlt(_localctx, 6); { - State = 194; + State = 198; dropStatement(); } break; case 7: EnterOuterAlt(_localctx, 7); { - State = 195; + State = 199; insertStatement(); } break; case 8: EnterOuterAlt(_localctx, 8); { - State = 196; + State = 200; updateStatement(); } break; case 9: EnterOuterAlt(_localctx, 9); { - State = 197; + State = 201; deleteStatement(); } break; case 10: EnterOuterAlt(_localctx, 10); { - State = 198; + State = 202; executeStatement(); } break; case 11: EnterOuterAlt(_localctx, 11); { - State = 199; + State = 203; queryExpression(); } break; @@ -631,7 +634,7 @@ public ExecuteStatementContext executeStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 202; + State = 206; _la = TokenStream.LA(1); if ( !(_la==EXECUTE || _la==EXEC) ) { ErrorHandler.RecoverInline(this); @@ -640,28 +643,28 @@ public ExecuteStatementContext executeStatement() { ErrorHandler.ReportMatch(this); Consume(); } - State = 203; + State = 207; _localctx.name = identifier(); - State = 212; + State = 216; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775660683520L) != 0) || ((((_la - 103)) & ~0x3f) == 0 && ((1L << (_la - 103)) & 281341858414589L) != 0)) { + if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775694237952L) != 0) || ((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & -283699838490247167L) != 0) || ((((_la - 149)) & ~0x3f) == 0 && ((1L << (_la - 149)) & 31L) != 0)) { { - State = 204; + State = 208; expression(0); - State = 209; + State = 213; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 205; + State = 209; Match(COMMA); - State = 206; + State = 210; expression(0); } } - State = 211; + State = 215; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -724,52 +727,52 @@ public UpdateStatementContext updateStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 214; + State = 218; Match(UPDATE); - State = 215; + State = 219; tableSource(); - State = 220; + State = 224; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 216; + State = 220; Match(COMMA); - State = 217; + State = 221; tableSource(); } } - State = 222; + State = 226; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 223; + State = 227; Match(SET); - State = 224; + State = 228; assignment(); - State = 229; + State = 233; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 225; + State = 229; Match(COMMA); - State = 226; + State = 230; assignment(); } } - State = 231; + State = 235; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 233; + State = 237; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 232; + State = 236; whereClause(); } } @@ -816,11 +819,11 @@ public AssignmentContext assignment() { try { EnterOuterAlt(_localctx, 1); { - State = 235; + State = 239; _localctx.target = columnRef(); - State = 236; + State = 240; Match(EQ); - State = 237; + State = 241; expression(0); } } @@ -878,11 +881,13 @@ public DeleteStatementContext deleteStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 239; + State = 243; Match(DELETE); - State = 245; + State = 249; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { + case YES: + case OFF: case RANGE: case GROUPS: case UNBOUNDED: @@ -901,17 +906,17 @@ public DeleteStatementContext deleteStatement() { case BACKTICK_ID: case IDENTIFIER: { - State = 240; + State = 244; _localctx.target = identifier(); - State = 241; + State = 245; Match(DOT); - State = 242; + State = 246; Match(STAR); } break; case STAR: { - State = 244; + State = 248; Match(STAR); } break; @@ -920,32 +925,32 @@ public DeleteStatementContext deleteStatement() { default: break; } - State = 247; + State = 251; Match(FROM); - State = 248; + State = 252; tableSource(); - State = 253; + State = 257; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 249; + State = 253; Match(COMMA); - State = 250; + State = 254; tableSource(); } } - State = 255; + State = 259; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 257; + State = 261; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 256; + State = 260; whereClause(); } } @@ -996,23 +1001,23 @@ public SystemVariableSelectContext systemVariableSelect() { try { EnterOuterAlt(_localctx, 1); { - State = 259; + State = 263; Match(SELECT); - State = 260; + State = 264; sysVarItem(); - State = 265; + State = 269; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 261; + State = 265; Match(COMMA); - State = 262; + State = 266; sysVarItem(); } } - State = 267; + State = 271; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -1057,24 +1062,24 @@ public SysVarItemContext sysVarItem() { try { EnterOuterAlt(_localctx, 1); { - State = 268; + State = 272; Match(SYSVAR); - State = 273; + State = 277; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) { + if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 270; + State = 274; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 269; + State = 273; Match(AS); } } - State = 272; + State = 276; _localctx.alias = identifier(); } } @@ -1126,27 +1131,27 @@ public ParametersClauseContext parametersClause() { try { EnterOuterAlt(_localctx, 1); { - State = 275; + State = 279; Match(PARAMETERS); - State = 276; + State = 280; procParam(); - State = 281; + State = 285; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 277; + State = 281; Match(COMMA); - State = 278; + State = 282; procParam(); } } - State = 283; + State = 287; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 284; + State = 288; Match(SEMI); } } @@ -1210,61 +1215,61 @@ public CreateTableStatementContext createTableStatement() { int _alt; EnterOuterAlt(_localctx, 1); { - State = 286; + State = 290; Match(CREATE); - State = 288; + State = 292; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TEMPORARY) { { - State = 287; + State = 291; _localctx.temp = Match(TEMPORARY); } } - State = 290; + State = 294; Match(TABLE); - State = 291; + State = 295; _localctx.table = identifier(); - State = 292; + State = 296; Match(LPAREN); - State = 293; + State = 297; columnDefinition(); - State = 298; + State = 302; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,18,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 294; + State = 298; Match(COMMA); - State = 295; + State = 299; columnDefinition(); } } } - State = 300; + State = 304; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,18,Context); } - State = 305; + State = 309; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 301; + State = 305; Match(COMMA); - State = 302; + State = 306; tableConstraint(); } } - State = 307; + State = 311; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 308; + State = 312; Match(RPAREN); } } @@ -1323,47 +1328,47 @@ public CreateViewStatementContext createViewStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 310; + State = 314; Match(CREATE); - State = 311; + State = 315; Match(VIEW); - State = 312; + State = 316; _localctx.name = identifier(); - State = 324; + State = 328; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 313; + State = 317; Match(LPAREN); - State = 314; + State = 318; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); - State = 319; + State = 323; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 315; + State = 319; Match(COMMA); - State = 316; + State = 320; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); } } - State = 321; + State = 325; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 322; + State = 326; Match(RPAREN); } } - State = 326; + State = 330; Match(AS); - State = 327; + State = 331; _localctx.query = queryExpression(); } } @@ -1414,25 +1419,25 @@ public CreateProcedureStatementContext createProcedureStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 329; + State = 333; Match(CREATE); - State = 330; + State = 334; Match(PROCEDURE); - State = 331; + State = 335; _localctx.name = identifier(); - State = 333; + State = 337; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7705305563135L) != 0)) { + if (((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123283815137275L) != 0)) { { - State = 332; + State = 336; procParamList(); } } - State = 335; + State = 339; Match(AS); - State = 336; + State = 340; _localctx.body = procedureBody(); } } @@ -1479,36 +1484,38 @@ public ProcParamListContext procParamList() { EnterRule(_localctx, 26, RULE_procParamList); int _la; try { - State = 357; + State = 361; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case LPAREN: EnterOuterAlt(_localctx, 1); { - State = 338; + State = 342; Match(LPAREN); - State = 339; + State = 343; procParam(); - State = 344; + State = 348; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 340; + State = 344; Match(COMMA); - State = 341; + State = 345; procParam(); } } - State = 346; + State = 350; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 347; + State = 351; Match(RPAREN); } break; + case YES: + case OFF: case RANGE: case GROUPS: case UNBOUNDED: @@ -1529,21 +1536,21 @@ public ProcParamListContext procParamList() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 349; + State = 353; procParam(); - State = 354; + State = 358; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 350; + State = 354; Match(COMMA); - State = 351; + State = 355; procParam(); } } - State = 356; + State = 360; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -1592,9 +1599,9 @@ public ProcParamContext procParam() { try { EnterOuterAlt(_localctx, 1); { - State = 359; + State = 363; _localctx.pname = procParamName(); - State = 360; + State = 364; dataType(); } } @@ -1610,8 +1617,8 @@ public ProcParamContext procParam() { } public partial class ProcParamNameContext : ParserRuleContext { - [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() { - return GetRuleContext(0); + [System.Diagnostics.DebuggerNonUserCode] public ColumnRefContext columnRef() { + return GetRuleContext(0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode PARAM() { return GetToken(AccessSqlParser.PARAM, 0); } public ProcParamNameContext(ParserRuleContext parent, int invokingState) @@ -1632,9 +1639,11 @@ public ProcParamNameContext procParamName() { ProcParamNameContext _localctx = new ProcParamNameContext(Context, State); EnterRule(_localctx, 30, RULE_procParamName); try { - State = 364; + State = 368; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { + case YES: + case OFF: case RANGE: case GROUPS: case UNBOUNDED: @@ -1654,14 +1663,14 @@ public ProcParamNameContext procParamName() { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 362; - identifier(); + State = 366; + columnRef(); } break; case PARAM: EnterOuterAlt(_localctx, 2); { - State = 363; + State = 367; Match(PARAM); } break; @@ -1710,13 +1719,13 @@ public AlterTableStatementContext alterTableStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 366; + State = 370; Match(ALTER); - State = 367; + State = 371; Match(TABLE); - State = 368; + State = 372; _localctx.table = identifier(); - State = 369; + State = 373; alterTableAction(); } } @@ -1923,26 +1932,26 @@ public AlterTableActionContext alterTableAction() { EnterRule(_localctx, 34, RULE_alterTableAction); int _la; try { - State = 428; + State = 432; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,32,Context) ) { case 1: _localctx = new AddColumnActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 371; + State = 375; Match(ADD); - State = 373; + State = 377; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 372; + State = 376; Match(COLUMN); } } - State = 375; + State = 379; columnDefinition(); } break; @@ -1950,9 +1959,9 @@ public AlterTableActionContext alterTableAction() { _localctx = new AddConstraintActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 376; + State = 380; Match(ADD); - State = 377; + State = 381; tableConstraint(); } break; @@ -1960,33 +1969,33 @@ public AlterTableActionContext alterTableAction() { _localctx = new AlterColumnActionContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 378; + State = 382; Match(ALTER); - State = 380; + State = 384; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 379; + State = 383; Match(COLUMN); } } - State = 382; + State = 386; ((AlterColumnActionContext)_localctx).field = identifier(); - State = 383; - dataType(); State = 387; + dataType(); + State = 391; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 17197213717L) != 0)) { + while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 68736821269L) != 0)) { { { - State = 384; + State = 388; columnConstraint(); } } - State = 389; + State = 393; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -1996,25 +2005,25 @@ public AlterTableActionContext alterTableAction() { _localctx = new AlterColumnSetDefaultActionContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 390; + State = 394; Match(ALTER); - State = 392; + State = 396; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 391; + State = 395; Match(COLUMN); } } - State = 394; + State = 398; ((AlterColumnSetDefaultActionContext)_localctx).field = identifier(); - State = 395; + State = 399; Match(SET); - State = 396; + State = 400; Match(DEFAULT); - State = 397; + State = 401; expression(0); } break; @@ -2022,23 +2031,23 @@ public AlterTableActionContext alterTableAction() { _localctx = new AlterColumnDropDefaultActionContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 399; + State = 403; Match(ALTER); - State = 401; + State = 405; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 400; + State = 404; Match(COLUMN); } } - State = 403; + State = 407; ((AlterColumnDropDefaultActionContext)_localctx).field = identifier(); - State = 404; + State = 408; Match(DROP); - State = 405; + State = 409; Match(DEFAULT); } break; @@ -2046,11 +2055,11 @@ public AlterTableActionContext alterTableAction() { _localctx = new DropColumnActionContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 407; + State = 411; Match(DROP); - State = 408; + State = 412; Match(COLUMN); - State = 409; + State = 413; ((DropColumnActionContext)_localctx).field = identifier(); } break; @@ -2058,11 +2067,11 @@ public AlterTableActionContext alterTableAction() { _localctx = new DropConstraintActionContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 410; + State = 414; Match(DROP); - State = 411; + State = 415; Match(CONSTRAINT); - State = 412; + State = 416; ((DropConstraintActionContext)_localctx).cname = identifier(); } break; @@ -2070,11 +2079,11 @@ public AlterTableActionContext alterTableAction() { _localctx = new RenameTableActionContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 413; + State = 417; Match(RENAME); - State = 414; + State = 418; Match(TO); - State = 415; + State = 419; ((RenameTableActionContext)_localctx).newName = identifier(); } break; @@ -2082,15 +2091,15 @@ public AlterTableActionContext alterTableAction() { _localctx = new RenameColumnActionContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 416; + State = 420; Match(RENAME); - State = 417; + State = 421; Match(COLUMN); - State = 418; + State = 422; ((RenameColumnActionContext)_localctx).field = identifier(); - State = 419; + State = 423; Match(TO); - State = 420; + State = 424; ((RenameColumnActionContext)_localctx).newName = identifier(); } break; @@ -2098,15 +2107,15 @@ public AlterTableActionContext alterTableAction() { _localctx = new RenameIndexActionContext(_localctx); EnterOuterAlt(_localctx, 10); { - State = 422; + State = 426; Match(RENAME); - State = 423; + State = 427; Match(INDEX); - State = 424; + State = 428; ((RenameIndexActionContext)_localctx).index = identifier(); - State = 425; + State = 429; Match(TO); - State = 426; + State = 430; ((RenameIndexActionContext)_localctx).newName = identifier(); } break; @@ -2157,7 +2166,7 @@ public ProcedureBodyContext procedureBody() { ProcedureBodyContext _localctx = new ProcedureBodyContext(Context, State); EnterRule(_localctx, 36, RULE_procedureBody); try { - State = 435; + State = 439; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: @@ -2165,35 +2174,35 @@ public ProcedureBodyContext procedureBody() { case LPAREN: EnterOuterAlt(_localctx, 1); { - State = 430; + State = 434; queryExpression(); } break; case INSERT: EnterOuterAlt(_localctx, 2); { - State = 431; + State = 435; insertStatement(); } break; case UPDATE: EnterOuterAlt(_localctx, 3); { - State = 432; + State = 436; updateStatement(); } break; case DELETE: EnterOuterAlt(_localctx, 4); { - State = 433; + State = 437; deleteStatement(); } break; case CREATE: EnterOuterAlt(_localctx, 5); { - State = 434; + State = 438; createTableStatement(); } break; @@ -2263,56 +2272,56 @@ public CreateIndexStatementContext createIndexStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 437; + State = 441; Match(CREATE); - State = 439; + State = 443; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==UNIQUE) { { - State = 438; + State = 442; _localctx.unique = Match(UNIQUE); } } - State = 441; + State = 445; Match(INDEX); - State = 442; + State = 446; _localctx.name = identifier(); - State = 443; + State = 447; Match(ON); - State = 444; + State = 448; _localctx.table = identifier(); - State = 445; + State = 449; Match(LPAREN); - State = 446; + State = 450; indexColumn(); - State = 451; + State = 455; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 447; + State = 451; Match(COMMA); - State = 448; + State = 452; indexColumn(); } } - State = 453; + State = 457; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 454; + State = 458; Match(RPAREN); - State = 457; + State = 461; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WITH) { { - State = 455; + State = 459; Match(WITH); - State = 456; + State = 460; withOption(); } } @@ -2413,18 +2422,18 @@ public DropStatementContext dropStatement() { DropStatementContext _localctx = new DropStatementContext(Context, State); EnterRule(_localctx, 40, RULE_dropStatement); try { - State = 474; + State = 478; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,37,Context) ) { case 1: _localctx = new DropTableStatementContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 459; + State = 463; Match(DROP); - State = 460; + State = 464; Match(TABLE); - State = 461; + State = 465; ((DropTableStatementContext)_localctx).table = identifier(); } break; @@ -2432,15 +2441,15 @@ public DropStatementContext dropStatement() { _localctx = new DropIndexStatementContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 462; + State = 466; Match(DROP); - State = 463; + State = 467; Match(INDEX); - State = 464; + State = 468; ((DropIndexStatementContext)_localctx).index = identifier(); - State = 465; + State = 469; Match(ON); - State = 466; + State = 470; ((DropIndexStatementContext)_localctx).table = identifier(); } break; @@ -2448,11 +2457,11 @@ public DropStatementContext dropStatement() { _localctx = new DropProcedureStatementContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 468; + State = 472; Match(DROP); - State = 469; + State = 473; Match(PROCEDURE); - State = 470; + State = 474; ((DropProcedureStatementContext)_localctx).proc = identifier(); } break; @@ -2460,11 +2469,11 @@ public DropStatementContext dropStatement() { _localctx = new DropViewStatementContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 471; + State = 475; Match(DROP); - State = 472; + State = 476; Match(VIEW); - State = 473; + State = 477; ((DropViewStatementContext)_localctx).view = identifier(); } break; @@ -2510,14 +2519,14 @@ public IndexColumnContext indexColumn() { try { EnterOuterAlt(_localctx, 1); { - State = 476; + State = 480; _localctx.col = identifier(); - State = 478; + State = 482; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ASC || _la==DESC) { { - State = 477; + State = 481; _localctx.dir = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -2593,14 +2602,14 @@ public WithOptionContext withOption() { WithOptionContext _localctx = new WithOptionContext(Context, State); EnterRule(_localctx, 44, RULE_withOption); try { - State = 485; + State = 489; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case PRIMARY: _localctx = new WithPrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 480; + State = 484; Match(PRIMARY); } break; @@ -2608,9 +2617,9 @@ public WithOptionContext withOption() { _localctx = new WithDisallowNullContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 481; + State = 485; Match(DISALLOW); - State = 482; + State = 486; Match(NULL); } break; @@ -2618,9 +2627,9 @@ public WithOptionContext withOption() { _localctx = new WithIgnoreNullContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 483; + State = 487; Match(IGNORE); - State = 484; + State = 488; Match(NULL); } break; @@ -2677,31 +2686,31 @@ public ColumnDefinitionContext columnDefinition() { try { EnterOuterAlt(_localctx, 1); { - State = 487; + State = 491; _localctx.name = identifier(); - State = 488; + State = 492; dataType(); - State = 490; + State = 494; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 489; + State = 493; calculatedClause(); } } - State = 495; + State = 499; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 17197213717L) != 0)) { + while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 68736821269L) != 0)) { { { - State = 492; + State = 496; columnConstraint(); } } - State = 497; + State = 501; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -2745,13 +2754,13 @@ public CalculatedClauseContext calculatedClause() { try { EnterOuterAlt(_localctx, 1); { - State = 498; + State = 502; Match(AS); - State = 499; + State = 503; Match(LPAREN); - State = 500; + State = 504; expression(0); - State = 501; + State = 505; Match(RPAREN); } } @@ -2810,9 +2819,11 @@ public DataTypeContext dataType() { try { EnterOuterAlt(_localctx, 1); { - State = 505; + State = 509; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { + case YES: + case OFF: case RANGE: case GROUPS: case UNBOUNDED: @@ -2831,61 +2842,61 @@ public DataTypeContext dataType() { case BACKTICK_ID: case IDENTIFIER: { - State = 503; + State = 507; _localctx.typeName = identifier(); } break; case IDENTITY: { - State = 504; + State = 508; _localctx.identityType = Match(IDENTITY); } break; default: throw new NoViableAltException(this); } - State = 508; + State = 512; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,43,Context) ) { case 1: { - State = 507; + State = 511; _localctx.extra = identifier(); } break; } - State = 511; + State = 515; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) { + if (((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 510; + State = 514; _localctx.extra2 = identifier(); } } - State = 521; + State = 525; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 513; + State = 517; Match(LPAREN); - State = 514; + State = 518; _localctx.size = signedInteger(); - State = 517; + State = 521; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COMMA) { { - State = 515; + State = 519; Match(COMMA); - State = 516; + State = 520; _localctx.scale = signedInteger(); } } - State = 519; + State = 523; Match(RPAREN); } } @@ -2927,17 +2938,17 @@ public SignedIntegerContext signedInteger() { try { EnterOuterAlt(_localctx, 1); { - State = 524; + State = 528; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==MINUS) { { - State = 523; + State = 527; Match(MINUS); } } - State = 526; + State = 530; Match(INTEGER_LITERAL); } } @@ -3128,16 +3139,16 @@ public ColumnConstraintContext columnConstraint() { EnterRule(_localctx, 54, RULE_columnConstraint); int _la; try { - State = 597; + State = 601; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,59,Context) ) { case 1: _localctx = new NotNullConstraintContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 528; + State = 532; Match(NOT); - State = 529; + State = 533; Match(NULL); } break; @@ -3145,7 +3156,7 @@ public ColumnConstraintContext columnConstraint() { _localctx = new NullableConstraintContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 530; + State = 534; Match(NULL); } break; @@ -3153,9 +3164,9 @@ public ColumnConstraintContext columnConstraint() { _localctx = new DefaultConstraintContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 531; + State = 535; Match(DEFAULT); - State = 532; + State = 536; expression(0); } break; @@ -3163,9 +3174,9 @@ public ColumnConstraintContext columnConstraint() { _localctx = new CompressionConstraintContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 533; + State = 537; Match(WITH); - State = 534; + State = 538; _la = TokenStream.LA(1); if ( !(_la==COMPRESSION || _la==COMP) ) { ErrorHandler.RecoverInline(this); @@ -3180,25 +3191,25 @@ public ColumnConstraintContext columnConstraint() { _localctx = new CheckColumnConstraintContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 537; + State = 541; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 535; + State = 539; Match(CONSTRAINT); - State = 536; + State = 540; ((CheckColumnConstraintContext)_localctx).cname = identifier(); } } - State = 539; + State = 543; Match(CHECK); - State = 540; + State = 544; Match(LPAREN); - State = 541; + State = 545; checkBody(); - State = 542; + State = 546; Match(RPAREN); } break; @@ -3206,28 +3217,28 @@ public ColumnConstraintContext columnConstraint() { _localctx = new PrimaryKeyConstraintContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 546; + State = 550; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 544; + State = 548; Match(CONSTRAINT); - State = 545; + State = 549; ((PrimaryKeyConstraintContext)_localctx).cname = identifier(); } } - State = 548; + State = 552; Match(PRIMARY); - State = 549; + State = 553; Match(KEY); - State = 551; + State = 555; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 550; + State = 554; clusteredOption(); } } @@ -3238,26 +3249,26 @@ public ColumnConstraintContext columnConstraint() { _localctx = new UniqueColumnConstraintContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 555; + State = 559; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 553; + State = 557; Match(CONSTRAINT); - State = 554; + State = 558; ((UniqueColumnConstraintContext)_localctx).cname = identifier(); } } - State = 557; + State = 561; Match(UNIQUE); - State = 559; + State = 563; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 558; + State = 562; clusteredOption(); } } @@ -3268,65 +3279,65 @@ public ColumnConstraintContext columnConstraint() { _localctx = new ColumnReferencesConstraintContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 563; + State = 567; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 561; + State = 565; Match(CONSTRAINT); - State = 562; + State = 566; ((ColumnReferencesConstraintContext)_localctx).cname = identifier(); } } - State = 565; + State = 569; Match(REFERENCES); - State = 566; + State = 570; ((ColumnReferencesConstraintContext)_localctx).refTable = identifier(); - State = 578; + State = 582; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 567; + State = 571; Match(LPAREN); - State = 568; + State = 572; ((ColumnReferencesConstraintContext)_localctx)._identifier = identifier(); ((ColumnReferencesConstraintContext)_localctx)._refColumns.Add(((ColumnReferencesConstraintContext)_localctx)._identifier); - State = 573; + State = 577; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 569; + State = 573; Match(COMMA); - State = 570; + State = 574; ((ColumnReferencesConstraintContext)_localctx)._identifier = identifier(); ((ColumnReferencesConstraintContext)_localctx)._refColumns.Add(((ColumnReferencesConstraintContext)_localctx)._identifier); } } - State = 575; + State = 579; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 576; + State = 580; Match(RPAREN); } } - State = 583; + State = 587; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==ON) { { { - State = 580; + State = 584; foreignKeyAction(); } } - State = 585; + State = 589; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -3336,30 +3347,30 @@ public ColumnConstraintContext columnConstraint() { _localctx = new IdentityConstraintContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 586; + State = 590; Match(IDENTITY); - State = 595; + State = 599; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 587; + State = 591; Match(LPAREN); - State = 588; + State = 592; ((IdentityConstraintContext)_localctx).seed = signedInteger(); - State = 591; + State = 595; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COMMA) { { - State = 589; + State = 593; Match(COMMA); - State = 590; + State = 594; ((IdentityConstraintContext)_localctx).increment = signedInteger(); } } - State = 593; + State = 597; Match(RPAREN); } } @@ -3522,62 +3533,62 @@ public TableConstraintContext tableConstraint() { EnterRule(_localctx, 56, RULE_tableConstraint); int _la; try { - State = 688; + State = 692; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,73,Context) ) { case 1: _localctx = new PrimaryKeyTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 601; + State = 605; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 599; + State = 603; Match(CONSTRAINT); - State = 600; + State = 604; ((PrimaryKeyTableConstraintContext)_localctx).name = identifier(); } } - State = 603; + State = 607; Match(PRIMARY); - State = 604; + State = 608; Match(KEY); - State = 606; + State = 610; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 605; + State = 609; clusteredOption(); } } - State = 608; + State = 612; Match(LPAREN); - State = 609; + State = 613; ((PrimaryKeyTableConstraintContext)_localctx)._identifier = identifier(); ((PrimaryKeyTableConstraintContext)_localctx)._columns.Add(((PrimaryKeyTableConstraintContext)_localctx)._identifier); - State = 614; + State = 618; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 610; + State = 614; Match(COMMA); - State = 611; + State = 615; ((PrimaryKeyTableConstraintContext)_localctx)._identifier = identifier(); ((PrimaryKeyTableConstraintContext)_localctx)._columns.Add(((PrimaryKeyTableConstraintContext)_localctx)._identifier); } } - State = 616; + State = 620; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 617; + State = 621; Match(RPAREN); } break; @@ -3585,53 +3596,53 @@ public TableConstraintContext tableConstraint() { _localctx = new UniqueTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 621; + State = 625; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 619; + State = 623; Match(CONSTRAINT); - State = 620; + State = 624; ((UniqueTableConstraintContext)_localctx).name = identifier(); } } - State = 623; + State = 627; Match(UNIQUE); - State = 625; + State = 629; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 624; + State = 628; clusteredOption(); } } - State = 627; + State = 631; Match(LPAREN); - State = 628; + State = 632; ((UniqueTableConstraintContext)_localctx)._identifier = identifier(); ((UniqueTableConstraintContext)_localctx)._columns.Add(((UniqueTableConstraintContext)_localctx)._identifier); - State = 633; + State = 637; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 629; + State = 633; Match(COMMA); - State = 630; + State = 634; ((UniqueTableConstraintContext)_localctx)._identifier = identifier(); ((UniqueTableConstraintContext)_localctx)._columns.Add(((UniqueTableConstraintContext)_localctx)._identifier); } } - State = 635; + State = 639; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 636; + State = 640; Match(RPAREN); } break; @@ -3639,105 +3650,105 @@ public TableConstraintContext tableConstraint() { _localctx = new ForeignKeyTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 640; + State = 644; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 638; + State = 642; Match(CONSTRAINT); - State = 639; + State = 643; ((ForeignKeyTableConstraintContext)_localctx).name = identifier(); } } - State = 642; + State = 646; Match(FOREIGN); - State = 643; + State = 647; Match(KEY); - State = 646; + State = 650; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NO) { { - State = 644; + State = 648; ((ForeignKeyTableConstraintContext)_localctx).noIndex = Match(NO); - State = 645; + State = 649; Match(INDEX); } } - State = 648; + State = 652; Match(LPAREN); - State = 649; + State = 653; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._columns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); - State = 654; + State = 658; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 650; + State = 654; Match(COMMA); - State = 651; + State = 655; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._columns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); } } - State = 656; + State = 660; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 657; + State = 661; Match(RPAREN); - State = 658; + State = 662; Match(REFERENCES); - State = 659; + State = 663; ((ForeignKeyTableConstraintContext)_localctx).refTable = identifier(); - State = 671; + State = 675; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 660; + State = 664; Match(LPAREN); - State = 661; + State = 665; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._refColumns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); - State = 666; + State = 670; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 662; + State = 666; Match(COMMA); - State = 663; + State = 667; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._refColumns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); } } - State = 668; + State = 672; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 669; + State = 673; Match(RPAREN); } } - State = 676; + State = 680; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==ON) { { { - State = 673; + State = 677; foreignKeyAction(); } } - State = 678; + State = 682; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -3747,25 +3758,25 @@ public TableConstraintContext tableConstraint() { _localctx = new CheckTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 681; + State = 685; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 679; + State = 683; Match(CONSTRAINT); - State = 680; + State = 684; ((CheckTableConstraintContext)_localctx).name = identifier(); } } - State = 683; + State = 687; Match(CHECK); - State = 684; + State = 688; Match(LPAREN); - State = 685; + State = 689; checkBody(); - State = 686; + State = 690; Match(RPAREN); } break; @@ -3818,12 +3829,12 @@ public CheckBodyContext checkBody() { try { EnterOuterAlt(_localctx, 1); { - State = 697; + State = 701; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - while ((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & -1L) != 0) || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 67108607L) != 0)) { + while ((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & -1L) != 0) || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 536869887L) != 0)) { { - State = 695; + State = 699; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: @@ -3932,6 +3943,8 @@ public CheckBodyContext checkBody() { case DESC: case TRUE: case FALSE: + case YES: + case OFF: case NULL: case RANGE: case GROUPS: @@ -3962,6 +3975,7 @@ public CheckBodyContext checkBody() { case GT: case COMMA: case DOT: + case BANG: case SEMI: case SYSVAR: case PARAM: @@ -3978,7 +3992,7 @@ public CheckBodyContext checkBody() { case LINE_COMMENT: case BLOCK_COMMENT: { - State = 690; + State = 694; _la = TokenStream.LA(1); if ( _la <= 0 || (_la==LPAREN || _la==RPAREN) ) { ErrorHandler.RecoverInline(this); @@ -3991,11 +4005,11 @@ public CheckBodyContext checkBody() { break; case LPAREN: { - State = 691; + State = 695; Match(LPAREN); - State = 692; + State = 696; checkBody(); - State = 693; + State = 697; Match(RPAREN); } break; @@ -4003,7 +4017,7 @@ public CheckBodyContext checkBody() { throw new NoViableAltException(this); } } - State = 699; + State = 703; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4066,18 +4080,18 @@ public ForeignKeyActionContext foreignKeyAction() { ForeignKeyActionContext _localctx = new ForeignKeyActionContext(Context, State); EnterRule(_localctx, 60, RULE_foreignKeyAction); try { - State = 706; + State = 710; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,76,Context) ) { case 1: _localctx = new OnUpdateActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 700; + State = 704; Match(ON); - State = 701; + State = 705; Match(UPDATE); - State = 702; + State = 706; referentialAction(); } break; @@ -4085,11 +4099,11 @@ public ForeignKeyActionContext foreignKeyAction() { _localctx = new OnDeleteActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 703; + State = 707; Match(ON); - State = 704; + State = 708; Match(DELETE); - State = 705; + State = 709; referentialAction(); } break; @@ -4177,14 +4191,14 @@ public ReferentialActionContext referentialAction() { ReferentialActionContext _localctx = new ReferentialActionContext(Context, State); EnterRule(_localctx, 62, RULE_referentialAction); try { - State = 716; + State = 720; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,77,Context) ) { case 1: _localctx = new CascadeActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 708; + State = 712; Match(CASCADE); } break; @@ -4192,9 +4206,9 @@ public ReferentialActionContext referentialAction() { _localctx = new NoActionActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 709; + State = 713; Match(NO); - State = 710; + State = 714; Match(ACTION); } break; @@ -4202,7 +4216,7 @@ public ReferentialActionContext referentialAction() { _localctx = new RestrictActionContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 711; + State = 715; Match(RESTRICT); } break; @@ -4210,9 +4224,9 @@ public ReferentialActionContext referentialAction() { _localctx = new SetNullActionContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 712; + State = 716; Match(SET); - State = 713; + State = 717; Match(NULL); } break; @@ -4220,9 +4234,9 @@ public ReferentialActionContext referentialAction() { _localctx = new SetDefaultActionContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 714; + State = 718; Match(SET); - State = 715; + State = 719; Match(DEFAULT); } break; @@ -4290,73 +4304,73 @@ public InsertStatementContext insertStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 718; + State = 722; Match(INSERT); - State = 719; + State = 723; Match(INTO); - State = 720; + State = 724; _localctx.table = identifier(); - State = 748; + State = 752; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: case VALUES: case LPAREN: { - State = 732; + State = 736; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,79,Context) ) { case 1: { - State = 721; + State = 725; Match(LPAREN); - State = 722; + State = 726; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); - State = 727; + State = 731; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 723; + State = 727; Match(COMMA); - State = 724; + State = 728; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); } } - State = 729; + State = 733; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 730; + State = 734; Match(RPAREN); } break; } - State = 744; + State = 748; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,81,Context) ) { case 1: { - State = 734; + State = 738; Match(VALUES); - State = 735; + State = 739; rowValues(); - State = 740; + State = 744; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 736; + State = 740; Match(COMMA); - State = 737; + State = 741; rowValues(); } } - State = 742; + State = 746; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4364,7 +4378,7 @@ public InsertStatementContext insertStatement() { break; case 2: { - State = 743; + State = 747; _localctx.source = queryExpression(); } break; @@ -4373,9 +4387,9 @@ public InsertStatementContext insertStatement() { break; case DEFAULT: { - State = 746; + State = 750; Match(DEFAULT); - State = 747; + State = 751; Match(VALUES); } break; @@ -4429,27 +4443,27 @@ public RowValuesContext rowValues() { try { EnterOuterAlt(_localctx, 1); { - State = 750; + State = 754; Match(LPAREN); - State = 751; + State = 755; rowValue(); - State = 756; + State = 760; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 752; + State = 756; Match(COMMA); - State = 753; + State = 757; rowValue(); } } - State = 758; + State = 762; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 759; + State = 763; Match(RPAREN); } } @@ -4487,13 +4501,13 @@ public RowValueContext rowValue() { RowValueContext _localctx = new RowValueContext(Context, State); EnterRule(_localctx, 68, RULE_rowValue); try { - State = 763; + State = 767; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case DEFAULT: EnterOuterAlt(_localctx, 1); { - State = 761; + State = 765; Match(DEFAULT); } break; @@ -4501,13 +4515,17 @@ public RowValueContext rowValue() { case BNOT: case LEFT: case RIGHT: + case ON: case EXISTS: case PARTITION: case CASE: case FIRST: + case NO: case ASC: case TRUE: case FALSE: + case YES: + case OFF: case NULL: case RANGE: case GROUPS: @@ -4539,7 +4557,7 @@ public RowValueContext rowValue() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 762; + State = 766; expression(0); } break; @@ -4598,40 +4616,40 @@ public QueryExpressionContext queryExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 765; + State = 769; queryTerm(); - State = 771; + State = 775; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 117093590311632896L) != 0)) { { { - State = 766; + State = 770; setOperator(); - State = 767; + State = 771; queryTerm(); } } - State = 773; + State = 777; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 775; + State = 779; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ORDER) { { - State = 774; + State = 778; orderByClause(); } } - State = 778; + State = 782; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OFFSET || _la==FETCH) { { - State = 777; + State = 781; offsetFetchClause(); } } @@ -4714,14 +4732,14 @@ public QueryTermContext queryTerm() { EnterRule(_localctx, 72, RULE_queryTerm); int _la; try { - State = 794; + State = 798; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: _localctx = new SelectTermContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 780; + State = 784; querySpecification(); } break; @@ -4729,11 +4747,11 @@ public QueryTermContext queryTerm() { _localctx = new ParenTermContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 781; + State = 785; Match(LPAREN); - State = 782; + State = 786; queryExpression(); - State = 783; + State = 787; Match(RPAREN); } break; @@ -4741,23 +4759,23 @@ public QueryTermContext queryTerm() { _localctx = new ValuesTermContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 785; + State = 789; Match(VALUES); - State = 786; + State = 790; rowValues(); - State = 791; + State = 795; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 787; + State = 791; Match(COMMA); - State = 788; + State = 792; rowValues(); } } - State = 793; + State = 797; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4802,20 +4820,20 @@ public SetOperatorContext setOperator() { EnterRule(_localctx, 74, RULE_setOperator); int _la; try { - State = 802; + State = 806; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case UNION: EnterOuterAlt(_localctx, 1); { - State = 796; + State = 800; Match(UNION); - State = 798; + State = 802; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ALL) { { - State = 797; + State = 801; Match(ALL); } } @@ -4825,14 +4843,14 @@ public SetOperatorContext setOperator() { case INTERSECT: EnterOuterAlt(_localctx, 2); { - State = 800; + State = 804; Match(INTERSECT); } break; case EXCEPT: EnterOuterAlt(_localctx, 3); { - State = 801; + State = 805; Match(EXCEPT); } break; @@ -4901,78 +4919,78 @@ public QuerySpecificationContext querySpecification() { try { EnterOuterAlt(_localctx, 1); { - State = 804; + State = 808; Match(SELECT); - State = 806; + State = 810; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 18014450049089536L) != 0)) { { - State = 805; + State = 809; _localctx.predicate = selectPredicate(); } } - State = 809; + State = 813; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TOP) { { - State = 808; + State = 812; topClause(); } } - State = 811; + State = 815; selectList(); - State = 814; + State = 818; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==INTO) { { - State = 812; + State = 816; Match(INTO); - State = 813; + State = 817; _localctx.into = identifier(); } } - State = 817; + State = 821; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FROM) { { - State = 816; + State = 820; fromClause(); } } - State = 820; + State = 824; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 819; + State = 823; whereClause(); } } - State = 823; + State = 827; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==GROUP) { { - State = 822; + State = 826; groupByClause(); } } - State = 826; + State = 830; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==HAVING) { { - State = 825; + State = 829; havingClause(); } } @@ -5015,7 +5033,7 @@ public SelectPredicateContext selectPredicate() { try { EnterOuterAlt(_localctx, 1); { - State = 828; + State = 832; _la = TokenStream.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 18014450049089536L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -5071,25 +5089,25 @@ public GroupByClauseContext groupByClause() { try { EnterOuterAlt(_localctx, 1); { - State = 830; + State = 834; Match(GROUP); - State = 831; + State = 835; Match(BY); - State = 832; + State = 836; expression(0); - State = 837; + State = 841; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 833; + State = 837; Match(COMMA); - State = 834; + State = 838; expression(0); } } - State = 839; + State = 843; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5131,9 +5149,9 @@ public HavingClauseContext havingClause() { try { EnterOuterAlt(_localctx, 1); { - State = 840; + State = 844; Match(HAVING); - State = 841; + State = 845; expression(0); } } @@ -5188,18 +5206,18 @@ public TopClauseContext topClause() { int _alt; EnterOuterAlt(_localctx, 1); { - State = 843; + State = 847; Match(TOP); - State = 844; + State = 848; topOperand(); - State = 849; + State = 853; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,100,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 845; + State = 849; _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { ErrorHandler.RecoverInline(this); @@ -5208,21 +5226,21 @@ public TopClauseContext topClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 846; + State = 850; topOperand(); } } } - State = 851; + State = 855; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,100,Context); } - State = 853; + State = 857; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PERCENT) { { - State = 852; + State = 856; _localctx.percent = Match(PERCENT); } } @@ -5266,31 +5284,31 @@ public TopOperandContext topOperand() { TopOperandContext _localctx = new TopOperandContext(Context, State); EnterRule(_localctx, 86, RULE_topOperand); try { - State = 861; + State = 865; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: EnterOuterAlt(_localctx, 1); { - State = 855; + State = 859; Match(INTEGER_LITERAL); } break; case PARAM: EnterOuterAlt(_localctx, 2); { - State = 856; + State = 860; Match(PARAM); } break; case LPAREN: EnterOuterAlt(_localctx, 3); { - State = 857; + State = 861; Match(LPAREN); - State = 858; + State = 862; expression(0); - State = 859; + State = 863; Match(RPAREN); } break; @@ -5348,26 +5366,26 @@ public OffsetFetchClauseContext offsetFetchClause() { EnterRule(_localctx, 88, RULE_offsetFetchClause); int _la; try { - State = 880; + State = 884; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case OFFSET: EnterOuterAlt(_localctx, 1); { - State = 863; + State = 867; Match(OFFSET); - State = 864; + State = 868; _localctx.offset = expression(0); - State = 865; + State = 869; rowKeyword(); - State = 872; + State = 876; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FETCH) { { - State = 866; + State = 870; Match(FETCH); - State = 867; + State = 871; _la = TokenStream.LA(1); if ( !(_la==NEXT || _la==FIRST) ) { ErrorHandler.RecoverInline(this); @@ -5376,11 +5394,11 @@ public OffsetFetchClauseContext offsetFetchClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 868; + State = 872; _localctx.limit = expression(0); - State = 869; + State = 873; rowKeyword(); - State = 870; + State = 874; Match(ONLY); } } @@ -5390,9 +5408,9 @@ public OffsetFetchClauseContext offsetFetchClause() { case FETCH: EnterOuterAlt(_localctx, 2); { - State = 874; + State = 878; Match(FETCH); - State = 875; + State = 879; _la = TokenStream.LA(1); if ( !(_la==NEXT || _la==FIRST) ) { ErrorHandler.RecoverInline(this); @@ -5401,11 +5419,11 @@ public OffsetFetchClauseContext offsetFetchClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 876; + State = 880; _localctx.limit = expression(0); - State = 877; + State = 881; rowKeyword(); - State = 878; + State = 882; Match(ONLY); } break; @@ -5448,7 +5466,7 @@ public RowKeywordContext rowKeyword() { try { EnterOuterAlt(_localctx, 1); { - State = 882; + State = 886; _la = TokenStream.LA(1); if ( !(_la==ROWS || _la==ROW) ) { ErrorHandler.RecoverInline(this); @@ -5501,13 +5519,13 @@ public SelectListContext selectList() { EnterRule(_localctx, 92, RULE_selectList); int _la; try { - State = 893; + State = 897; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: EnterOuterAlt(_localctx, 1); { - State = 884; + State = 888; Match(STAR); } break; @@ -5515,13 +5533,17 @@ public SelectListContext selectList() { case BNOT: case LEFT: case RIGHT: + case ON: case EXISTS: case PARTITION: case CASE: case FIRST: + case NO: case ASC: case TRUE: case FALSE: + case YES: + case OFF: case NULL: case RANGE: case GROUPS: @@ -5553,21 +5575,21 @@ public SelectListContext selectList() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 885; + State = 889; selectItem(); - State = 890; + State = 894; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 886; + State = 890; Match(COMMA); - State = 887; + State = 891; selectItem(); } } - State = 892; + State = 896; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5639,18 +5661,18 @@ public SelectItemContext selectItem() { EnterRule(_localctx, 94, RULE_selectItem); int _la; try { - State = 906; + State = 910; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,109,Context) ) { case 1: _localctx = new QualifiedStarSelectItemContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 895; + State = 899; ((QualifiedStarSelectItemContext)_localctx).qualifier = identifier(); - State = 896; + State = 900; Match(DOT); - State = 897; + State = 901; Match(STAR); } break; @@ -5658,24 +5680,24 @@ public SelectItemContext selectItem() { _localctx = new ExpressionSelectItemContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 899; + State = 903; expression(0); - State = 904; + State = 908; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) { + if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 901; + State = 905; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 900; + State = 904; Match(AS); } } - State = 903; + State = 907; ((ExpressionSelectItemContext)_localctx).alias = identifier(); } } @@ -5728,23 +5750,23 @@ public FromClauseContext fromClause() { try { EnterOuterAlt(_localctx, 1); { - State = 908; + State = 912; Match(FROM); - State = 909; + State = 913; tableSource(); - State = 914; + State = 918; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 910; + State = 914; Match(COMMA); - State = 911; + State = 915; tableSource(); } } - State = 916; + State = 920; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5792,19 +5814,19 @@ public TableSourceContext tableSource() { try { EnterOuterAlt(_localctx, 1); { - State = 917; - tablePrimary(); State = 921; + tablePrimary(); + State = 925; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 137455468544L) != 0)) { { { - State = 918; + State = 922; joinClause(); } } - State = 923; + State = 927; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5891,31 +5913,31 @@ public TablePrimaryContext tablePrimary() { EnterRule(_localctx, 100, RULE_tablePrimary); int _la; try { - State = 944; + State = 948; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,116,Context) ) { case 1: _localctx = new NamedTablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 924; + State = 928; ((NamedTablePrimaryContext)_localctx).table = identifier(); - State = 929; + State = 933; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) { + if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 926; + State = 930; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 925; + State = 929; Match(AS); } } - State = 928; + State = 932; ((NamedTablePrimaryContext)_localctx).alias = identifier(); } } @@ -5926,28 +5948,28 @@ public TablePrimaryContext tablePrimary() { _localctx = new SubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 931; + State = 935; Match(LPAREN); - State = 932; + State = 936; queryExpression(); - State = 933; + State = 937; Match(RPAREN); - State = 938; + State = 942; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) { + if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 935; + State = 939; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 934; + State = 938; Match(AS); } } - State = 937; + State = 941; ((SubqueryPrimaryContext)_localctx).alias = identifier(); } } @@ -5958,11 +5980,11 @@ public TablePrimaryContext tablePrimary() { _localctx = new ParenJoinPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 940; + State = 944; Match(LPAREN); - State = 941; + State = 945; tableSource(); - State = 942; + State = 946; Match(RPAREN); } break; @@ -6059,22 +6081,22 @@ public JoinClauseContext joinClause() { JoinClauseContext _localctx = new JoinClauseContext(Context, State); EnterRule(_localctx, 102, RULE_joinClause); try { - State = 961; + State = 965; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,117,Context) ) { case 1: _localctx = new ConditionalJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 946; + State = 950; joinType(); - State = 947; + State = 951; Match(JOIN); - State = 948; + State = 952; tablePrimary(); - State = 949; + State = 953; Match(ON); - State = 950; + State = 954; expression(0); } break; @@ -6082,11 +6104,11 @@ public JoinClauseContext joinClause() { _localctx = new CrossJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 952; + State = 956; Match(CROSS); - State = 953; + State = 957; Match(JOIN); - State = 954; + State = 958; tablePrimary(); } break; @@ -6094,11 +6116,11 @@ public JoinClauseContext joinClause() { _localctx = new CrossApplyContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 955; + State = 959; Match(CROSS); - State = 956; + State = 960; Match(APPLY); - State = 957; + State = 961; tablePrimary(); } break; @@ -6106,11 +6128,11 @@ public JoinClauseContext joinClause() { _localctx = new OuterApplyContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 958; + State = 962; Match(OUTER); - State = 959; + State = 963; Match(APPLY); - State = 960; + State = 964; tablePrimary(); } break; @@ -6189,7 +6211,7 @@ public JoinTypeContext joinType() { EnterRule(_localctx, 104, RULE_joinType); int _la; try { - State = 978; + State = 982; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INNER: @@ -6197,12 +6219,12 @@ public JoinTypeContext joinType() { _localctx = new InnerJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 964; + State = 968; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==INNER) { { - State = 963; + State = 967; Match(INNER); } } @@ -6213,14 +6235,14 @@ public JoinTypeContext joinType() { _localctx = new LeftJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 966; + State = 970; Match(LEFT); - State = 968; + State = 972; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 967; + State = 971; Match(OUTER); } } @@ -6231,14 +6253,14 @@ public JoinTypeContext joinType() { _localctx = new RightJoinContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 970; + State = 974; Match(RIGHT); - State = 972; + State = 976; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 971; + State = 975; Match(OUTER); } } @@ -6249,14 +6271,14 @@ public JoinTypeContext joinType() { _localctx = new FullJoinContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 974; + State = 978; Match(FULL); - State = 976; + State = 980; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 975; + State = 979; Match(OUTER); } } @@ -6303,9 +6325,9 @@ public WhereClauseContext whereClause() { try { EnterOuterAlt(_localctx, 1); { - State = 980; + State = 984; Match(WHERE); - State = 981; + State = 985; expression(0); } } @@ -6354,25 +6376,25 @@ public OrderByClauseContext orderByClause() { try { EnterOuterAlt(_localctx, 1); { - State = 983; + State = 987; Match(ORDER); - State = 984; + State = 988; Match(BY); - State = 985; + State = 989; orderByItem(); - State = 990; + State = 994; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 986; + State = 990; Match(COMMA); - State = 987; + State = 991; orderByItem(); } } - State = 992; + State = 996; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -6417,14 +6439,14 @@ public OrderByItemContext orderByItem() { try { EnterOuterAlt(_localctx, 1); { - State = 993; + State = 997; expression(0); - State = 995; + State = 999; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ASC || _la==DESC) { { - State = 994; + State = 998; _localctx.dir = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -6908,7 +6930,7 @@ private ExpressionContext expression(int _p) { int _alt; EnterOuterAlt(_localctx, 1); { - State = 1003; + State = 1007; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case PLUS: @@ -6918,7 +6940,7 @@ private ExpressionContext expression(int _p) { Context = _localctx; _prevctx = _localctx; - State = 998; + State = 1002; ((NegateExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -6928,7 +6950,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 999; + State = 1003; expression(21); } break; @@ -6938,7 +6960,7 @@ private ExpressionContext expression(int _p) { _localctx = new NotExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 1000; + State = 1004; ((NotExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==NOT || _la==BNOT) ) { @@ -6948,19 +6970,23 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1001; + State = 1005; expression(7); } break; case LEFT: case RIGHT: + case ON: case EXISTS: case PARTITION: case CASE: case FIRST: + case NO: case ASC: case TRUE: case FALSE: + case YES: + case OFF: case NULL: case RANGE: case GROUPS: @@ -6992,7 +7018,7 @@ private ExpressionContext expression(int _p) { _localctx = new PrimaryExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 1002; + State = 1006; primary(); } break; @@ -7000,7 +7026,7 @@ private ExpressionContext expression(int _p) { throw new NoViableAltException(this); } Context.Stop = TokenStream.LT(-1); - State = 1103; + State = 1107; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,135,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { @@ -7009,7 +7035,7 @@ private ExpressionContext expression(int _p) { TriggerExitRuleEvent(); _prevctx = _localctx; { - State = 1101; + State = 1105; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,134,Context) ) { case 1: @@ -7017,11 +7043,11 @@ private ExpressionContext expression(int _p) { _localctx = new PowExprContext(new ExpressionContext(_parentctx, _parentState)); ((PowExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1005; + State = 1009; if (!(Precpred(Context, 22))) throw new FailedPredicateException(this, "Precpred(Context, 22)"); - State = 1006; + State = 1010; Match(CARET); - State = 1007; + State = 1011; ((PowExprContext)_localctx).right = expression(23); } break; @@ -7030,9 +7056,9 @@ private ExpressionContext expression(int _p) { _localctx = new MulDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((MulDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1008; + State = 1012; if (!(Precpred(Context, 20))) throw new FailedPredicateException(this, "Precpred(Context, 20)"); - State = 1009; + State = 1013; ((MulDivExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==STAR || _la==SLASH) ) { @@ -7042,7 +7068,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1010; + State = 1014; ((MulDivExprContext)_localctx).right = expression(21); } break; @@ -7051,11 +7077,11 @@ private ExpressionContext expression(int _p) { _localctx = new IntDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((IntDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1011; + State = 1015; if (!(Precpred(Context, 19))) throw new FailedPredicateException(this, "Precpred(Context, 19)"); - State = 1012; + State = 1016; ((IntDivExprContext)_localctx).op = Match(BACKSLASH); - State = 1013; + State = 1017; ((IntDivExprContext)_localctx).right = expression(20); } break; @@ -7064,11 +7090,11 @@ private ExpressionContext expression(int _p) { _localctx = new ModExprContext(new ExpressionContext(_parentctx, _parentState)); ((ModExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1014; + State = 1018; if (!(Precpred(Context, 18))) throw new FailedPredicateException(this, "Precpred(Context, 18)"); - State = 1015; + State = 1019; ((ModExprContext)_localctx).op = Match(MOD); - State = 1016; + State = 1020; ((ModExprContext)_localctx).right = expression(19); } break; @@ -7077,9 +7103,9 @@ private ExpressionContext expression(int _p) { _localctx = new AddSubExprContext(new ExpressionContext(_parentctx, _parentState)); ((AddSubExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1017; + State = 1021; if (!(Precpred(Context, 17))) throw new FailedPredicateException(this, "Precpred(Context, 17)"); - State = 1018; + State = 1022; ((AddSubExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -7089,7 +7115,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1019; + State = 1023; ((AddSubExprContext)_localctx).right = expression(18); } break; @@ -7098,11 +7124,11 @@ private ExpressionContext expression(int _p) { _localctx = new ConcatExprContext(new ExpressionContext(_parentctx, _parentState)); ((ConcatExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1020; + State = 1024; if (!(Precpred(Context, 16))) throw new FailedPredicateException(this, "Precpred(Context, 16)"); - State = 1021; + State = 1025; ((ConcatExprContext)_localctx).op = Match(AMP); - State = 1022; + State = 1026; ((ConcatExprContext)_localctx).right = expression(17); } break; @@ -7111,19 +7137,19 @@ private ExpressionContext expression(int _p) { _localctx = new ComparisonExprContext(new ExpressionContext(_parentctx, _parentState)); ((ComparisonExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1023; + State = 1027; if (!(Precpred(Context, 15))) throw new FailedPredicateException(this, "Precpred(Context, 15)"); - State = 1024; + State = 1028; ((ComparisonExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); - if ( !(((((_la - 129)) & ~0x3f) == 0 && ((1L << (_la - 129)) & 63L) != 0)) ) { + if ( !(((((_la - 131)) & ~0x3f) == 0 && ((1L << (_la - 131)) & 63L) != 0)) ) { ((ComparisonExprContext)_localctx).op = ErrorHandler.RecoverInline(this); } else { ErrorHandler.ReportMatch(this); Consume(); } - State = 1025; + State = 1029; ((ComparisonExprContext)_localctx).right = expression(16); } break; @@ -7132,25 +7158,25 @@ private ExpressionContext expression(int _p) { _localctx = new BetweenExprContext(new ExpressionContext(_parentctx, _parentState)); ((BetweenExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1026; + State = 1030; if (!(Precpred(Context, 14))) throw new FailedPredicateException(this, "Precpred(Context, 14)"); - State = 1028; + State = 1032; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1027; + State = 1031; ((BetweenExprContext)_localctx).not = Match(NOT); } } - State = 1030; + State = 1034; Match(BETWEEN); - State = 1031; + State = 1035; ((BetweenExprContext)_localctx).lo = expression(0); - State = 1032; + State = 1036; Match(AND); - State = 1033; + State = 1037; ((BetweenExprContext)_localctx).hi = expression(15); } break; @@ -7159,21 +7185,21 @@ private ExpressionContext expression(int _p) { _localctx = new LikeExprContext(new ExpressionContext(_parentctx, _parentState)); ((LikeExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1035; + State = 1039; if (!(Precpred(Context, 13))) throw new FailedPredicateException(this, "Precpred(Context, 13)"); - State = 1037; + State = 1041; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1036; + State = 1040; ((LikeExprContext)_localctx).not = Match(NOT); } } - State = 1039; + State = 1043; Match(LIKE); - State = 1040; + State = 1044; ((LikeExprContext)_localctx).right = expression(14); } break; @@ -7182,25 +7208,25 @@ private ExpressionContext expression(int _p) { _localctx = new IsDistinctFromExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsDistinctFromExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1041; + State = 1045; if (!(Precpred(Context, 8))) throw new FailedPredicateException(this, "Precpred(Context, 8)"); - State = 1042; + State = 1046; Match(IS); - State = 1044; + State = 1048; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1043; + State = 1047; ((IsDistinctFromExprContext)_localctx).not = Match(NOT); } } - State = 1046; + State = 1050; Match(DISTINCT); - State = 1047; + State = 1051; Match(FROM); - State = 1048; + State = 1052; ((IsDistinctFromExprContext)_localctx).right = expression(9); } break; @@ -7209,9 +7235,9 @@ private ExpressionContext expression(int _p) { _localctx = new AndExprContext(new ExpressionContext(_parentctx, _parentState)); ((AndExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1049; + State = 1053; if (!(Precpred(Context, 6))) throw new FailedPredicateException(this, "Precpred(Context, 6)"); - State = 1050; + State = 1054; ((AndExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==AND || _la==BAND) ) { @@ -7221,7 +7247,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1051; + State = 1055; ((AndExprContext)_localctx).right = expression(7); } break; @@ -7230,9 +7256,9 @@ private ExpressionContext expression(int _p) { _localctx = new OrExprContext(new ExpressionContext(_parentctx, _parentState)); ((OrExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1052; + State = 1056; if (!(Precpred(Context, 5))) throw new FailedPredicateException(this, "Precpred(Context, 5)"); - State = 1053; + State = 1057; ((OrExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==OR || _la==BOR) ) { @@ -7242,7 +7268,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1054; + State = 1058; ((OrExprContext)_localctx).right = expression(6); } break; @@ -7251,9 +7277,9 @@ private ExpressionContext expression(int _p) { _localctx = new XorExprContext(new ExpressionContext(_parentctx, _parentState)); ((XorExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1055; + State = 1059; if (!(Precpred(Context, 4))) throw new FailedPredicateException(this, "Precpred(Context, 4)"); - State = 1056; + State = 1060; ((XorExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==XOR || _la==BXOR) ) { @@ -7263,7 +7289,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1057; + State = 1061; ((XorExprContext)_localctx).right = expression(5); } break; @@ -7272,11 +7298,11 @@ private ExpressionContext expression(int _p) { _localctx = new EqvExprContext(new ExpressionContext(_parentctx, _parentState)); ((EqvExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1058; + State = 1062; if (!(Precpred(Context, 3))) throw new FailedPredicateException(this, "Precpred(Context, 3)"); - State = 1059; + State = 1063; ((EqvExprContext)_localctx).op = Match(EQV); - State = 1060; + State = 1064; ((EqvExprContext)_localctx).right = expression(4); } break; @@ -7285,11 +7311,11 @@ private ExpressionContext expression(int _p) { _localctx = new ImpExprContext(new ExpressionContext(_parentctx, _parentState)); ((ImpExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1061; + State = 1065; if (!(Precpred(Context, 2))) throw new FailedPredicateException(this, "Precpred(Context, 2)"); - State = 1062; + State = 1066; ((ImpExprContext)_localctx).op = Match(IMP); - State = 1063; + State = 1067; ((ImpExprContext)_localctx).right = expression(3); } break; @@ -7298,25 +7324,25 @@ private ExpressionContext expression(int _p) { _localctx = new InSubqueryExprContext(new ExpressionContext(_parentctx, _parentState)); ((InSubqueryExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1064; + State = 1068; if (!(Precpred(Context, 12))) throw new FailedPredicateException(this, "Precpred(Context, 12)"); - State = 1066; + State = 1070; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1065; + State = 1069; ((InSubqueryExprContext)_localctx).not = Match(NOT); } } - State = 1068; + State = 1072; Match(IN); - State = 1069; + State = 1073; Match(LPAREN); - State = 1070; + State = 1074; ((InSubqueryExprContext)_localctx).sub = queryExpression(); - State = 1071; + State = 1075; Match(RPAREN); } break; @@ -7325,43 +7351,43 @@ private ExpressionContext expression(int _p) { _localctx = new InExprContext(new ExpressionContext(_parentctx, _parentState)); ((InExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1073; + State = 1077; if (!(Precpred(Context, 11))) throw new FailedPredicateException(this, "Precpred(Context, 11)"); - State = 1075; + State = 1079; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1074; + State = 1078; ((InExprContext)_localctx).not = Match(NOT); } } - State = 1077; + State = 1081; Match(IN); - State = 1078; + State = 1082; Match(LPAREN); - State = 1079; + State = 1083; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); - State = 1084; + State = 1088; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1080; + State = 1084; Match(COMMA); - State = 1081; + State = 1085; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); } } - State = 1086; + State = 1090; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 1087; + State = 1091; Match(RPAREN); } break; @@ -7370,21 +7396,21 @@ private ExpressionContext expression(int _p) { _localctx = new IsNullExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsNullExprContext)_localctx).operand = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1089; + State = 1093; if (!(Precpred(Context, 10))) throw new FailedPredicateException(this, "Precpred(Context, 10)"); - State = 1090; + State = 1094; Match(IS); - State = 1092; + State = 1096; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1091; + State = 1095; ((IsNullExprContext)_localctx).not = Match(NOT); } } - State = 1094; + State = 1098; Match(NULL); } break; @@ -7393,21 +7419,21 @@ private ExpressionContext expression(int _p) { _localctx = new IsTruthExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsTruthExprContext)_localctx).operand = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1095; + State = 1099; if (!(Precpred(Context, 9))) throw new FailedPredicateException(this, "Precpred(Context, 9)"); - State = 1096; + State = 1100; Match(IS); - State = 1098; + State = 1102; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1097; + State = 1101; ((IsTruthExprContext)_localctx).not = Match(NOT); } } - State = 1100; + State = 1104; ((IsTruthExprContext)_localctx).truth = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==TRUE || _la==FALSE) ) { @@ -7422,7 +7448,7 @@ private ExpressionContext expression(int _p) { } } } - State = 1105; + State = 1109; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,135,Context); } @@ -7568,14 +7594,14 @@ public PrimaryContext primary() { PrimaryContext _localctx = new PrimaryContext(Context, State); EnterRule(_localctx, 114, RULE_primary); try { - State = 1125; + State = 1129; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,136,Context) ) { case 1: _localctx = new LiteralPrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1106; + State = 1110; literal(); } break; @@ -7583,7 +7609,7 @@ public PrimaryContext primary() { _localctx = new CasePrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1107; + State = 1111; caseExpression(); } break; @@ -7591,7 +7617,7 @@ public PrimaryContext primary() { _localctx = new FunctionCallPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1108; + State = 1112; functionCall(); } break; @@ -7599,7 +7625,7 @@ public PrimaryContext primary() { _localctx = new ColumnPrimaryContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 1109; + State = 1113; columnRef(); } break; @@ -7607,7 +7633,7 @@ public PrimaryContext primary() { _localctx = new ParamPrimaryContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 1110; + State = 1114; Match(PARAM); } break; @@ -7615,7 +7641,7 @@ public PrimaryContext primary() { _localctx = new SystemVariablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 1111; + State = 1115; Match(SYSVAR); } break; @@ -7623,13 +7649,13 @@ public PrimaryContext primary() { _localctx = new ExistsPrimaryContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 1112; + State = 1116; Match(EXISTS); - State = 1113; + State = 1117; Match(LPAREN); - State = 1114; + State = 1118; queryExpression(); - State = 1115; + State = 1119; Match(RPAREN); } break; @@ -7637,11 +7663,11 @@ public PrimaryContext primary() { _localctx = new ScalarSubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 1117; + State = 1121; Match(LPAREN); - State = 1118; + State = 1122; queryExpression(); - State = 1119; + State = 1123; Match(RPAREN); } break; @@ -7649,11 +7675,11 @@ public PrimaryContext primary() { _localctx = new ParenPrimaryContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 1121; + State = 1125; Match(LPAREN); - State = 1122; + State = 1126; expression(0); - State = 1123; + State = 1127; Match(RPAREN); } break; @@ -7709,45 +7735,45 @@ public CaseExpressionContext caseExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 1127; + State = 1131; Match(CASE); - State = 1129; + State = 1133; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775660683520L) != 0) || ((((_la - 103)) & ~0x3f) == 0 && ((1L << (_la - 103)) & 281341858414589L) != 0)) { + if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775694237952L) != 0) || ((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & -283699838490247167L) != 0) || ((((_la - 149)) & ~0x3f) == 0 && ((1L << (_la - 149)) & 31L) != 0)) { { - State = 1128; + State = 1132; _localctx.operand = expression(0); } } - State = 1132; + State = 1136; ErrorHandler.Sync(this); _la = TokenStream.LA(1); do { { { - State = 1131; + State = 1135; caseWhen(); } } - State = 1134; + State = 1138; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } while ( _la==WHEN ); - State = 1138; + State = 1142; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ELSE) { { - State = 1136; + State = 1140; Match(ELSE); - State = 1137; + State = 1141; _localctx.elseResult = expression(0); } } - State = 1140; + State = 1144; Match(END); } } @@ -7793,13 +7819,13 @@ public CaseWhenContext caseWhen() { try { EnterOuterAlt(_localctx, 1); { - State = 1142; + State = 1146; Match(WHEN); - State = 1143; + State = 1147; _localctx.condition = expression(0); - State = 1144; + State = 1148; Match(THEN); - State = 1145; + State = 1149; _localctx.result = expression(0); } } @@ -7872,16 +7898,16 @@ public FunctionCallContext functionCall() { try { EnterOuterAlt(_localctx, 1); { - State = 1147; + State = 1151; _localctx.name = functionName(); - State = 1148; + State = 1152; Match(LPAREN); - State = 1161; + State = 1165; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: { - State = 1149; + State = 1153; _localctx.star = Match(STAR); } break; @@ -7889,14 +7915,18 @@ public FunctionCallContext functionCall() { case BNOT: case LEFT: case RIGHT: + case ON: case EXISTS: case DISTINCT: case PARTITION: case CASE: case FIRST: + case NO: case ASC: case TRUE: case FALSE: + case YES: + case OFF: case NULL: case RANGE: case GROUPS: @@ -7928,31 +7958,31 @@ public FunctionCallContext functionCall() { case IDENTIFIER: { { - State = 1151; + State = 1155; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==DISTINCT) { { - State = 1150; + State = 1154; _localctx.distinct = Match(DISTINCT); } } - State = 1153; + State = 1157; expression(0); - State = 1158; + State = 1162; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1154; + State = 1158; Match(COMMA); - State = 1155; + State = 1159; expression(0); } } - State = 1160; + State = 1164; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -7964,56 +7994,56 @@ public FunctionCallContext functionCall() { default: break; } - State = 1163; + State = 1167; Match(RPAREN); - State = 1165; + State = 1169; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,143,Context) ) { case 1: { - State = 1164; + State = 1168; withinGroup(); } break; } - State = 1168; + State = 1172; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,144,Context) ) { case 1: { - State = 1167; + State = 1171; filterClause(); } break; } - State = 1178; + State = 1182; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,147,Context) ) { case 1: { - State = 1171; + State = 1175; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FROM) { { - State = 1170; + State = 1174; nthRowFrom(); } } - State = 1174; + State = 1178; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==IGNORE || _la==RESPECT) { { - State = 1173; + State = 1177; nullTreatment(); } } - State = 1176; + State = 1180; Match(OVER); - State = 1177; + State = 1181; windowSpecification(); } break; @@ -8058,9 +8088,11 @@ public FunctionNameContext functionName() { FunctionNameContext _localctx = new FunctionNameContext(Context, State); EnterRule(_localctx, 122, RULE_functionName); try { - State = 1186; + State = 1190; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { + case YES: + case OFF: case RANGE: case GROUPS: case UNBOUNDED: @@ -8080,42 +8112,42 @@ public FunctionNameContext functionName() { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1180; + State = 1184; identifier(); } break; case LEFT: EnterOuterAlt(_localctx, 2); { - State = 1181; + State = 1185; Match(LEFT); } break; case RIGHT: EnterOuterAlt(_localctx, 3); { - State = 1182; + State = 1186; Match(RIGHT); } break; case ASC: EnterOuterAlt(_localctx, 4); { - State = 1183; + State = 1187; Match(ASC); } break; case FIRST: EnterOuterAlt(_localctx, 5); { - State = 1184; + State = 1188; Match(FIRST); } break; case PARTITION: EnterOuterAlt(_localctx, 6); { - State = 1185; + State = 1189; Match(PARTITION); } break; @@ -8135,15 +8167,30 @@ public FunctionNameContext functionName() { } public partial class ColumnRefContext : ParserRuleContext { - public IdentifierContext qualifier; - public IdentifierContext name; - [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext[] identifier() { - return GetRuleContexts(); + public IdentifierContext first; + public IToken _DOT; + public IList _separators = new List(); + public IToken _BANG; + public IToken _tset3255; + public MemberNameContext _memberName; + public IList _rest = new List(); + [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() { + return GetRuleContext(0); } - [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier(int i) { - return GetRuleContext(i); + [System.Diagnostics.DebuggerNonUserCode] public MemberNameContext[] memberName() { + return GetRuleContexts(); + } + [System.Diagnostics.DebuggerNonUserCode] public MemberNameContext memberName(int i) { + return GetRuleContext(i); + } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode[] DOT() { return GetTokens(AccessSqlParser.DOT); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DOT(int i) { + return GetToken(AccessSqlParser.DOT, i); + } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode[] BANG() { return GetTokens(AccessSqlParser.BANG); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BANG(int i) { + return GetToken(AccessSqlParser.BANG, i); } - [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DOT() { return GetToken(AccessSqlParser.DOT, 0); } public ColumnRefContext(ParserRuleContext parent, int invokingState) : base(parent, invokingState) { @@ -8161,23 +8208,222 @@ public override TResult Accept(IParseTreeVisitor visitor) { public ColumnRefContext columnRef() { ColumnRefContext _localctx = new ColumnRefContext(Context, State); EnterRule(_localctx, 124, RULE_columnRef); + int _la; try { + int _alt; EnterOuterAlt(_localctx, 1); { - State = 1191; + State = 1192; + _localctx.first = identifier(); + State = 1197; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,149,Context) ) { - case 1: + _alt = Interpreter.AdaptivePredict(TokenStream,149,Context); + while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { + if ( _alt==1 ) { + { + { + State = 1193; + _localctx._tset3255 = TokenStream.LT(1); + _la = TokenStream.LA(1); + if ( !(_la==DOT || _la==BANG) ) { + _localctx._tset3255 = ErrorHandler.RecoverInline(this); + } + else { + ErrorHandler.ReportMatch(this); + Consume(); + } + _localctx._separators.Add(_localctx._tset3255); + State = 1194; + _localctx._memberName = memberName(); + _localctx._rest.Add(_localctx._memberName); + } + } + } + State = 1199; + ErrorHandler.Sync(this); + _alt = Interpreter.AdaptivePredict(TokenStream,149,Context); + } + } + } + catch (RecognitionException re) { + _localctx.exception = re; + ErrorHandler.ReportError(this, re); + ErrorHandler.Recover(this, re); + } + finally { + ExitRule(); + } + return _localctx; + } + + public partial class MemberNameContext : ParserRuleContext { + [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() { + return GetRuleContext(0); + } + [System.Diagnostics.DebuggerNonUserCode] public ReservedKeywordContext reservedKeyword() { + return GetRuleContext(0); + } + public MemberNameContext(ParserRuleContext parent, int invokingState) + : base(parent, invokingState) + { + } + public override int RuleIndex { get { return RULE_memberName; } } + [System.Diagnostics.DebuggerNonUserCode] + public override TResult Accept(IParseTreeVisitor visitor) { + IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor; + if (typedVisitor != null) return typedVisitor.VisitMemberName(this); + else return visitor.VisitChildren(this); + } + } + + [RuleVersion(0)] + public MemberNameContext memberName() { + MemberNameContext _localctx = new MemberNameContext(Context, State); + EnterRule(_localctx, 126, RULE_memberName); + try { + State = 1202; + ErrorHandler.Sync(this); + switch (TokenStream.LA(1)) { + case YES: + case OFF: + case RANGE: + case GROUPS: + case UNBOUNDED: + case PRECEDING: + case FOLLOWING: + case CURRENT: + case EXCLUDE: + case TIES: + case OTHERS: + case WITHIN: + case LAST: + case RESPECT: + case NULLS: + case FILTER: + case BRACKET_ID: + case BACKTICK_ID: + case IDENTIFIER: + EnterOuterAlt(_localctx, 1); { - State = 1188; - _localctx.qualifier = identifier(); - State = 1189; - Match(DOT); + State = 1200; + identifier(); } break; - } - State = 1193; - _localctx.name = identifier(); + case SELECT: + case FROM: + case WHERE: + case TOP: + case AS: + case AND: + case OR: + case NOT: + case XOR: + case EQV: + case IMP: + case BAND: + case BOR: + case BXOR: + case BNOT: + case LIKE: + case MOD: + case INNER: + case LEFT: + case RIGHT: + case FULL: + case OUTER: + case JOIN: + case IN: + case ON: + case ORDER: + case GROUP: + case IS: + case BY: + case HAVING: + case EXISTS: + case IF: + case THEN: + case DISTINCTROW: + case DISTINCT: + case PERCENT: + case CROSS: + case APPLY: + case OVER: + case PARTITION: + case CASE: + case WHEN: + case ELSE: + case END: + case OFFSET: + case FETCH: + case NEXT: + case FIRST: + case ROWS: + case ROW: + case ONLY: + case BETWEEN: + case UNION: + case ALL: + case INTERSECT: + case EXCEPT: + case CREATE: + case TABLE: + case BEGIN: + case COMMIT: + case ROLLBACK: + case TRANSACTION: + case WORK: + case ALTER: + case RENAME: + case TO: + case ADD: + case DROP: + case COLUMN: + case INSERT: + case INTO: + case VALUES: + case PRIMARY: + case KEY: + case CONSTRAINT: + case FOREIGN: + case REFERENCES: + case DELETE: + case UPDATE: + case CASCADE: + case RESTRICT: + case ACTION: + case SET: + case DEFAULT: + case NO: + case UNIQUE: + case CLUSTERED: + case IDENTITY: + case NONCLUSTERED: + case INDEX: + case TEMPORARY: + case WITH: + case COMPRESSION: + case COMP: + case DISALLOW: + case IGNORE: + case CHECK: + case VIEW: + case PROCEDURE: + case PARAMETERS: + case EXECUTE: + case EXEC: + case ASC: + case DESC: + case TRUE: + case FALSE: + case NULL: + EnterOuterAlt(_localctx, 2); + { + State = 1201; + reservedKeyword(); + } + break; + default: + throw new NoViableAltException(this); } } catch (RecognitionException re) { @@ -8214,32 +8460,34 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public IdentifierContext identifier() { IdentifierContext _localctx = new IdentifierContext(Context, State); - EnterRule(_localctx, 126, RULE_identifier); + EnterRule(_localctx, 128, RULE_identifier); try { - State = 1199; + State = 1208; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1195; + State = 1204; Match(IDENTIFIER); } break; case BRACKET_ID: EnterOuterAlt(_localctx, 2); { - State = 1196; + State = 1205; Match(BRACKET_ID); } break; case BACKTICK_ID: EnterOuterAlt(_localctx, 3); { - State = 1197; + State = 1206; Match(BACKTICK_ID); } break; + case YES: + case OFF: case RANGE: case GROUPS: case UNBOUNDED: @@ -8256,7 +8504,7 @@ public IdentifierContext identifier() { case FILTER: EnterOuterAlt(_localctx, 4); { - State = 1198; + State = 1207; nonReservedKeyword(); } break; @@ -8309,6 +8557,8 @@ public override TResult Accept(IParseTreeVisitor visitor) { } public partial class TrueLiteralContext : LiteralContext { [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode TRUE() { return GetToken(AccessSqlParser.TRUE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode YES() { return GetToken(AccessSqlParser.YES, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ON() { return GetToken(AccessSqlParser.ON, 0); } public TrueLiteralContext(LiteralContext context) { CopyFrom(context); } [System.Diagnostics.DebuggerNonUserCode] public override TResult Accept(IParseTreeVisitor visitor) { @@ -8369,6 +8619,8 @@ public override TResult Accept(IParseTreeVisitor visitor) { } public partial class FalseLiteralContext : LiteralContext { [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FALSE() { return GetToken(AccessSqlParser.FALSE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode NO() { return GetToken(AccessSqlParser.NO, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OFF() { return GetToken(AccessSqlParser.OFF, 0); } public FalseLiteralContext(LiteralContext context) { CopyFrom(context); } [System.Diagnostics.DebuggerNonUserCode] public override TResult Accept(IParseTreeVisitor visitor) { @@ -8381,16 +8633,17 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public LiteralContext literal() { LiteralContext _localctx = new LiteralContext(Context, State); - EnterRule(_localctx, 128, RULE_literal); + EnterRule(_localctx, 130, RULE_literal); + int _la; try { - State = 1210; + State = 1219; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: _localctx = new IntLiteralContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1201; + State = 1210; Match(INTEGER_LITERAL); } break; @@ -8398,7 +8651,7 @@ public LiteralContext literal() { _localctx = new NumberLiteralContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1202; + State = 1211; Match(NUMBER_LITERAL); } break; @@ -8406,7 +8659,7 @@ public LiteralContext literal() { _localctx = new HexLiteralContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1203; + State = 1212; Match(HEX_LITERAL); } break; @@ -8414,7 +8667,7 @@ public LiteralContext literal() { _localctx = new StringLiteralContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 1204; + State = 1213; Match(STRING_LITERAL); } break; @@ -8422,7 +8675,7 @@ public LiteralContext literal() { _localctx = new DateLiteralContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 1205; + State = 1214; Match(DATE_LITERAL); } break; @@ -8430,31 +8683,49 @@ public LiteralContext literal() { _localctx = new GuidLiteralContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 1206; + State = 1215; Match(GUID_LITERAL); } break; + case ON: case TRUE: + case YES: _localctx = new TrueLiteralContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 1207; - Match(TRUE); + State = 1216; + _la = TokenStream.LA(1); + if ( !(_la==ON || _la==TRUE || _la==YES) ) { + ErrorHandler.RecoverInline(this); + } + else { + ErrorHandler.ReportMatch(this); + Consume(); + } } break; + case NO: case FALSE: + case OFF: _localctx = new FalseLiteralContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 1208; - Match(FALSE); + State = 1217; + _la = TokenStream.LA(1); + if ( !(((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & 10485761L) != 0)) ) { + ErrorHandler.RecoverInline(this); + } + else { + ErrorHandler.ReportMatch(this); + Consume(); + } } break; case NULL: _localctx = new NullLiteralContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 1209; + State = 1218; Match(NULL); } break; @@ -8525,24 +8796,24 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public TransactionStatementContext transactionStatement() { TransactionStatementContext _localctx = new TransactionStatementContext(Context, State); - EnterRule(_localctx, 130, RULE_transactionStatement); + EnterRule(_localctx, 132, RULE_transactionStatement); int _la; try { - State = 1224; + State = 1233; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BEGIN: _localctx = new BeginTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1212; + State = 1221; Match(BEGIN); - State = 1214; + State = 1223; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1213; + State = 1222; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8560,14 +8831,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new CommitTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1216; + State = 1225; Match(COMMIT); - State = 1218; + State = 1227; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1217; + State = 1226; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8585,14 +8856,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new RollbackTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1220; + State = 1229; Match(ROLLBACK); - State = 1222; + State = 1231; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1221; + State = 1230; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8642,13 +8913,13 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public StandaloneExpressionContext standaloneExpression() { StandaloneExpressionContext _localctx = new StandaloneExpressionContext(Context, State); - EnterRule(_localctx, 132, RULE_standaloneExpression); + EnterRule(_localctx, 134, RULE_standaloneExpression); try { EnterOuterAlt(_localctx, 1); { - State = 1226; + State = 1235; expression(0); - State = 1227; + State = 1236; Match(Eof); } } @@ -8702,66 +8973,66 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public WindowSpecificationContext windowSpecification() { WindowSpecificationContext _localctx = new WindowSpecificationContext(Context, State); - EnterRule(_localctx, 134, RULE_windowSpecification); + EnterRule(_localctx, 136, RULE_windowSpecification); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1229; + State = 1238; Match(LPAREN); - State = 1240; + State = 1249; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PARTITION) { { - State = 1230; + State = 1239; Match(PARTITION); - State = 1231; + State = 1240; Match(BY); - State = 1232; + State = 1241; _localctx._expression = expression(0); _localctx._partition.Add(_localctx._expression); - State = 1237; + State = 1246; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1233; + State = 1242; Match(COMMA); - State = 1234; + State = 1243; _localctx._expression = expression(0); _localctx._partition.Add(_localctx._expression); } } - State = 1239; + State = 1248; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } } } - State = 1243; + State = 1252; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ORDER) { { - State = 1242; + State = 1251; orderByClause(); } } - State = 1246; + State = 1255; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 1729382256910270465L) != 0)) { + if (((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 6917529027641081857L) != 0)) { { - State = 1245; + State = 1254; windowFrame(); } } - State = 1248; + State = 1257; Match(RPAREN); } } @@ -8795,12 +9066,12 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public ClusteredOptionContext clusteredOption() { ClusteredOptionContext _localctx = new ClusteredOptionContext(Context, State); - EnterRule(_localctx, 136, RULE_clusteredOption); + EnterRule(_localctx, 138, RULE_clusteredOption); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1250; + State = 1259; _la = TokenStream.LA(1); if ( !(_la==CLUSTERED || _la==NONCLUSTERED) ) { ErrorHandler.RecoverInline(this); @@ -8858,33 +9129,33 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public WindowFrameContext windowFrame() { WindowFrameContext _localctx = new WindowFrameContext(Context, State); - EnterRule(_localctx, 138, RULE_windowFrame); + EnterRule(_localctx, 140, RULE_windowFrame); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1252; + State = 1261; _localctx.unit = TokenStream.LT(1); _la = TokenStream.LA(1); - if ( !(((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 1729382256910270465L) != 0)) ) { + if ( !(((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 6917529027641081857L) != 0)) ) { _localctx.unit = ErrorHandler.RecoverInline(this); } else { ErrorHandler.ReportMatch(this); Consume(); } - State = 1259; + State = 1268; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BETWEEN: { - State = 1253; + State = 1262; Match(BETWEEN); - State = 1254; + State = 1263; _localctx.start = frameBound(); - State = 1255; + State = 1264; Match(AND); - State = 1256; + State = 1265; _localctx.end = frameBound(); } break; @@ -8892,13 +9163,17 @@ public WindowFrameContext windowFrame() { case BNOT: case LEFT: case RIGHT: + case ON: case EXISTS: case PARTITION: case CASE: case FIRST: + case NO: case ASC: case TRUE: case FALSE: + case YES: + case OFF: case NULL: case RANGE: case GROUPS: @@ -8929,21 +9204,21 @@ public WindowFrameContext windowFrame() { case BACKTICK_ID: case IDENTIFIER: { - State = 1258; + State = 1267; _localctx.start = frameBound(); } break; default: throw new NoViableAltException(this); } - State = 1263; + State = 1272; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==EXCLUDE) { { - State = 1261; + State = 1270; Match(EXCLUDE); - State = 1262; + State = 1271; _localctx.exclusion = frameExclusion(); } } @@ -8988,18 +9263,18 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public FrameBoundContext frameBound() { FrameBoundContext _localctx = new FrameBoundContext(Context, State); - EnterRule(_localctx, 140, RULE_frameBound); + EnterRule(_localctx, 142, RULE_frameBound); int _la; try { - State = 1272; + State = 1281; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,162,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,163,Context) ) { case 1: EnterOuterAlt(_localctx, 1); { - State = 1265; + State = 1274; Match(UNBOUNDED); - State = 1266; + State = 1275; _localctx.direction = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PRECEDING || _la==FOLLOWING) ) { @@ -9014,18 +9289,18 @@ public FrameBoundContext frameBound() { case 2: EnterOuterAlt(_localctx, 2); { - State = 1267; + State = 1276; Match(CURRENT); - State = 1268; + State = 1277; Match(ROW); } break; case 3: EnterOuterAlt(_localctx, 3); { - State = 1269; + State = 1278; _localctx.offset = expression(0); - State = 1270; + State = 1279; _localctx.direction = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PRECEDING || _la==FOLLOWING) ) { @@ -9073,40 +9348,40 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public FrameExclusionContext frameExclusion() { FrameExclusionContext _localctx = new FrameExclusionContext(Context, State); - EnterRule(_localctx, 142, RULE_frameExclusion); + EnterRule(_localctx, 144, RULE_frameExclusion); try { - State = 1280; + State = 1289; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case CURRENT: EnterOuterAlt(_localctx, 1); { - State = 1274; + State = 1283; Match(CURRENT); - State = 1275; + State = 1284; Match(ROW); } break; case GROUP: EnterOuterAlt(_localctx, 2); { - State = 1276; + State = 1285; Match(GROUP); } break; case TIES: EnterOuterAlt(_localctx, 3); { - State = 1277; + State = 1286; Match(TIES); } break; case NO: EnterOuterAlt(_localctx, 4); { - State = 1278; + State = 1287; Match(NO); - State = 1279; + State = 1288; Match(OTHERS); } break; @@ -9140,6 +9415,8 @@ public partial class NonReservedKeywordContext : ParserRuleContext { [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode RESPECT() { return GetToken(AccessSqlParser.RESPECT, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode NULLS() { return GetToken(AccessSqlParser.NULLS, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FILTER() { return GetToken(AccessSqlParser.FILTER, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode YES() { return GetToken(AccessSqlParser.YES, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OFF() { return GetToken(AccessSqlParser.OFF, 0); } public NonReservedKeywordContext(ParserRuleContext parent, int invokingState) : base(parent, invokingState) { @@ -9156,14 +9433,165 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public NonReservedKeywordContext nonReservedKeyword() { NonReservedKeywordContext _localctx = new NonReservedKeywordContext(Context, State); - EnterRule(_localctx, 144, RULE_nonReservedKeyword); + EnterRule(_localctx, 146, RULE_nonReservedKeyword); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1282; + State = 1291; _la = TokenStream.LA(1); - if ( !(((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 16383L) != 0)) ) { + if ( !(((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 131067L) != 0)) ) { + ErrorHandler.RecoverInline(this); + } + else { + ErrorHandler.ReportMatch(this); + Consume(); + } + } + } + catch (RecognitionException re) { + _localctx.exception = re; + ErrorHandler.ReportError(this, re); + ErrorHandler.Recover(this, re); + } + finally { + ExitRule(); + } + return _localctx; + } + + public partial class ReservedKeywordContext : ParserRuleContext { + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode SELECT() { return GetToken(AccessSqlParser.SELECT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FROM() { return GetToken(AccessSqlParser.FROM, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode WHERE() { return GetToken(AccessSqlParser.WHERE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode TOP() { return GetToken(AccessSqlParser.TOP, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode AS() { return GetToken(AccessSqlParser.AS, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode AND() { return GetToken(AccessSqlParser.AND, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OR() { return GetToken(AccessSqlParser.OR, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode NOT() { return GetToken(AccessSqlParser.NOT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode XOR() { return GetToken(AccessSqlParser.XOR, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode EQV() { return GetToken(AccessSqlParser.EQV, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode IMP() { return GetToken(AccessSqlParser.IMP, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BAND() { return GetToken(AccessSqlParser.BAND, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BOR() { return GetToken(AccessSqlParser.BOR, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BXOR() { return GetToken(AccessSqlParser.BXOR, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BNOT() { return GetToken(AccessSqlParser.BNOT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode LIKE() { return GetToken(AccessSqlParser.LIKE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode MOD() { return GetToken(AccessSqlParser.MOD, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode INNER() { return GetToken(AccessSqlParser.INNER, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode LEFT() { return GetToken(AccessSqlParser.LEFT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode RIGHT() { return GetToken(AccessSqlParser.RIGHT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FULL() { return GetToken(AccessSqlParser.FULL, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OUTER() { return GetToken(AccessSqlParser.OUTER, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode JOIN() { return GetToken(AccessSqlParser.JOIN, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode IN() { return GetToken(AccessSqlParser.IN, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ON() { return GetToken(AccessSqlParser.ON, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ORDER() { return GetToken(AccessSqlParser.ORDER, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode GROUP() { return GetToken(AccessSqlParser.GROUP, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode IS() { return GetToken(AccessSqlParser.IS, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BY() { return GetToken(AccessSqlParser.BY, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode HAVING() { return GetToken(AccessSqlParser.HAVING, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode EXISTS() { return GetToken(AccessSqlParser.EXISTS, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode IF() { return GetToken(AccessSqlParser.IF, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode THEN() { return GetToken(AccessSqlParser.THEN, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DISTINCTROW() { return GetToken(AccessSqlParser.DISTINCTROW, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DISTINCT() { return GetToken(AccessSqlParser.DISTINCT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode PERCENT() { return GetToken(AccessSqlParser.PERCENT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CROSS() { return GetToken(AccessSqlParser.CROSS, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode APPLY() { return GetToken(AccessSqlParser.APPLY, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OVER() { return GetToken(AccessSqlParser.OVER, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode PARTITION() { return GetToken(AccessSqlParser.PARTITION, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CASE() { return GetToken(AccessSqlParser.CASE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode WHEN() { return GetToken(AccessSqlParser.WHEN, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ELSE() { return GetToken(AccessSqlParser.ELSE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode END() { return GetToken(AccessSqlParser.END, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OFFSET() { return GetToken(AccessSqlParser.OFFSET, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FETCH() { return GetToken(AccessSqlParser.FETCH, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode NEXT() { return GetToken(AccessSqlParser.NEXT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FIRST() { return GetToken(AccessSqlParser.FIRST, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ROWS() { return GetToken(AccessSqlParser.ROWS, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ROW() { return GetToken(AccessSqlParser.ROW, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ONLY() { return GetToken(AccessSqlParser.ONLY, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BETWEEN() { return GetToken(AccessSqlParser.BETWEEN, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode UNION() { return GetToken(AccessSqlParser.UNION, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ALL() { return GetToken(AccessSqlParser.ALL, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode INTERSECT() { return GetToken(AccessSqlParser.INTERSECT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode EXCEPT() { return GetToken(AccessSqlParser.EXCEPT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CREATE() { return GetToken(AccessSqlParser.CREATE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode TABLE() { return GetToken(AccessSqlParser.TABLE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BEGIN() { return GetToken(AccessSqlParser.BEGIN, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMMIT() { return GetToken(AccessSqlParser.COMMIT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ROLLBACK() { return GetToken(AccessSqlParser.ROLLBACK, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode TRANSACTION() { return GetToken(AccessSqlParser.TRANSACTION, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode WORK() { return GetToken(AccessSqlParser.WORK, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ALTER() { return GetToken(AccessSqlParser.ALTER, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode RENAME() { return GetToken(AccessSqlParser.RENAME, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode TO() { return GetToken(AccessSqlParser.TO, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ADD() { return GetToken(AccessSqlParser.ADD, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DROP() { return GetToken(AccessSqlParser.DROP, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COLUMN() { return GetToken(AccessSqlParser.COLUMN, 0); } + [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 ITerminalNode VALUES() { return GetToken(AccessSqlParser.VALUES, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode PRIMARY() { return GetToken(AccessSqlParser.PRIMARY, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode KEY() { return GetToken(AccessSqlParser.KEY, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CONSTRAINT() { return GetToken(AccessSqlParser.CONSTRAINT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FOREIGN() { return GetToken(AccessSqlParser.FOREIGN, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode REFERENCES() { return GetToken(AccessSqlParser.REFERENCES, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DELETE() { return GetToken(AccessSqlParser.DELETE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode UPDATE() { return GetToken(AccessSqlParser.UPDATE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CASCADE() { return GetToken(AccessSqlParser.CASCADE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode RESTRICT() { return GetToken(AccessSqlParser.RESTRICT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ACTION() { return GetToken(AccessSqlParser.ACTION, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode SET() { return GetToken(AccessSqlParser.SET, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DEFAULT() { return GetToken(AccessSqlParser.DEFAULT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode NO() { return GetToken(AccessSqlParser.NO, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode UNIQUE() { return GetToken(AccessSqlParser.UNIQUE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CLUSTERED() { return GetToken(AccessSqlParser.CLUSTERED, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode IDENTITY() { return GetToken(AccessSqlParser.IDENTITY, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode NONCLUSTERED() { return GetToken(AccessSqlParser.NONCLUSTERED, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode INDEX() { return GetToken(AccessSqlParser.INDEX, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode TEMPORARY() { return GetToken(AccessSqlParser.TEMPORARY, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode WITH() { return GetToken(AccessSqlParser.WITH, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMPRESSION() { return GetToken(AccessSqlParser.COMPRESSION, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMP() { return GetToken(AccessSqlParser.COMP, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DISALLOW() { return GetToken(AccessSqlParser.DISALLOW, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode IGNORE() { return GetToken(AccessSqlParser.IGNORE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CHECK() { return GetToken(AccessSqlParser.CHECK, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode VIEW() { return GetToken(AccessSqlParser.VIEW, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode PROCEDURE() { return GetToken(AccessSqlParser.PROCEDURE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode PARAMETERS() { return GetToken(AccessSqlParser.PARAMETERS, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode EXECUTE() { return GetToken(AccessSqlParser.EXECUTE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode EXEC() { return GetToken(AccessSqlParser.EXEC, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ASC() { return GetToken(AccessSqlParser.ASC, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DESC() { return GetToken(AccessSqlParser.DESC, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode TRUE() { return GetToken(AccessSqlParser.TRUE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FALSE() { return GetToken(AccessSqlParser.FALSE, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode NULL() { return GetToken(AccessSqlParser.NULL, 0); } + public ReservedKeywordContext(ParserRuleContext parent, int invokingState) + : base(parent, invokingState) + { + } + public override int RuleIndex { get { return RULE_reservedKeyword; } } + [System.Diagnostics.DebuggerNonUserCode] + public override TResult Accept(IParseTreeVisitor visitor) { + IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor; + if (typedVisitor != null) return typedVisitor.VisitReservedKeyword(this); + else return visitor.VisitChildren(this); + } + } + + [RuleVersion(0)] + public ReservedKeywordContext reservedKeyword() { + ReservedKeywordContext _localctx = new ReservedKeywordContext(Context, State); + EnterRule(_localctx, 148, RULE_reservedKeyword); + int _la; + try { + EnterOuterAlt(_localctx, 1); + { + State = 1293; + _la = TokenStream.LA(1); + if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & 43980465111039L) != 0)) ) { ErrorHandler.RecoverInline(this); } else { @@ -9208,19 +9636,19 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public FilterClauseContext filterClause() { FilterClauseContext _localctx = new FilterClauseContext(Context, State); - EnterRule(_localctx, 146, RULE_filterClause); + EnterRule(_localctx, 150, RULE_filterClause); try { EnterOuterAlt(_localctx, 1); { - State = 1284; + State = 1295; Match(FILTER); - State = 1285; + State = 1296; Match(LPAREN); - State = 1286; + State = 1297; Match(WHERE); - State = 1287; + State = 1298; _localctx.condition = expression(0); - State = 1288; + State = 1299; Match(RPAREN); } } @@ -9259,19 +9687,19 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public WithinGroupContext withinGroup() { WithinGroupContext _localctx = new WithinGroupContext(Context, State); - EnterRule(_localctx, 148, RULE_withinGroup); + EnterRule(_localctx, 152, RULE_withinGroup); try { EnterOuterAlt(_localctx, 1); { - State = 1290; + State = 1301; Match(WITHIN); - State = 1291; + State = 1302; Match(GROUP); - State = 1292; + State = 1303; Match(LPAREN); - State = 1293; + State = 1304; orderByClause(); - State = 1294; + State = 1305; Match(RPAREN); } } @@ -9307,14 +9735,14 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public NthRowFromContext nthRowFrom() { NthRowFromContext _localctx = new NthRowFromContext(Context, State); - EnterRule(_localctx, 150, RULE_nthRowFrom); + EnterRule(_localctx, 154, RULE_nthRowFrom); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1296; + State = 1307; Match(FROM); - State = 1297; + State = 1308; _localctx.edge = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==FIRST || _la==LAST) ) { @@ -9358,12 +9786,12 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public NullTreatmentContext nullTreatment() { NullTreatmentContext _localctx = new NullTreatmentContext(Context, State); - EnterRule(_localctx, 152, RULE_nullTreatment); + EnterRule(_localctx, 156, RULE_nullTreatment); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1299; + State = 1310; _localctx.treatment = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==IGNORE || _la==RESPECT) ) { @@ -9373,7 +9801,7 @@ public NullTreatmentContext nullTreatment() { ErrorHandler.ReportMatch(this); Consume(); } - State = 1300; + State = 1311; Match(NULLS); } } @@ -9420,7 +9848,7 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { } private static int[] _serializedATN = { - 4,1,153,1303,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,156,1314,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, @@ -9430,478 +9858,483 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { 2,50,7,50,2,51,7,51,2,52,7,52,2,53,7,53,2,54,7,54,2,55,7,55,2,56,7,56, 2,57,7,57,2,58,7,58,2,59,7,59,2,60,7,60,2,61,7,61,2,62,7,62,2,63,7,63, 2,64,7,64,2,65,7,65,2,66,7,66,2,67,7,67,2,68,7,68,2,69,7,69,2,70,7,70, - 2,71,7,71,2,72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,1,0,3,0,156, - 8,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,3,0,172,8, - 0,1,0,3,0,175,8,0,1,0,1,0,1,1,1,1,3,1,181,8,1,1,1,1,1,1,1,1,1,1,1,1,1, - 1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,3,2,201,8,2,1,3,1,3,1, - 3,1,3,1,3,5,3,208,8,3,10,3,12,3,211,9,3,3,3,213,8,3,1,4,1,4,1,4,1,4,5, - 4,219,8,4,10,4,12,4,222,9,4,1,4,1,4,1,4,1,4,5,4,228,8,4,10,4,12,4,231, - 9,4,1,4,3,4,234,8,4,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,3,6,246,8, - 6,1,6,1,6,1,6,1,6,5,6,252,8,6,10,6,12,6,255,9,6,1,6,3,6,258,8,6,1,7,1, - 7,1,7,1,7,5,7,264,8,7,10,7,12,7,267,9,7,1,8,1,8,3,8,271,8,8,1,8,3,8,274, - 8,8,1,9,1,9,1,9,1,9,5,9,280,8,9,10,9,12,9,283,9,9,1,9,1,9,1,10,1,10,3, - 10,289,8,10,1,10,1,10,1,10,1,10,1,10,1,10,5,10,297,8,10,10,10,12,10,300, - 9,10,1,10,1,10,5,10,304,8,10,10,10,12,10,307,9,10,1,10,1,10,1,11,1,11, - 1,11,1,11,1,11,1,11,1,11,5,11,318,8,11,10,11,12,11,321,9,11,1,11,1,11, - 3,11,325,8,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,3,12,334,8,12,1,12,1, - 12,1,12,1,13,1,13,1,13,1,13,5,13,343,8,13,10,13,12,13,346,9,13,1,13,1, - 13,1,13,1,13,1,13,5,13,353,8,13,10,13,12,13,356,9,13,3,13,358,8,13,1,14, - 1,14,1,14,1,15,1,15,3,15,365,8,15,1,16,1,16,1,16,1,16,1,16,1,17,1,17,3, - 17,374,8,17,1,17,1,17,1,17,1,17,1,17,3,17,381,8,17,1,17,1,17,1,17,5,17, - 386,8,17,10,17,12,17,389,9,17,1,17,1,17,3,17,393,8,17,1,17,1,17,1,17,1, - 17,1,17,1,17,1,17,3,17,402,8,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17, - 1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17, - 1,17,1,17,1,17,3,17,429,8,17,1,18,1,18,1,18,1,18,1,18,3,18,436,8,18,1, - 19,1,19,3,19,440,8,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,5,19,450, - 8,19,10,19,12,19,453,9,19,1,19,1,19,1,19,3,19,458,8,19,1,20,1,20,1,20, - 1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,3,20,475,8, - 20,1,21,1,21,3,21,479,8,21,1,22,1,22,1,22,1,22,1,22,3,22,486,8,22,1,23, - 1,23,1,23,3,23,491,8,23,1,23,5,23,494,8,23,10,23,12,23,497,9,23,1,24,1, - 24,1,24,1,24,1,24,1,25,1,25,3,25,506,8,25,1,25,3,25,509,8,25,1,25,3,25, - 512,8,25,1,25,1,25,1,25,1,25,3,25,518,8,25,1,25,1,25,3,25,522,8,25,1,26, - 3,26,525,8,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3, - 27,538,8,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,547,8,27,1,27,1,27, - 1,27,3,27,552,8,27,1,27,1,27,3,27,556,8,27,1,27,1,27,3,27,560,8,27,1,27, - 1,27,3,27,564,8,27,1,27,1,27,1,27,1,27,1,27,1,27,5,27,572,8,27,10,27,12, - 27,575,9,27,1,27,1,27,3,27,579,8,27,1,27,5,27,582,8,27,10,27,12,27,585, - 9,27,1,27,1,27,1,27,1,27,1,27,3,27,592,8,27,1,27,1,27,3,27,596,8,27,3, - 27,598,8,27,1,28,1,28,3,28,602,8,28,1,28,1,28,1,28,3,28,607,8,28,1,28, - 1,28,1,28,1,28,5,28,613,8,28,10,28,12,28,616,9,28,1,28,1,28,1,28,1,28, - 3,28,622,8,28,1,28,1,28,3,28,626,8,28,1,28,1,28,1,28,1,28,5,28,632,8,28, - 10,28,12,28,635,9,28,1,28,1,28,1,28,1,28,3,28,641,8,28,1,28,1,28,1,28, - 1,28,3,28,647,8,28,1,28,1,28,1,28,1,28,5,28,653,8,28,10,28,12,28,656,9, - 28,1,28,1,28,1,28,1,28,1,28,1,28,1,28,5,28,665,8,28,10,28,12,28,668,9, - 28,1,28,1,28,3,28,672,8,28,1,28,5,28,675,8,28,10,28,12,28,678,9,28,1,28, - 1,28,3,28,682,8,28,1,28,1,28,1,28,1,28,1,28,3,28,689,8,28,1,29,1,29,1, - 29,1,29,1,29,5,29,696,8,29,10,29,12,29,699,9,29,1,30,1,30,1,30,1,30,1, - 30,1,30,3,30,707,8,30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,3,31,717, - 8,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32,5,32,726,8,32,10,32,12,32,729, - 9,32,1,32,1,32,3,32,733,8,32,1,32,1,32,1,32,1,32,5,32,739,8,32,10,32,12, - 32,742,9,32,1,32,3,32,745,8,32,1,32,1,32,3,32,749,8,32,1,33,1,33,1,33, - 1,33,5,33,755,8,33,10,33,12,33,758,9,33,1,33,1,33,1,34,1,34,3,34,764,8, - 34,1,35,1,35,1,35,1,35,5,35,770,8,35,10,35,12,35,773,9,35,1,35,3,35,776, - 8,35,1,35,3,35,779,8,35,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,5, - 36,790,8,36,10,36,12,36,793,9,36,3,36,795,8,36,1,37,1,37,3,37,799,8,37, - 1,37,1,37,3,37,803,8,37,1,38,1,38,3,38,807,8,38,1,38,3,38,810,8,38,1,38, - 1,38,1,38,3,38,815,8,38,1,38,3,38,818,8,38,1,38,3,38,821,8,38,1,38,3,38, - 824,8,38,1,38,3,38,827,8,38,1,39,1,39,1,40,1,40,1,40,1,40,1,40,5,40,836, - 8,40,10,40,12,40,839,9,40,1,41,1,41,1,41,1,42,1,42,1,42,1,42,5,42,848, - 8,42,10,42,12,42,851,9,42,1,42,3,42,854,8,42,1,43,1,43,1,43,1,43,1,43, - 1,43,3,43,862,8,43,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,873, - 8,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,881,8,44,1,45,1,45,1,46,1,46,1, - 46,1,46,5,46,889,8,46,10,46,12,46,892,9,46,3,46,894,8,46,1,47,1,47,1,47, - 1,47,1,47,1,47,3,47,902,8,47,1,47,3,47,905,8,47,3,47,907,8,47,1,48,1,48, - 1,48,1,48,5,48,913,8,48,10,48,12,48,916,9,48,1,49,1,49,5,49,920,8,49,10, - 49,12,49,923,9,49,1,50,1,50,3,50,927,8,50,1,50,3,50,930,8,50,1,50,1,50, - 1,50,1,50,3,50,936,8,50,1,50,3,50,939,8,50,1,50,1,50,1,50,1,50,3,50,945, - 8,50,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,1,51,3,51,962,8,51,1,52,3,52,965,8,52,1,52,1,52,3,52,969,8,52,1,52, - 1,52,3,52,973,8,52,1,52,1,52,3,52,977,8,52,3,52,979,8,52,1,53,1,53,1,53, - 1,54,1,54,1,54,1,54,1,54,5,54,989,8,54,10,54,12,54,992,9,54,1,55,1,55, - 3,55,996,8,55,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1004,8,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1029,8,56,1,56,1,56,1,56,1,56,1, - 56,1,56,1,56,3,56,1038,8,56,1,56,1,56,1,56,1,56,1,56,3,56,1045,8,56,1, - 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,1,56,3,56,1067,8,56,1,56,1,56,1,56,1,56,1,56,1, - 56,1,56,3,56,1076,8,56,1,56,1,56,1,56,1,56,1,56,5,56,1083,8,56,10,56,12, - 56,1086,9,56,1,56,1,56,1,56,1,56,1,56,3,56,1093,8,56,1,56,1,56,1,56,1, - 56,3,56,1099,8,56,1,56,5,56,1102,8,56,10,56,12,56,1105,9,56,1,57,1,57, - 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, - 1,57,1,57,1,57,3,57,1126,8,57,1,58,1,58,3,58,1130,8,58,1,58,4,58,1133, - 8,58,11,58,12,58,1134,1,58,1,58,3,58,1139,8,58,1,58,1,58,1,59,1,59,1,59, - 1,59,1,59,1,60,1,60,1,60,1,60,3,60,1152,8,60,1,60,1,60,1,60,5,60,1157, - 8,60,10,60,12,60,1160,9,60,3,60,1162,8,60,1,60,1,60,3,60,1166,8,60,1,60, - 3,60,1169,8,60,1,60,3,60,1172,8,60,1,60,3,60,1175,8,60,1,60,1,60,3,60, - 1179,8,60,1,61,1,61,1,61,1,61,1,61,1,61,3,61,1187,8,61,1,62,1,62,1,62, - 3,62,1192,8,62,1,62,1,62,1,63,1,63,1,63,1,63,3,63,1200,8,63,1,64,1,64, - 1,64,1,64,1,64,1,64,1,64,1,64,1,64,3,64,1211,8,64,1,65,1,65,3,65,1215, - 8,65,1,65,1,65,3,65,1219,8,65,1,65,1,65,3,65,1223,8,65,3,65,1225,8,65, - 1,66,1,66,1,66,1,67,1,67,1,67,1,67,1,67,1,67,5,67,1236,8,67,10,67,12,67, - 1239,9,67,3,67,1241,8,67,1,67,3,67,1244,8,67,1,67,3,67,1247,8,67,1,67, - 1,67,1,68,1,68,1,69,1,69,1,69,1,69,1,69,1,69,1,69,3,69,1260,8,69,1,69, - 1,69,3,69,1264,8,69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,3,70,1273,8,70, - 1,71,1,71,1,71,1,71,1,71,1,71,3,71,1281,8,71,1,72,1,72,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,76,1,76, - 1,76,1,76,0,1,112,77,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34, + 2,71,7,71,2,72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77, + 2,78,7,78,1,0,3,0,160,8,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0, + 1,0,1,0,1,0,3,0,176,8,0,1,0,3,0,179,8,0,1,0,1,0,1,1,1,1,3,1,185,8,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2, + 3,2,205,8,2,1,3,1,3,1,3,1,3,1,3,5,3,212,8,3,10,3,12,3,215,9,3,3,3,217, + 8,3,1,4,1,4,1,4,1,4,5,4,223,8,4,10,4,12,4,226,9,4,1,4,1,4,1,4,1,4,5,4, + 232,8,4,10,4,12,4,235,9,4,1,4,3,4,238,8,4,1,5,1,5,1,5,1,5,1,6,1,6,1,6, + 1,6,1,6,1,6,3,6,250,8,6,1,6,1,6,1,6,1,6,5,6,256,8,6,10,6,12,6,259,9,6, + 1,6,3,6,262,8,6,1,7,1,7,1,7,1,7,5,7,268,8,7,10,7,12,7,271,9,7,1,8,1,8, + 3,8,275,8,8,1,8,3,8,278,8,8,1,9,1,9,1,9,1,9,5,9,284,8,9,10,9,12,9,287, + 9,9,1,9,1,9,1,10,1,10,3,10,293,8,10,1,10,1,10,1,10,1,10,1,10,1,10,5,10, + 301,8,10,10,10,12,10,304,9,10,1,10,1,10,5,10,308,8,10,10,10,12,10,311, + 9,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,11,1,11,5,11,322,8,11,10,11, + 12,11,325,9,11,1,11,1,11,3,11,329,8,11,1,11,1,11,1,11,1,12,1,12,1,12,1, + 12,3,12,338,8,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,5,13,347,8,13,10,13, + 12,13,350,9,13,1,13,1,13,1,13,1,13,1,13,5,13,357,8,13,10,13,12,13,360, + 9,13,3,13,362,8,13,1,14,1,14,1,14,1,15,1,15,3,15,369,8,15,1,16,1,16,1, + 16,1,16,1,16,1,17,1,17,3,17,378,8,17,1,17,1,17,1,17,1,17,1,17,3,17,385, + 8,17,1,17,1,17,1,17,5,17,390,8,17,10,17,12,17,393,9,17,1,17,1,17,3,17, + 397,8,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,406,8,17,1,17,1,17,1, + 17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1, + 17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,433,8,17,1,18,1,18,1,18, + 1,18,1,18,3,18,440,8,18,1,19,1,19,3,19,444,8,19,1,19,1,19,1,19,1,19,1, + 19,1,19,1,19,1,19,5,19,454,8,19,10,19,12,19,457,9,19,1,19,1,19,1,19,3, + 19,462,8,19,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20, + 1,20,1,20,1,20,3,20,479,8,20,1,21,1,21,3,21,483,8,21,1,22,1,22,1,22,1, + 22,1,22,3,22,490,8,22,1,23,1,23,1,23,3,23,495,8,23,1,23,5,23,498,8,23, + 10,23,12,23,501,9,23,1,24,1,24,1,24,1,24,1,24,1,25,1,25,3,25,510,8,25, + 1,25,3,25,513,8,25,1,25,3,25,516,8,25,1,25,1,25,1,25,1,25,3,25,522,8,25, + 1,25,1,25,3,25,526,8,25,1,26,3,26,529,8,26,1,26,1,26,1,27,1,27,1,27,1, + 27,1,27,1,27,1,27,1,27,1,27,3,27,542,8,27,1,27,1,27,1,27,1,27,1,27,1,27, + 1,27,3,27,551,8,27,1,27,1,27,1,27,3,27,556,8,27,1,27,1,27,3,27,560,8,27, + 1,27,1,27,3,27,564,8,27,1,27,1,27,3,27,568,8,27,1,27,1,27,1,27,1,27,1, + 27,1,27,5,27,576,8,27,10,27,12,27,579,9,27,1,27,1,27,3,27,583,8,27,1,27, + 5,27,586,8,27,10,27,12,27,589,9,27,1,27,1,27,1,27,1,27,1,27,3,27,596,8, + 27,1,27,1,27,3,27,600,8,27,3,27,602,8,27,1,28,1,28,3,28,606,8,28,1,28, + 1,28,1,28,3,28,611,8,28,1,28,1,28,1,28,1,28,5,28,617,8,28,10,28,12,28, + 620,9,28,1,28,1,28,1,28,1,28,3,28,626,8,28,1,28,1,28,3,28,630,8,28,1,28, + 1,28,1,28,1,28,5,28,636,8,28,10,28,12,28,639,9,28,1,28,1,28,1,28,1,28, + 3,28,645,8,28,1,28,1,28,1,28,1,28,3,28,651,8,28,1,28,1,28,1,28,1,28,5, + 28,657,8,28,10,28,12,28,660,9,28,1,28,1,28,1,28,1,28,1,28,1,28,1,28,5, + 28,669,8,28,10,28,12,28,672,9,28,1,28,1,28,3,28,676,8,28,1,28,5,28,679, + 8,28,10,28,12,28,682,9,28,1,28,1,28,3,28,686,8,28,1,28,1,28,1,28,1,28, + 1,28,3,28,693,8,28,1,29,1,29,1,29,1,29,1,29,5,29,700,8,29,10,29,12,29, + 703,9,29,1,30,1,30,1,30,1,30,1,30,1,30,3,30,711,8,30,1,31,1,31,1,31,1, + 31,1,31,1,31,1,31,1,31,3,31,721,8,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32, + 5,32,730,8,32,10,32,12,32,733,9,32,1,32,1,32,3,32,737,8,32,1,32,1,32,1, + 32,1,32,5,32,743,8,32,10,32,12,32,746,9,32,1,32,3,32,749,8,32,1,32,1,32, + 3,32,753,8,32,1,33,1,33,1,33,1,33,5,33,759,8,33,10,33,12,33,762,9,33,1, + 33,1,33,1,34,1,34,3,34,768,8,34,1,35,1,35,1,35,1,35,5,35,774,8,35,10,35, + 12,35,777,9,35,1,35,3,35,780,8,35,1,35,3,35,783,8,35,1,36,1,36,1,36,1, + 36,1,36,1,36,1,36,1,36,1,36,5,36,794,8,36,10,36,12,36,797,9,36,3,36,799, + 8,36,1,37,1,37,3,37,803,8,37,1,37,1,37,3,37,807,8,37,1,38,1,38,3,38,811, + 8,38,1,38,3,38,814,8,38,1,38,1,38,1,38,3,38,819,8,38,1,38,3,38,822,8,38, + 1,38,3,38,825,8,38,1,38,3,38,828,8,38,1,38,3,38,831,8,38,1,39,1,39,1,40, + 1,40,1,40,1,40,1,40,5,40,840,8,40,10,40,12,40,843,9,40,1,41,1,41,1,41, + 1,42,1,42,1,42,1,42,5,42,852,8,42,10,42,12,42,855,9,42,1,42,3,42,858,8, + 42,1,43,1,43,1,43,1,43,1,43,1,43,3,43,866,8,43,1,44,1,44,1,44,1,44,1,44, + 1,44,1,44,1,44,1,44,3,44,877,8,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,885, + 8,44,1,45,1,45,1,46,1,46,1,46,1,46,5,46,893,8,46,10,46,12,46,896,9,46, + 3,46,898,8,46,1,47,1,47,1,47,1,47,1,47,1,47,3,47,906,8,47,1,47,3,47,909, + 8,47,3,47,911,8,47,1,48,1,48,1,48,1,48,5,48,917,8,48,10,48,12,48,920,9, + 48,1,49,1,49,5,49,924,8,49,10,49,12,49,927,9,49,1,50,1,50,3,50,931,8,50, + 1,50,3,50,934,8,50,1,50,1,50,1,50,1,50,3,50,940,8,50,1,50,3,50,943,8,50, + 1,50,1,50,1,50,1,50,3,50,949,8,50,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,1,51,3,51,966,8,51,1,52,3,52,969,8,52, + 1,52,1,52,3,52,973,8,52,1,52,1,52,3,52,977,8,52,1,52,1,52,3,52,981,8,52, + 3,52,983,8,52,1,53,1,53,1,53,1,54,1,54,1,54,1,54,1,54,5,54,993,8,54,10, + 54,12,54,996,9,54,1,55,1,55,3,55,1000,8,55,1,56,1,56,1,56,1,56,1,56,1, + 56,3,56,1008,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, + 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1033, + 8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1042,8,56,1,56,1,56,1,56, + 1,56,1,56,3,56,1049,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56, + 1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1071,8,56, + 1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1080,8,56,1,56,1,56,1,56,1,56, + 1,56,5,56,1087,8,56,10,56,12,56,1090,9,56,1,56,1,56,1,56,1,56,1,56,3,56, + 1097,8,56,1,56,1,56,1,56,1,56,3,56,1103,8,56,1,56,5,56,1106,8,56,10,56, + 12,56,1109,9,56,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, + 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,3,57,1130,8,57,1,58,1,58,3,58, + 1134,8,58,1,58,4,58,1137,8,58,11,58,12,58,1138,1,58,1,58,3,58,1143,8,58, + 1,58,1,58,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60,1,60,3,60,1156,8,60, + 1,60,1,60,1,60,5,60,1161,8,60,10,60,12,60,1164,9,60,3,60,1166,8,60,1,60, + 1,60,3,60,1170,8,60,1,60,3,60,1173,8,60,1,60,3,60,1176,8,60,1,60,3,60, + 1179,8,60,1,60,1,60,3,60,1183,8,60,1,61,1,61,1,61,1,61,1,61,1,61,3,61, + 1191,8,61,1,62,1,62,1,62,5,62,1196,8,62,10,62,12,62,1199,9,62,1,63,1,63, + 3,63,1203,8,63,1,64,1,64,1,64,1,64,3,64,1209,8,64,1,65,1,65,1,65,1,65, + 1,65,1,65,1,65,1,65,1,65,3,65,1220,8,65,1,66,1,66,3,66,1224,8,66,1,66, + 1,66,3,66,1228,8,66,1,66,1,66,3,66,1232,8,66,3,66,1234,8,66,1,67,1,67, + 1,67,1,68,1,68,1,68,1,68,1,68,1,68,5,68,1245,8,68,10,68,12,68,1248,9,68, + 3,68,1250,8,68,1,68,3,68,1253,8,68,1,68,3,68,1256,8,68,1,68,1,68,1,69, + 1,69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,3,70,1269,8,70,1,70,1,70,3,70, + 1273,8,70,1,71,1,71,1,71,1,71,1,71,1,71,1,71,3,71,1282,8,71,1,72,1,72, + 1,72,1,72,1,72,1,72,3,72,1290,8,72,1,73,1,73,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,76,1,77,1,77,1,77,1,78,1,78, + 1,78,1,78,0,1,112,79,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,120,122, - 124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,0,22,1,0,101, - 102,1,0,103,104,1,0,93,94,1,0,135,136,2,0,34,35,54,54,1,0,126,127,1,0, - 47,48,1,0,49,50,2,0,8,8,15,15,1,0,122,123,1,0,129,134,2,0,6,6,12,12,2, - 0,7,7,13,13,2,0,9,9,14,14,1,0,105,106,1,0,62,63,2,0,87,87,89,89,2,0,49, - 49,108,109,1,0,111,112,1,0,108,121,2,0,48,48,118,118,2,0,96,96,119,119, - 1488,0,155,1,0,0,0,2,178,1,0,0,0,4,200,1,0,0,0,6,202,1,0,0,0,8,214,1,0, - 0,0,10,235,1,0,0,0,12,239,1,0,0,0,14,259,1,0,0,0,16,268,1,0,0,0,18,275, - 1,0,0,0,20,286,1,0,0,0,22,310,1,0,0,0,24,329,1,0,0,0,26,357,1,0,0,0,28, - 359,1,0,0,0,30,364,1,0,0,0,32,366,1,0,0,0,34,428,1,0,0,0,36,435,1,0,0, - 0,38,437,1,0,0,0,40,474,1,0,0,0,42,476,1,0,0,0,44,485,1,0,0,0,46,487,1, - 0,0,0,48,498,1,0,0,0,50,505,1,0,0,0,52,524,1,0,0,0,54,597,1,0,0,0,56,688, - 1,0,0,0,58,697,1,0,0,0,60,706,1,0,0,0,62,716,1,0,0,0,64,718,1,0,0,0,66, - 750,1,0,0,0,68,763,1,0,0,0,70,765,1,0,0,0,72,794,1,0,0,0,74,802,1,0,0, - 0,76,804,1,0,0,0,78,828,1,0,0,0,80,830,1,0,0,0,82,840,1,0,0,0,84,843,1, - 0,0,0,86,861,1,0,0,0,88,880,1,0,0,0,90,882,1,0,0,0,92,893,1,0,0,0,94,906, - 1,0,0,0,96,908,1,0,0,0,98,917,1,0,0,0,100,944,1,0,0,0,102,961,1,0,0,0, - 104,978,1,0,0,0,106,980,1,0,0,0,108,983,1,0,0,0,110,993,1,0,0,0,112,1003, - 1,0,0,0,114,1125,1,0,0,0,116,1127,1,0,0,0,118,1142,1,0,0,0,120,1147,1, - 0,0,0,122,1186,1,0,0,0,124,1191,1,0,0,0,126,1199,1,0,0,0,128,1210,1,0, - 0,0,130,1224,1,0,0,0,132,1226,1,0,0,0,134,1229,1,0,0,0,136,1250,1,0,0, - 0,138,1252,1,0,0,0,140,1272,1,0,0,0,142,1280,1,0,0,0,144,1282,1,0,0,0, - 146,1284,1,0,0,0,148,1290,1,0,0,0,150,1296,1,0,0,0,152,1299,1,0,0,0,154, - 156,3,18,9,0,155,154,1,0,0,0,155,156,1,0,0,0,156,171,1,0,0,0,157,172,3, - 2,1,0,158,172,3,20,10,0,159,172,3,38,19,0,160,172,3,22,11,0,161,172,3, - 24,12,0,162,172,3,32,16,0,163,172,3,40,20,0,164,172,3,64,32,0,165,172, - 3,8,4,0,166,172,3,12,6,0,167,172,3,130,65,0,168,172,3,6,3,0,169,172,3, - 14,7,0,170,172,3,70,35,0,171,157,1,0,0,0,171,158,1,0,0,0,171,159,1,0,0, - 0,171,160,1,0,0,0,171,161,1,0,0,0,171,162,1,0,0,0,171,163,1,0,0,0,171, - 164,1,0,0,0,171,165,1,0,0,0,171,166,1,0,0,0,171,167,1,0,0,0,171,168,1, - 0,0,0,171,169,1,0,0,0,171,170,1,0,0,0,172,174,1,0,0,0,173,175,5,139,0, - 0,174,173,1,0,0,0,174,175,1,0,0,0,175,176,1,0,0,0,176,177,5,0,0,1,177, - 1,1,0,0,0,178,180,5,32,0,0,179,181,5,8,0,0,180,179,1,0,0,0,180,181,1,0, - 0,0,181,182,1,0,0,0,182,183,5,31,0,0,183,184,5,135,0,0,184,185,3,70,35, - 0,185,186,5,136,0,0,186,187,5,33,0,0,187,188,3,4,2,0,188,3,1,0,0,0,189, - 201,3,20,10,0,190,201,3,38,19,0,191,201,3,22,11,0,192,201,3,24,12,0,193, - 201,3,32,16,0,194,201,3,40,20,0,195,201,3,64,32,0,196,201,3,8,4,0,197, - 201,3,12,6,0,198,201,3,6,3,0,199,201,3,70,35,0,200,189,1,0,0,0,200,190, - 1,0,0,0,200,191,1,0,0,0,200,192,1,0,0,0,200,193,1,0,0,0,200,194,1,0,0, - 0,200,195,1,0,0,0,200,196,1,0,0,0,200,197,1,0,0,0,200,198,1,0,0,0,200, - 199,1,0,0,0,201,5,1,0,0,0,202,203,7,0,0,0,203,212,3,126,63,0,204,209,3, - 112,56,0,205,206,5,137,0,0,206,208,3,112,56,0,207,205,1,0,0,0,208,211, - 1,0,0,0,209,207,1,0,0,0,209,210,1,0,0,0,210,213,1,0,0,0,211,209,1,0,0, - 0,212,204,1,0,0,0,212,213,1,0,0,0,213,7,1,0,0,0,214,215,5,79,0,0,215,220, - 3,98,49,0,216,217,5,137,0,0,217,219,3,98,49,0,218,216,1,0,0,0,219,222, - 1,0,0,0,220,218,1,0,0,0,220,221,1,0,0,0,221,223,1,0,0,0,222,220,1,0,0, - 0,223,224,5,83,0,0,224,229,3,10,5,0,225,226,5,137,0,0,226,228,3,10,5,0, - 227,225,1,0,0,0,228,231,1,0,0,0,229,227,1,0,0,0,229,230,1,0,0,0,230,233, - 1,0,0,0,231,229,1,0,0,0,232,234,3,106,53,0,233,232,1,0,0,0,233,234,1,0, - 0,0,234,9,1,0,0,0,235,236,3,124,62,0,236,237,5,129,0,0,237,238,3,112,56, - 0,238,11,1,0,0,0,239,245,5,78,0,0,240,241,3,126,63,0,241,242,5,138,0,0, - 242,243,5,122,0,0,243,246,1,0,0,0,244,246,5,122,0,0,245,240,1,0,0,0,245, - 244,1,0,0,0,245,246,1,0,0,0,246,247,1,0,0,0,247,248,5,2,0,0,248,253,3, - 98,49,0,249,250,5,137,0,0,250,252,3,98,49,0,251,249,1,0,0,0,252,255,1, - 0,0,0,253,251,1,0,0,0,253,254,1,0,0,0,254,257,1,0,0,0,255,253,1,0,0,0, - 256,258,3,106,53,0,257,256,1,0,0,0,257,258,1,0,0,0,258,13,1,0,0,0,259, - 260,5,1,0,0,260,265,3,16,8,0,261,262,5,137,0,0,262,264,3,16,8,0,263,261, - 1,0,0,0,264,267,1,0,0,0,265,263,1,0,0,0,265,266,1,0,0,0,266,15,1,0,0,0, - 267,265,1,0,0,0,268,273,5,140,0,0,269,271,5,5,0,0,270,269,1,0,0,0,270, - 271,1,0,0,0,271,272,1,0,0,0,272,274,3,126,63,0,273,270,1,0,0,0,273,274, - 1,0,0,0,274,17,1,0,0,0,275,276,5,100,0,0,276,281,3,28,14,0,277,278,5,137, - 0,0,278,280,3,28,14,0,279,277,1,0,0,0,280,283,1,0,0,0,281,279,1,0,0,0, - 281,282,1,0,0,0,282,284,1,0,0,0,283,281,1,0,0,0,284,285,5,139,0,0,285, - 19,1,0,0,0,286,288,5,57,0,0,287,289,5,91,0,0,288,287,1,0,0,0,288,289,1, - 0,0,0,289,290,1,0,0,0,290,291,5,58,0,0,291,292,3,126,63,0,292,293,5,135, - 0,0,293,298,3,46,23,0,294,295,5,137,0,0,295,297,3,46,23,0,296,294,1,0, - 0,0,297,300,1,0,0,0,298,296,1,0,0,0,298,299,1,0,0,0,299,305,1,0,0,0,300, - 298,1,0,0,0,301,302,5,137,0,0,302,304,3,56,28,0,303,301,1,0,0,0,304,307, - 1,0,0,0,305,303,1,0,0,0,305,306,1,0,0,0,306,308,1,0,0,0,307,305,1,0,0, - 0,308,309,5,136,0,0,309,21,1,0,0,0,310,311,5,57,0,0,311,312,5,98,0,0,312, - 324,3,126,63,0,313,314,5,135,0,0,314,319,3,126,63,0,315,316,5,137,0,0, - 316,318,3,126,63,0,317,315,1,0,0,0,318,321,1,0,0,0,319,317,1,0,0,0,319, - 320,1,0,0,0,320,322,1,0,0,0,321,319,1,0,0,0,322,323,5,136,0,0,323,325, - 1,0,0,0,324,313,1,0,0,0,324,325,1,0,0,0,325,326,1,0,0,0,326,327,5,5,0, - 0,327,328,3,70,35,0,328,23,1,0,0,0,329,330,5,57,0,0,330,331,5,99,0,0,331, - 333,3,126,63,0,332,334,3,26,13,0,333,332,1,0,0,0,333,334,1,0,0,0,334,335, - 1,0,0,0,335,336,5,5,0,0,336,337,3,36,18,0,337,25,1,0,0,0,338,339,5,135, - 0,0,339,344,3,28,14,0,340,341,5,137,0,0,341,343,3,28,14,0,342,340,1,0, - 0,0,343,346,1,0,0,0,344,342,1,0,0,0,344,345,1,0,0,0,345,347,1,0,0,0,346, - 344,1,0,0,0,347,348,5,136,0,0,348,358,1,0,0,0,349,354,3,28,14,0,350,351, - 5,137,0,0,351,353,3,28,14,0,352,350,1,0,0,0,353,356,1,0,0,0,354,352,1, - 0,0,0,354,355,1,0,0,0,355,358,1,0,0,0,356,354,1,0,0,0,357,338,1,0,0,0, - 357,349,1,0,0,0,358,27,1,0,0,0,359,360,3,30,15,0,360,361,3,50,25,0,361, - 29,1,0,0,0,362,365,3,126,63,0,363,365,5,141,0,0,364,362,1,0,0,0,364,363, - 1,0,0,0,365,31,1,0,0,0,366,367,5,64,0,0,367,368,5,58,0,0,368,369,3,126, - 63,0,369,370,3,34,17,0,370,33,1,0,0,0,371,373,5,67,0,0,372,374,5,69,0, - 0,373,372,1,0,0,0,373,374,1,0,0,0,374,375,1,0,0,0,375,429,3,46,23,0,376, - 377,5,67,0,0,377,429,3,56,28,0,378,380,5,64,0,0,379,381,5,69,0,0,380,379, - 1,0,0,0,380,381,1,0,0,0,381,382,1,0,0,0,382,383,3,126,63,0,383,387,3,50, - 25,0,384,386,3,54,27,0,385,384,1,0,0,0,386,389,1,0,0,0,387,385,1,0,0,0, - 387,388,1,0,0,0,388,429,1,0,0,0,389,387,1,0,0,0,390,392,5,64,0,0,391,393, - 5,69,0,0,392,391,1,0,0,0,392,393,1,0,0,0,393,394,1,0,0,0,394,395,3,126, - 63,0,395,396,5,83,0,0,396,397,5,84,0,0,397,398,3,112,56,0,398,429,1,0, - 0,0,399,401,5,64,0,0,400,402,5,69,0,0,401,400,1,0,0,0,401,402,1,0,0,0, - 402,403,1,0,0,0,403,404,3,126,63,0,404,405,5,68,0,0,405,406,5,84,0,0,406, - 429,1,0,0,0,407,408,5,68,0,0,408,409,5,69,0,0,409,429,3,126,63,0,410,411, - 5,68,0,0,411,412,5,75,0,0,412,429,3,126,63,0,413,414,5,65,0,0,414,415, - 5,66,0,0,415,429,3,126,63,0,416,417,5,65,0,0,417,418,5,69,0,0,418,419, - 3,126,63,0,419,420,5,66,0,0,420,421,3,126,63,0,421,429,1,0,0,0,422,423, - 5,65,0,0,423,424,5,90,0,0,424,425,3,126,63,0,425,426,5,66,0,0,426,427, - 3,126,63,0,427,429,1,0,0,0,428,371,1,0,0,0,428,376,1,0,0,0,428,378,1,0, - 0,0,428,390,1,0,0,0,428,399,1,0,0,0,428,407,1,0,0,0,428,410,1,0,0,0,428, - 413,1,0,0,0,428,416,1,0,0,0,428,422,1,0,0,0,429,35,1,0,0,0,430,436,3,70, - 35,0,431,436,3,64,32,0,432,436,3,8,4,0,433,436,3,12,6,0,434,436,3,20,10, - 0,435,430,1,0,0,0,435,431,1,0,0,0,435,432,1,0,0,0,435,433,1,0,0,0,435, - 434,1,0,0,0,436,37,1,0,0,0,437,439,5,57,0,0,438,440,5,86,0,0,439,438,1, - 0,0,0,439,440,1,0,0,0,440,441,1,0,0,0,441,442,5,90,0,0,442,443,3,126,63, - 0,443,444,5,25,0,0,444,445,3,126,63,0,445,446,5,135,0,0,446,451,3,42,21, - 0,447,448,5,137,0,0,448,450,3,42,21,0,449,447,1,0,0,0,450,453,1,0,0,0, - 451,449,1,0,0,0,451,452,1,0,0,0,452,454,1,0,0,0,453,451,1,0,0,0,454,457, - 5,136,0,0,455,456,5,92,0,0,456,458,3,44,22,0,457,455,1,0,0,0,457,458,1, - 0,0,0,458,39,1,0,0,0,459,460,5,68,0,0,460,461,5,58,0,0,461,475,3,126,63, - 0,462,463,5,68,0,0,463,464,5,90,0,0,464,465,3,126,63,0,465,466,5,25,0, - 0,466,467,3,126,63,0,467,475,1,0,0,0,468,469,5,68,0,0,469,470,5,99,0,0, - 470,475,3,126,63,0,471,472,5,68,0,0,472,473,5,98,0,0,473,475,3,126,63, - 0,474,459,1,0,0,0,474,462,1,0,0,0,474,468,1,0,0,0,474,471,1,0,0,0,475, - 41,1,0,0,0,476,478,3,126,63,0,477,479,7,1,0,0,478,477,1,0,0,0,478,479, - 1,0,0,0,479,43,1,0,0,0,480,486,5,73,0,0,481,482,5,95,0,0,482,486,5,107, - 0,0,483,484,5,96,0,0,484,486,5,107,0,0,485,480,1,0,0,0,485,481,1,0,0,0, - 485,483,1,0,0,0,486,45,1,0,0,0,487,488,3,126,63,0,488,490,3,50,25,0,489, - 491,3,48,24,0,490,489,1,0,0,0,490,491,1,0,0,0,491,495,1,0,0,0,492,494, - 3,54,27,0,493,492,1,0,0,0,494,497,1,0,0,0,495,493,1,0,0,0,495,496,1,0, - 0,0,496,47,1,0,0,0,497,495,1,0,0,0,498,499,5,5,0,0,499,500,5,135,0,0,500, - 501,3,112,56,0,501,502,5,136,0,0,502,49,1,0,0,0,503,506,3,126,63,0,504, - 506,5,88,0,0,505,503,1,0,0,0,505,504,1,0,0,0,506,508,1,0,0,0,507,509,3, - 126,63,0,508,507,1,0,0,0,508,509,1,0,0,0,509,511,1,0,0,0,510,512,3,126, - 63,0,511,510,1,0,0,0,511,512,1,0,0,0,512,521,1,0,0,0,513,514,5,135,0,0, - 514,517,3,52,26,0,515,516,5,137,0,0,516,518,3,52,26,0,517,515,1,0,0,0, - 517,518,1,0,0,0,518,519,1,0,0,0,519,520,5,136,0,0,520,522,1,0,0,0,521, - 513,1,0,0,0,521,522,1,0,0,0,522,51,1,0,0,0,523,525,5,127,0,0,524,523,1, - 0,0,0,524,525,1,0,0,0,525,526,1,0,0,0,526,527,5,143,0,0,527,53,1,0,0,0, - 528,529,5,8,0,0,529,598,5,107,0,0,530,598,5,107,0,0,531,532,5,84,0,0,532, - 598,3,112,56,0,533,534,5,92,0,0,534,598,7,2,0,0,535,536,5,75,0,0,536,538, - 3,126,63,0,537,535,1,0,0,0,537,538,1,0,0,0,538,539,1,0,0,0,539,540,5,97, - 0,0,540,541,5,135,0,0,541,542,3,58,29,0,542,543,5,136,0,0,543,598,1,0, - 0,0,544,545,5,75,0,0,545,547,3,126,63,0,546,544,1,0,0,0,546,547,1,0,0, - 0,547,548,1,0,0,0,548,549,5,73,0,0,549,551,5,74,0,0,550,552,3,136,68,0, - 551,550,1,0,0,0,551,552,1,0,0,0,552,598,1,0,0,0,553,554,5,75,0,0,554,556, - 3,126,63,0,555,553,1,0,0,0,555,556,1,0,0,0,556,557,1,0,0,0,557,559,5,86, - 0,0,558,560,3,136,68,0,559,558,1,0,0,0,559,560,1,0,0,0,560,598,1,0,0,0, - 561,562,5,75,0,0,562,564,3,126,63,0,563,561,1,0,0,0,563,564,1,0,0,0,564, - 565,1,0,0,0,565,566,5,77,0,0,566,578,3,126,63,0,567,568,5,135,0,0,568, - 573,3,126,63,0,569,570,5,137,0,0,570,572,3,126,63,0,571,569,1,0,0,0,572, - 575,1,0,0,0,573,571,1,0,0,0,573,574,1,0,0,0,574,576,1,0,0,0,575,573,1, - 0,0,0,576,577,5,136,0,0,577,579,1,0,0,0,578,567,1,0,0,0,578,579,1,0,0, - 0,579,583,1,0,0,0,580,582,3,60,30,0,581,580,1,0,0,0,582,585,1,0,0,0,583, - 581,1,0,0,0,583,584,1,0,0,0,584,598,1,0,0,0,585,583,1,0,0,0,586,595,5, - 88,0,0,587,588,5,135,0,0,588,591,3,52,26,0,589,590,5,137,0,0,590,592,3, - 52,26,0,591,589,1,0,0,0,591,592,1,0,0,0,592,593,1,0,0,0,593,594,5,136, - 0,0,594,596,1,0,0,0,595,587,1,0,0,0,595,596,1,0,0,0,596,598,1,0,0,0,597, - 528,1,0,0,0,597,530,1,0,0,0,597,531,1,0,0,0,597,533,1,0,0,0,597,537,1, - 0,0,0,597,546,1,0,0,0,597,555,1,0,0,0,597,563,1,0,0,0,597,586,1,0,0,0, - 598,55,1,0,0,0,599,600,5,75,0,0,600,602,3,126,63,0,601,599,1,0,0,0,601, - 602,1,0,0,0,602,603,1,0,0,0,603,604,5,73,0,0,604,606,5,74,0,0,605,607, - 3,136,68,0,606,605,1,0,0,0,606,607,1,0,0,0,607,608,1,0,0,0,608,609,5,135, - 0,0,609,614,3,126,63,0,610,611,5,137,0,0,611,613,3,126,63,0,612,610,1, - 0,0,0,613,616,1,0,0,0,614,612,1,0,0,0,614,615,1,0,0,0,615,617,1,0,0,0, - 616,614,1,0,0,0,617,618,5,136,0,0,618,689,1,0,0,0,619,620,5,75,0,0,620, - 622,3,126,63,0,621,619,1,0,0,0,621,622,1,0,0,0,622,623,1,0,0,0,623,625, - 5,86,0,0,624,626,3,136,68,0,625,624,1,0,0,0,625,626,1,0,0,0,626,627,1, - 0,0,0,627,628,5,135,0,0,628,633,3,126,63,0,629,630,5,137,0,0,630,632,3, - 126,63,0,631,629,1,0,0,0,632,635,1,0,0,0,633,631,1,0,0,0,633,634,1,0,0, - 0,634,636,1,0,0,0,635,633,1,0,0,0,636,637,5,136,0,0,637,689,1,0,0,0,638, - 639,5,75,0,0,639,641,3,126,63,0,640,638,1,0,0,0,640,641,1,0,0,0,641,642, - 1,0,0,0,642,643,5,76,0,0,643,646,5,74,0,0,644,645,5,85,0,0,645,647,5,90, - 0,0,646,644,1,0,0,0,646,647,1,0,0,0,647,648,1,0,0,0,648,649,5,135,0,0, - 649,654,3,126,63,0,650,651,5,137,0,0,651,653,3,126,63,0,652,650,1,0,0, - 0,653,656,1,0,0,0,654,652,1,0,0,0,654,655,1,0,0,0,655,657,1,0,0,0,656, - 654,1,0,0,0,657,658,5,136,0,0,658,659,5,77,0,0,659,671,3,126,63,0,660, - 661,5,135,0,0,661,666,3,126,63,0,662,663,5,137,0,0,663,665,3,126,63,0, - 664,662,1,0,0,0,665,668,1,0,0,0,666,664,1,0,0,0,666,667,1,0,0,0,667,669, - 1,0,0,0,668,666,1,0,0,0,669,670,5,136,0,0,670,672,1,0,0,0,671,660,1,0, - 0,0,671,672,1,0,0,0,672,676,1,0,0,0,673,675,3,60,30,0,674,673,1,0,0,0, - 675,678,1,0,0,0,676,674,1,0,0,0,676,677,1,0,0,0,677,689,1,0,0,0,678,676, - 1,0,0,0,679,680,5,75,0,0,680,682,3,126,63,0,681,679,1,0,0,0,681,682,1, - 0,0,0,682,683,1,0,0,0,683,684,5,97,0,0,684,685,5,135,0,0,685,686,3,58, - 29,0,686,687,5,136,0,0,687,689,1,0,0,0,688,601,1,0,0,0,688,621,1,0,0,0, - 688,640,1,0,0,0,688,681,1,0,0,0,689,57,1,0,0,0,690,696,8,3,0,0,691,692, - 5,135,0,0,692,693,3,58,29,0,693,694,5,136,0,0,694,696,1,0,0,0,695,690, - 1,0,0,0,695,691,1,0,0,0,696,699,1,0,0,0,697,695,1,0,0,0,697,698,1,0,0, - 0,698,59,1,0,0,0,699,697,1,0,0,0,700,701,5,25,0,0,701,702,5,79,0,0,702, - 707,3,62,31,0,703,704,5,25,0,0,704,705,5,78,0,0,705,707,3,62,31,0,706, - 700,1,0,0,0,706,703,1,0,0,0,707,61,1,0,0,0,708,717,5,80,0,0,709,710,5, - 85,0,0,710,717,5,82,0,0,711,717,5,81,0,0,712,713,5,83,0,0,713,717,5,107, - 0,0,714,715,5,83,0,0,715,717,5,84,0,0,716,708,1,0,0,0,716,709,1,0,0,0, - 716,711,1,0,0,0,716,712,1,0,0,0,716,714,1,0,0,0,717,63,1,0,0,0,718,719, - 5,70,0,0,719,720,5,71,0,0,720,748,3,126,63,0,721,722,5,135,0,0,722,727, - 3,126,63,0,723,724,5,137,0,0,724,726,3,126,63,0,725,723,1,0,0,0,726,729, - 1,0,0,0,727,725,1,0,0,0,727,728,1,0,0,0,728,730,1,0,0,0,729,727,1,0,0, - 0,730,731,5,136,0,0,731,733,1,0,0,0,732,721,1,0,0,0,732,733,1,0,0,0,733, - 744,1,0,0,0,734,735,5,72,0,0,735,740,3,66,33,0,736,737,5,137,0,0,737,739, - 3,66,33,0,738,736,1,0,0,0,739,742,1,0,0,0,740,738,1,0,0,0,740,741,1,0, - 0,0,741,745,1,0,0,0,742,740,1,0,0,0,743,745,3,70,35,0,744,734,1,0,0,0, - 744,743,1,0,0,0,745,749,1,0,0,0,746,747,5,84,0,0,747,749,5,72,0,0,748, - 732,1,0,0,0,748,746,1,0,0,0,749,65,1,0,0,0,750,751,5,135,0,0,751,756,3, - 68,34,0,752,753,5,137,0,0,753,755,3,68,34,0,754,752,1,0,0,0,755,758,1, - 0,0,0,756,754,1,0,0,0,756,757,1,0,0,0,757,759,1,0,0,0,758,756,1,0,0,0, - 759,760,5,136,0,0,760,67,1,0,0,0,761,764,5,84,0,0,762,764,3,112,56,0,763, - 761,1,0,0,0,763,762,1,0,0,0,764,69,1,0,0,0,765,771,3,72,36,0,766,767,3, - 74,37,0,767,768,3,72,36,0,768,770,1,0,0,0,769,766,1,0,0,0,770,773,1,0, - 0,0,771,769,1,0,0,0,771,772,1,0,0,0,772,775,1,0,0,0,773,771,1,0,0,0,774, - 776,3,108,54,0,775,774,1,0,0,0,775,776,1,0,0,0,776,778,1,0,0,0,777,779, - 3,88,44,0,778,777,1,0,0,0,778,779,1,0,0,0,779,71,1,0,0,0,780,795,3,76, - 38,0,781,782,5,135,0,0,782,783,3,70,35,0,783,784,5,136,0,0,784,795,1,0, - 0,0,785,786,5,72,0,0,786,791,3,66,33,0,787,788,5,137,0,0,788,790,3,66, - 33,0,789,787,1,0,0,0,790,793,1,0,0,0,791,789,1,0,0,0,791,792,1,0,0,0,792, - 795,1,0,0,0,793,791,1,0,0,0,794,780,1,0,0,0,794,781,1,0,0,0,794,785,1, - 0,0,0,795,73,1,0,0,0,796,798,5,53,0,0,797,799,5,54,0,0,798,797,1,0,0,0, - 798,799,1,0,0,0,799,803,1,0,0,0,800,803,5,55,0,0,801,803,5,56,0,0,802, - 796,1,0,0,0,802,800,1,0,0,0,802,801,1,0,0,0,803,75,1,0,0,0,804,806,5,1, - 0,0,805,807,3,78,39,0,806,805,1,0,0,0,806,807,1,0,0,0,807,809,1,0,0,0, - 808,810,3,84,42,0,809,808,1,0,0,0,809,810,1,0,0,0,810,811,1,0,0,0,811, - 814,3,92,46,0,812,813,5,71,0,0,813,815,3,126,63,0,814,812,1,0,0,0,814, - 815,1,0,0,0,815,817,1,0,0,0,816,818,3,96,48,0,817,816,1,0,0,0,817,818, - 1,0,0,0,818,820,1,0,0,0,819,821,3,106,53,0,820,819,1,0,0,0,820,821,1,0, - 0,0,821,823,1,0,0,0,822,824,3,80,40,0,823,822,1,0,0,0,823,824,1,0,0,0, - 824,826,1,0,0,0,825,827,3,82,41,0,826,825,1,0,0,0,826,827,1,0,0,0,827, - 77,1,0,0,0,828,829,7,4,0,0,829,79,1,0,0,0,830,831,5,27,0,0,831,832,5,29, - 0,0,832,837,3,112,56,0,833,834,5,137,0,0,834,836,3,112,56,0,835,833,1, - 0,0,0,836,839,1,0,0,0,837,835,1,0,0,0,837,838,1,0,0,0,838,81,1,0,0,0,839, - 837,1,0,0,0,840,841,5,30,0,0,841,842,3,112,56,0,842,83,1,0,0,0,843,844, - 5,4,0,0,844,849,3,86,43,0,845,846,7,5,0,0,846,848,3,86,43,0,847,845,1, - 0,0,0,848,851,1,0,0,0,849,847,1,0,0,0,849,850,1,0,0,0,850,853,1,0,0,0, - 851,849,1,0,0,0,852,854,5,36,0,0,853,852,1,0,0,0,853,854,1,0,0,0,854,85, - 1,0,0,0,855,862,5,143,0,0,856,862,5,141,0,0,857,858,5,135,0,0,858,859, - 3,112,56,0,859,860,5,136,0,0,860,862,1,0,0,0,861,855,1,0,0,0,861,856,1, - 0,0,0,861,857,1,0,0,0,862,87,1,0,0,0,863,864,5,45,0,0,864,865,3,112,56, - 0,865,872,3,90,45,0,866,867,5,46,0,0,867,868,7,6,0,0,868,869,3,112,56, - 0,869,870,3,90,45,0,870,871,5,51,0,0,871,873,1,0,0,0,872,866,1,0,0,0,872, - 873,1,0,0,0,873,881,1,0,0,0,874,875,5,46,0,0,875,876,7,6,0,0,876,877,3, - 112,56,0,877,878,3,90,45,0,878,879,5,51,0,0,879,881,1,0,0,0,880,863,1, - 0,0,0,880,874,1,0,0,0,881,89,1,0,0,0,882,883,7,7,0,0,883,91,1,0,0,0,884, - 894,5,122,0,0,885,890,3,94,47,0,886,887,5,137,0,0,887,889,3,94,47,0,888, - 886,1,0,0,0,889,892,1,0,0,0,890,888,1,0,0,0,890,891,1,0,0,0,891,894,1, - 0,0,0,892,890,1,0,0,0,893,884,1,0,0,0,893,885,1,0,0,0,894,93,1,0,0,0,895, - 896,3,126,63,0,896,897,5,138,0,0,897,898,5,122,0,0,898,907,1,0,0,0,899, - 904,3,112,56,0,900,902,5,5,0,0,901,900,1,0,0,0,901,902,1,0,0,0,902,903, - 1,0,0,0,903,905,3,126,63,0,904,901,1,0,0,0,904,905,1,0,0,0,905,907,1,0, - 0,0,906,895,1,0,0,0,906,899,1,0,0,0,907,95,1,0,0,0,908,909,5,2,0,0,909, - 914,3,98,49,0,910,911,5,137,0,0,911,913,3,98,49,0,912,910,1,0,0,0,913, - 916,1,0,0,0,914,912,1,0,0,0,914,915,1,0,0,0,915,97,1,0,0,0,916,914,1,0, - 0,0,917,921,3,100,50,0,918,920,3,102,51,0,919,918,1,0,0,0,920,923,1,0, - 0,0,921,919,1,0,0,0,921,922,1,0,0,0,922,99,1,0,0,0,923,921,1,0,0,0,924, - 929,3,126,63,0,925,927,5,5,0,0,926,925,1,0,0,0,926,927,1,0,0,0,927,928, - 1,0,0,0,928,930,3,126,63,0,929,926,1,0,0,0,929,930,1,0,0,0,930,945,1,0, - 0,0,931,932,5,135,0,0,932,933,3,70,35,0,933,938,5,136,0,0,934,936,5,5, - 0,0,935,934,1,0,0,0,935,936,1,0,0,0,936,937,1,0,0,0,937,939,3,126,63,0, - 938,935,1,0,0,0,938,939,1,0,0,0,939,945,1,0,0,0,940,941,5,135,0,0,941, - 942,3,98,49,0,942,943,5,136,0,0,943,945,1,0,0,0,944,924,1,0,0,0,944,931, - 1,0,0,0,944,940,1,0,0,0,945,101,1,0,0,0,946,947,3,104,52,0,947,948,5,23, - 0,0,948,949,3,100,50,0,949,950,5,25,0,0,950,951,3,112,56,0,951,962,1,0, - 0,0,952,953,5,37,0,0,953,954,5,23,0,0,954,962,3,100,50,0,955,956,5,37, - 0,0,956,957,5,38,0,0,957,962,3,100,50,0,958,959,5,22,0,0,959,960,5,38, - 0,0,960,962,3,100,50,0,961,946,1,0,0,0,961,952,1,0,0,0,961,955,1,0,0,0, - 961,958,1,0,0,0,962,103,1,0,0,0,963,965,5,18,0,0,964,963,1,0,0,0,964,965, - 1,0,0,0,965,979,1,0,0,0,966,968,5,19,0,0,967,969,5,22,0,0,968,967,1,0, - 0,0,968,969,1,0,0,0,969,979,1,0,0,0,970,972,5,20,0,0,971,973,5,22,0,0, - 972,971,1,0,0,0,972,973,1,0,0,0,973,979,1,0,0,0,974,976,5,21,0,0,975,977, - 5,22,0,0,976,975,1,0,0,0,976,977,1,0,0,0,977,979,1,0,0,0,978,964,1,0,0, - 0,978,966,1,0,0,0,978,970,1,0,0,0,978,974,1,0,0,0,979,105,1,0,0,0,980, - 981,5,3,0,0,981,982,3,112,56,0,982,107,1,0,0,0,983,984,5,26,0,0,984,985, - 5,29,0,0,985,990,3,110,55,0,986,987,5,137,0,0,987,989,3,110,55,0,988,986, - 1,0,0,0,989,992,1,0,0,0,990,988,1,0,0,0,990,991,1,0,0,0,991,109,1,0,0, - 0,992,990,1,0,0,0,993,995,3,112,56,0,994,996,7,1,0,0,995,994,1,0,0,0,995, - 996,1,0,0,0,996,111,1,0,0,0,997,998,6,56,-1,0,998,999,7,5,0,0,999,1004, - 3,112,56,21,1000,1001,7,8,0,0,1001,1004,3,112,56,7,1002,1004,3,114,57, - 0,1003,997,1,0,0,0,1003,1000,1,0,0,0,1003,1002,1,0,0,0,1004,1103,1,0,0, - 0,1005,1006,10,22,0,0,1006,1007,5,125,0,0,1007,1102,3,112,56,23,1008,1009, - 10,20,0,0,1009,1010,7,9,0,0,1010,1102,3,112,56,21,1011,1012,10,19,0,0, - 1012,1013,5,124,0,0,1013,1102,3,112,56,20,1014,1015,10,18,0,0,1015,1016, - 5,17,0,0,1016,1102,3,112,56,19,1017,1018,10,17,0,0,1018,1019,7,5,0,0,1019, - 1102,3,112,56,18,1020,1021,10,16,0,0,1021,1022,5,128,0,0,1022,1102,3,112, - 56,17,1023,1024,10,15,0,0,1024,1025,7,10,0,0,1025,1102,3,112,56,16,1026, - 1028,10,14,0,0,1027,1029,5,8,0,0,1028,1027,1,0,0,0,1028,1029,1,0,0,0,1029, - 1030,1,0,0,0,1030,1031,5,52,0,0,1031,1032,3,112,56,0,1032,1033,5,6,0,0, - 1033,1034,3,112,56,15,1034,1102,1,0,0,0,1035,1037,10,13,0,0,1036,1038, - 5,8,0,0,1037,1036,1,0,0,0,1037,1038,1,0,0,0,1038,1039,1,0,0,0,1039,1040, - 5,16,0,0,1040,1102,3,112,56,14,1041,1042,10,8,0,0,1042,1044,5,28,0,0,1043, - 1045,5,8,0,0,1044,1043,1,0,0,0,1044,1045,1,0,0,0,1045,1046,1,0,0,0,1046, - 1047,5,35,0,0,1047,1048,5,2,0,0,1048,1102,3,112,56,9,1049,1050,10,6,0, - 0,1050,1051,7,11,0,0,1051,1102,3,112,56,7,1052,1053,10,5,0,0,1053,1054, - 7,12,0,0,1054,1102,3,112,56,6,1055,1056,10,4,0,0,1056,1057,7,13,0,0,1057, - 1102,3,112,56,5,1058,1059,10,3,0,0,1059,1060,5,10,0,0,1060,1102,3,112, - 56,4,1061,1062,10,2,0,0,1062,1063,5,11,0,0,1063,1102,3,112,56,3,1064,1066, - 10,12,0,0,1065,1067,5,8,0,0,1066,1065,1,0,0,0,1066,1067,1,0,0,0,1067,1068, - 1,0,0,0,1068,1069,5,24,0,0,1069,1070,5,135,0,0,1070,1071,3,70,35,0,1071, - 1072,5,136,0,0,1072,1102,1,0,0,0,1073,1075,10,11,0,0,1074,1076,5,8,0,0, - 1075,1074,1,0,0,0,1075,1076,1,0,0,0,1076,1077,1,0,0,0,1077,1078,5,24,0, - 0,1078,1079,5,135,0,0,1079,1084,3,112,56,0,1080,1081,5,137,0,0,1081,1083, - 3,112,56,0,1082,1080,1,0,0,0,1083,1086,1,0,0,0,1084,1082,1,0,0,0,1084, - 1085,1,0,0,0,1085,1087,1,0,0,0,1086,1084,1,0,0,0,1087,1088,5,136,0,0,1088, - 1102,1,0,0,0,1089,1090,10,10,0,0,1090,1092,5,28,0,0,1091,1093,5,8,0,0, - 1092,1091,1,0,0,0,1092,1093,1,0,0,0,1093,1094,1,0,0,0,1094,1102,5,107, - 0,0,1095,1096,10,9,0,0,1096,1098,5,28,0,0,1097,1099,5,8,0,0,1098,1097, - 1,0,0,0,1098,1099,1,0,0,0,1099,1100,1,0,0,0,1100,1102,7,14,0,0,1101,1005, - 1,0,0,0,1101,1008,1,0,0,0,1101,1011,1,0,0,0,1101,1014,1,0,0,0,1101,1017, - 1,0,0,0,1101,1020,1,0,0,0,1101,1023,1,0,0,0,1101,1026,1,0,0,0,1101,1035, - 1,0,0,0,1101,1041,1,0,0,0,1101,1049,1,0,0,0,1101,1052,1,0,0,0,1101,1055, - 1,0,0,0,1101,1058,1,0,0,0,1101,1061,1,0,0,0,1101,1064,1,0,0,0,1101,1073, - 1,0,0,0,1101,1089,1,0,0,0,1101,1095,1,0,0,0,1102,1105,1,0,0,0,1103,1101, - 1,0,0,0,1103,1104,1,0,0,0,1104,113,1,0,0,0,1105,1103,1,0,0,0,1106,1126, - 3,128,64,0,1107,1126,3,116,58,0,1108,1126,3,120,60,0,1109,1126,3,124,62, - 0,1110,1126,5,141,0,0,1111,1126,5,140,0,0,1112,1113,5,31,0,0,1113,1114, - 5,135,0,0,1114,1115,3,70,35,0,1115,1116,5,136,0,0,1116,1126,1,0,0,0,1117, - 1118,5,135,0,0,1118,1119,3,70,35,0,1119,1120,5,136,0,0,1120,1126,1,0,0, - 0,1121,1122,5,135,0,0,1122,1123,3,112,56,0,1123,1124,5,136,0,0,1124,1126, - 1,0,0,0,1125,1106,1,0,0,0,1125,1107,1,0,0,0,1125,1108,1,0,0,0,1125,1109, - 1,0,0,0,1125,1110,1,0,0,0,1125,1111,1,0,0,0,1125,1112,1,0,0,0,1125,1117, - 1,0,0,0,1125,1121,1,0,0,0,1126,115,1,0,0,0,1127,1129,5,41,0,0,1128,1130, - 3,112,56,0,1129,1128,1,0,0,0,1129,1130,1,0,0,0,1130,1132,1,0,0,0,1131, - 1133,3,118,59,0,1132,1131,1,0,0,0,1133,1134,1,0,0,0,1134,1132,1,0,0,0, - 1134,1135,1,0,0,0,1135,1138,1,0,0,0,1136,1137,5,43,0,0,1137,1139,3,112, - 56,0,1138,1136,1,0,0,0,1138,1139,1,0,0,0,1139,1140,1,0,0,0,1140,1141,5, - 44,0,0,1141,117,1,0,0,0,1142,1143,5,42,0,0,1143,1144,3,112,56,0,1144,1145, - 5,33,0,0,1145,1146,3,112,56,0,1146,119,1,0,0,0,1147,1148,3,122,61,0,1148, - 1161,5,135,0,0,1149,1162,5,122,0,0,1150,1152,5,35,0,0,1151,1150,1,0,0, - 0,1151,1152,1,0,0,0,1152,1153,1,0,0,0,1153,1158,3,112,56,0,1154,1155,5, - 137,0,0,1155,1157,3,112,56,0,1156,1154,1,0,0,0,1157,1160,1,0,0,0,1158, - 1156,1,0,0,0,1158,1159,1,0,0,0,1159,1162,1,0,0,0,1160,1158,1,0,0,0,1161, - 1149,1,0,0,0,1161,1151,1,0,0,0,1161,1162,1,0,0,0,1162,1163,1,0,0,0,1163, - 1165,5,136,0,0,1164,1166,3,148,74,0,1165,1164,1,0,0,0,1165,1166,1,0,0, - 0,1166,1168,1,0,0,0,1167,1169,3,146,73,0,1168,1167,1,0,0,0,1168,1169,1, - 0,0,0,1169,1178,1,0,0,0,1170,1172,3,150,75,0,1171,1170,1,0,0,0,1171,1172, - 1,0,0,0,1172,1174,1,0,0,0,1173,1175,3,152,76,0,1174,1173,1,0,0,0,1174, - 1175,1,0,0,0,1175,1176,1,0,0,0,1176,1177,5,39,0,0,1177,1179,3,134,67,0, - 1178,1171,1,0,0,0,1178,1179,1,0,0,0,1179,121,1,0,0,0,1180,1187,3,126,63, - 0,1181,1187,5,19,0,0,1182,1187,5,20,0,0,1183,1187,5,103,0,0,1184,1187, - 5,48,0,0,1185,1187,5,40,0,0,1186,1180,1,0,0,0,1186,1181,1,0,0,0,1186,1182, - 1,0,0,0,1186,1183,1,0,0,0,1186,1184,1,0,0,0,1186,1185,1,0,0,0,1187,123, - 1,0,0,0,1188,1189,3,126,63,0,1189,1190,5,138,0,0,1190,1192,1,0,0,0,1191, - 1188,1,0,0,0,1191,1192,1,0,0,0,1192,1193,1,0,0,0,1193,1194,3,126,63,0, - 1194,125,1,0,0,0,1195,1200,5,150,0,0,1196,1200,5,148,0,0,1197,1200,5,149, - 0,0,1198,1200,3,144,72,0,1199,1195,1,0,0,0,1199,1196,1,0,0,0,1199,1197, - 1,0,0,0,1199,1198,1,0,0,0,1200,127,1,0,0,0,1201,1211,5,143,0,0,1202,1211, - 5,144,0,0,1203,1211,5,142,0,0,1204,1211,5,145,0,0,1205,1211,5,146,0,0, - 1206,1211,5,147,0,0,1207,1211,5,105,0,0,1208,1211,5,106,0,0,1209,1211, - 5,107,0,0,1210,1201,1,0,0,0,1210,1202,1,0,0,0,1210,1203,1,0,0,0,1210,1204, - 1,0,0,0,1210,1205,1,0,0,0,1210,1206,1,0,0,0,1210,1207,1,0,0,0,1210,1208, - 1,0,0,0,1210,1209,1,0,0,0,1211,129,1,0,0,0,1212,1214,5,59,0,0,1213,1215, - 7,15,0,0,1214,1213,1,0,0,0,1214,1215,1,0,0,0,1215,1225,1,0,0,0,1216,1218, - 5,60,0,0,1217,1219,7,15,0,0,1218,1217,1,0,0,0,1218,1219,1,0,0,0,1219,1225, - 1,0,0,0,1220,1222,5,61,0,0,1221,1223,7,15,0,0,1222,1221,1,0,0,0,1222,1223, - 1,0,0,0,1223,1225,1,0,0,0,1224,1212,1,0,0,0,1224,1216,1,0,0,0,1224,1220, - 1,0,0,0,1225,131,1,0,0,0,1226,1227,3,112,56,0,1227,1228,5,0,0,1,1228,133, - 1,0,0,0,1229,1240,5,135,0,0,1230,1231,5,40,0,0,1231,1232,5,29,0,0,1232, - 1237,3,112,56,0,1233,1234,5,137,0,0,1234,1236,3,112,56,0,1235,1233,1,0, - 0,0,1236,1239,1,0,0,0,1237,1235,1,0,0,0,1237,1238,1,0,0,0,1238,1241,1, - 0,0,0,1239,1237,1,0,0,0,1240,1230,1,0,0,0,1240,1241,1,0,0,0,1241,1243, - 1,0,0,0,1242,1244,3,108,54,0,1243,1242,1,0,0,0,1243,1244,1,0,0,0,1244, - 1246,1,0,0,0,1245,1247,3,138,69,0,1246,1245,1,0,0,0,1246,1247,1,0,0,0, - 1247,1248,1,0,0,0,1248,1249,5,136,0,0,1249,135,1,0,0,0,1250,1251,7,16, - 0,0,1251,137,1,0,0,0,1252,1259,7,17,0,0,1253,1254,5,52,0,0,1254,1255,3, - 140,70,0,1255,1256,5,6,0,0,1256,1257,3,140,70,0,1257,1260,1,0,0,0,1258, - 1260,3,140,70,0,1259,1253,1,0,0,0,1259,1258,1,0,0,0,1260,1263,1,0,0,0, - 1261,1262,5,114,0,0,1262,1264,3,142,71,0,1263,1261,1,0,0,0,1263,1264,1, - 0,0,0,1264,139,1,0,0,0,1265,1266,5,110,0,0,1266,1273,7,18,0,0,1267,1268, - 5,113,0,0,1268,1273,5,50,0,0,1269,1270,3,112,56,0,1270,1271,7,18,0,0,1271, - 1273,1,0,0,0,1272,1265,1,0,0,0,1272,1267,1,0,0,0,1272,1269,1,0,0,0,1273, - 141,1,0,0,0,1274,1275,5,113,0,0,1275,1281,5,50,0,0,1276,1281,5,27,0,0, - 1277,1281,5,115,0,0,1278,1279,5,85,0,0,1279,1281,5,116,0,0,1280,1274,1, - 0,0,0,1280,1276,1,0,0,0,1280,1277,1,0,0,0,1280,1278,1,0,0,0,1281,143,1, - 0,0,0,1282,1283,7,19,0,0,1283,145,1,0,0,0,1284,1285,5,121,0,0,1285,1286, - 5,135,0,0,1286,1287,5,3,0,0,1287,1288,3,112,56,0,1288,1289,5,136,0,0,1289, - 147,1,0,0,0,1290,1291,5,117,0,0,1291,1292,5,27,0,0,1292,1293,5,135,0,0, - 1293,1294,3,108,54,0,1294,1295,5,136,0,0,1295,149,1,0,0,0,1296,1297,5, - 2,0,0,1297,1298,7,20,0,0,1298,151,1,0,0,0,1299,1300,7,21,0,0,1300,1301, - 5,120,0,0,1301,153,1,0,0,0,164,155,171,174,180,200,209,212,220,229,233, - 245,253,257,265,270,273,281,288,298,305,319,324,333,344,354,357,364,373, - 380,387,392,401,428,435,439,451,457,474,478,485,490,495,505,508,511,517, - 521,524,537,546,551,555,559,563,573,578,583,591,595,597,601,606,614,621, - 625,633,640,646,654,666,671,676,681,688,695,697,706,716,727,732,740,744, - 748,756,763,771,775,778,791,794,798,802,806,809,814,817,820,823,826,837, - 849,853,861,872,880,890,893,901,904,906,914,921,926,929,935,938,944,961, - 964,968,972,976,978,990,995,1003,1028,1037,1044,1066,1075,1084,1092,1098, - 1101,1103,1125,1129,1134,1138,1151,1158,1161,1165,1168,1171,1174,1178, - 1186,1191,1199,1210,1214,1218,1222,1224,1237,1240,1243,1246,1259,1263, - 1272,1280 + 124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,0, + 26,1,0,101,102,1,0,103,104,1,0,93,94,1,0,137,138,2,0,34,35,54,54,1,0,128, + 129,1,0,47,48,1,0,49,50,2,0,8,8,15,15,1,0,124,125,1,0,131,136,2,0,6,6, + 12,12,2,0,7,7,13,13,2,0,9,9,14,14,1,0,105,106,1,0,140,141,3,0,25,25,105, + 105,107,107,3,0,85,85,106,106,108,108,1,0,62,63,2,0,87,87,89,89,2,0,49, + 49,110,111,1,0,113,114,2,0,107,108,110,123,2,0,1,106,109,109,2,0,48,48, + 120,120,2,0,96,96,121,121,1498,0,159,1,0,0,0,2,182,1,0,0,0,4,204,1,0,0, + 0,6,206,1,0,0,0,8,218,1,0,0,0,10,239,1,0,0,0,12,243,1,0,0,0,14,263,1,0, + 0,0,16,272,1,0,0,0,18,279,1,0,0,0,20,290,1,0,0,0,22,314,1,0,0,0,24,333, + 1,0,0,0,26,361,1,0,0,0,28,363,1,0,0,0,30,368,1,0,0,0,32,370,1,0,0,0,34, + 432,1,0,0,0,36,439,1,0,0,0,38,441,1,0,0,0,40,478,1,0,0,0,42,480,1,0,0, + 0,44,489,1,0,0,0,46,491,1,0,0,0,48,502,1,0,0,0,50,509,1,0,0,0,52,528,1, + 0,0,0,54,601,1,0,0,0,56,692,1,0,0,0,58,701,1,0,0,0,60,710,1,0,0,0,62,720, + 1,0,0,0,64,722,1,0,0,0,66,754,1,0,0,0,68,767,1,0,0,0,70,769,1,0,0,0,72, + 798,1,0,0,0,74,806,1,0,0,0,76,808,1,0,0,0,78,832,1,0,0,0,80,834,1,0,0, + 0,82,844,1,0,0,0,84,847,1,0,0,0,86,865,1,0,0,0,88,884,1,0,0,0,90,886,1, + 0,0,0,92,897,1,0,0,0,94,910,1,0,0,0,96,912,1,0,0,0,98,921,1,0,0,0,100, + 948,1,0,0,0,102,965,1,0,0,0,104,982,1,0,0,0,106,984,1,0,0,0,108,987,1, + 0,0,0,110,997,1,0,0,0,112,1007,1,0,0,0,114,1129,1,0,0,0,116,1131,1,0,0, + 0,118,1146,1,0,0,0,120,1151,1,0,0,0,122,1190,1,0,0,0,124,1192,1,0,0,0, + 126,1202,1,0,0,0,128,1208,1,0,0,0,130,1219,1,0,0,0,132,1233,1,0,0,0,134, + 1235,1,0,0,0,136,1238,1,0,0,0,138,1259,1,0,0,0,140,1261,1,0,0,0,142,1281, + 1,0,0,0,144,1289,1,0,0,0,146,1291,1,0,0,0,148,1293,1,0,0,0,150,1295,1, + 0,0,0,152,1301,1,0,0,0,154,1307,1,0,0,0,156,1310,1,0,0,0,158,160,3,18, + 9,0,159,158,1,0,0,0,159,160,1,0,0,0,160,175,1,0,0,0,161,176,3,2,1,0,162, + 176,3,20,10,0,163,176,3,38,19,0,164,176,3,22,11,0,165,176,3,24,12,0,166, + 176,3,32,16,0,167,176,3,40,20,0,168,176,3,64,32,0,169,176,3,8,4,0,170, + 176,3,12,6,0,171,176,3,132,66,0,172,176,3,6,3,0,173,176,3,14,7,0,174,176, + 3,70,35,0,175,161,1,0,0,0,175,162,1,0,0,0,175,163,1,0,0,0,175,164,1,0, + 0,0,175,165,1,0,0,0,175,166,1,0,0,0,175,167,1,0,0,0,175,168,1,0,0,0,175, + 169,1,0,0,0,175,170,1,0,0,0,175,171,1,0,0,0,175,172,1,0,0,0,175,173,1, + 0,0,0,175,174,1,0,0,0,176,178,1,0,0,0,177,179,5,142,0,0,178,177,1,0,0, + 0,178,179,1,0,0,0,179,180,1,0,0,0,180,181,5,0,0,1,181,1,1,0,0,0,182,184, + 5,32,0,0,183,185,5,8,0,0,184,183,1,0,0,0,184,185,1,0,0,0,185,186,1,0,0, + 0,186,187,5,31,0,0,187,188,5,137,0,0,188,189,3,70,35,0,189,190,5,138,0, + 0,190,191,5,33,0,0,191,192,3,4,2,0,192,3,1,0,0,0,193,205,3,20,10,0,194, + 205,3,38,19,0,195,205,3,22,11,0,196,205,3,24,12,0,197,205,3,32,16,0,198, + 205,3,40,20,0,199,205,3,64,32,0,200,205,3,8,4,0,201,205,3,12,6,0,202,205, + 3,6,3,0,203,205,3,70,35,0,204,193,1,0,0,0,204,194,1,0,0,0,204,195,1,0, + 0,0,204,196,1,0,0,0,204,197,1,0,0,0,204,198,1,0,0,0,204,199,1,0,0,0,204, + 200,1,0,0,0,204,201,1,0,0,0,204,202,1,0,0,0,204,203,1,0,0,0,205,5,1,0, + 0,0,206,207,7,0,0,0,207,216,3,128,64,0,208,213,3,112,56,0,209,210,5,139, + 0,0,210,212,3,112,56,0,211,209,1,0,0,0,212,215,1,0,0,0,213,211,1,0,0,0, + 213,214,1,0,0,0,214,217,1,0,0,0,215,213,1,0,0,0,216,208,1,0,0,0,216,217, + 1,0,0,0,217,7,1,0,0,0,218,219,5,79,0,0,219,224,3,98,49,0,220,221,5,139, + 0,0,221,223,3,98,49,0,222,220,1,0,0,0,223,226,1,0,0,0,224,222,1,0,0,0, + 224,225,1,0,0,0,225,227,1,0,0,0,226,224,1,0,0,0,227,228,5,83,0,0,228,233, + 3,10,5,0,229,230,5,139,0,0,230,232,3,10,5,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,237,1,0,0,0,235,233,1,0,0,0, + 236,238,3,106,53,0,237,236,1,0,0,0,237,238,1,0,0,0,238,9,1,0,0,0,239,240, + 3,124,62,0,240,241,5,131,0,0,241,242,3,112,56,0,242,11,1,0,0,0,243,249, + 5,78,0,0,244,245,3,128,64,0,245,246,5,140,0,0,246,247,5,124,0,0,247,250, + 1,0,0,0,248,250,5,124,0,0,249,244,1,0,0,0,249,248,1,0,0,0,249,250,1,0, + 0,0,250,251,1,0,0,0,251,252,5,2,0,0,252,257,3,98,49,0,253,254,5,139,0, + 0,254,256,3,98,49,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,261,1,0,0,0,259,257,1,0,0,0,260,262,3,106,53,0,261,260, + 1,0,0,0,261,262,1,0,0,0,262,13,1,0,0,0,263,264,5,1,0,0,264,269,3,16,8, + 0,265,266,5,139,0,0,266,268,3,16,8,0,267,265,1,0,0,0,268,271,1,0,0,0,269, + 267,1,0,0,0,269,270,1,0,0,0,270,15,1,0,0,0,271,269,1,0,0,0,272,277,5,143, + 0,0,273,275,5,5,0,0,274,273,1,0,0,0,274,275,1,0,0,0,275,276,1,0,0,0,276, + 278,3,128,64,0,277,274,1,0,0,0,277,278,1,0,0,0,278,17,1,0,0,0,279,280, + 5,100,0,0,280,285,3,28,14,0,281,282,5,139,0,0,282,284,3,28,14,0,283,281, + 1,0,0,0,284,287,1,0,0,0,285,283,1,0,0,0,285,286,1,0,0,0,286,288,1,0,0, + 0,287,285,1,0,0,0,288,289,5,142,0,0,289,19,1,0,0,0,290,292,5,57,0,0,291, + 293,5,91,0,0,292,291,1,0,0,0,292,293,1,0,0,0,293,294,1,0,0,0,294,295,5, + 58,0,0,295,296,3,128,64,0,296,297,5,137,0,0,297,302,3,46,23,0,298,299, + 5,139,0,0,299,301,3,46,23,0,300,298,1,0,0,0,301,304,1,0,0,0,302,300,1, + 0,0,0,302,303,1,0,0,0,303,309,1,0,0,0,304,302,1,0,0,0,305,306,5,139,0, + 0,306,308,3,56,28,0,307,305,1,0,0,0,308,311,1,0,0,0,309,307,1,0,0,0,309, + 310,1,0,0,0,310,312,1,0,0,0,311,309,1,0,0,0,312,313,5,138,0,0,313,21,1, + 0,0,0,314,315,5,57,0,0,315,316,5,98,0,0,316,328,3,128,64,0,317,318,5,137, + 0,0,318,323,3,128,64,0,319,320,5,139,0,0,320,322,3,128,64,0,321,319,1, + 0,0,0,322,325,1,0,0,0,323,321,1,0,0,0,323,324,1,0,0,0,324,326,1,0,0,0, + 325,323,1,0,0,0,326,327,5,138,0,0,327,329,1,0,0,0,328,317,1,0,0,0,328, + 329,1,0,0,0,329,330,1,0,0,0,330,331,5,5,0,0,331,332,3,70,35,0,332,23,1, + 0,0,0,333,334,5,57,0,0,334,335,5,99,0,0,335,337,3,128,64,0,336,338,3,26, + 13,0,337,336,1,0,0,0,337,338,1,0,0,0,338,339,1,0,0,0,339,340,5,5,0,0,340, + 341,3,36,18,0,341,25,1,0,0,0,342,343,5,137,0,0,343,348,3,28,14,0,344,345, + 5,139,0,0,345,347,3,28,14,0,346,344,1,0,0,0,347,350,1,0,0,0,348,346,1, + 0,0,0,348,349,1,0,0,0,349,351,1,0,0,0,350,348,1,0,0,0,351,352,5,138,0, + 0,352,362,1,0,0,0,353,358,3,28,14,0,354,355,5,139,0,0,355,357,3,28,14, + 0,356,354,1,0,0,0,357,360,1,0,0,0,358,356,1,0,0,0,358,359,1,0,0,0,359, + 362,1,0,0,0,360,358,1,0,0,0,361,342,1,0,0,0,361,353,1,0,0,0,362,27,1,0, + 0,0,363,364,3,30,15,0,364,365,3,50,25,0,365,29,1,0,0,0,366,369,3,124,62, + 0,367,369,5,144,0,0,368,366,1,0,0,0,368,367,1,0,0,0,369,31,1,0,0,0,370, + 371,5,64,0,0,371,372,5,58,0,0,372,373,3,128,64,0,373,374,3,34,17,0,374, + 33,1,0,0,0,375,377,5,67,0,0,376,378,5,69,0,0,377,376,1,0,0,0,377,378,1, + 0,0,0,378,379,1,0,0,0,379,433,3,46,23,0,380,381,5,67,0,0,381,433,3,56, + 28,0,382,384,5,64,0,0,383,385,5,69,0,0,384,383,1,0,0,0,384,385,1,0,0,0, + 385,386,1,0,0,0,386,387,3,128,64,0,387,391,3,50,25,0,388,390,3,54,27,0, + 389,388,1,0,0,0,390,393,1,0,0,0,391,389,1,0,0,0,391,392,1,0,0,0,392,433, + 1,0,0,0,393,391,1,0,0,0,394,396,5,64,0,0,395,397,5,69,0,0,396,395,1,0, + 0,0,396,397,1,0,0,0,397,398,1,0,0,0,398,399,3,128,64,0,399,400,5,83,0, + 0,400,401,5,84,0,0,401,402,3,112,56,0,402,433,1,0,0,0,403,405,5,64,0,0, + 404,406,5,69,0,0,405,404,1,0,0,0,405,406,1,0,0,0,406,407,1,0,0,0,407,408, + 3,128,64,0,408,409,5,68,0,0,409,410,5,84,0,0,410,433,1,0,0,0,411,412,5, + 68,0,0,412,413,5,69,0,0,413,433,3,128,64,0,414,415,5,68,0,0,415,416,5, + 75,0,0,416,433,3,128,64,0,417,418,5,65,0,0,418,419,5,66,0,0,419,433,3, + 128,64,0,420,421,5,65,0,0,421,422,5,69,0,0,422,423,3,128,64,0,423,424, + 5,66,0,0,424,425,3,128,64,0,425,433,1,0,0,0,426,427,5,65,0,0,427,428,5, + 90,0,0,428,429,3,128,64,0,429,430,5,66,0,0,430,431,3,128,64,0,431,433, + 1,0,0,0,432,375,1,0,0,0,432,380,1,0,0,0,432,382,1,0,0,0,432,394,1,0,0, + 0,432,403,1,0,0,0,432,411,1,0,0,0,432,414,1,0,0,0,432,417,1,0,0,0,432, + 420,1,0,0,0,432,426,1,0,0,0,433,35,1,0,0,0,434,440,3,70,35,0,435,440,3, + 64,32,0,436,440,3,8,4,0,437,440,3,12,6,0,438,440,3,20,10,0,439,434,1,0, + 0,0,439,435,1,0,0,0,439,436,1,0,0,0,439,437,1,0,0,0,439,438,1,0,0,0,440, + 37,1,0,0,0,441,443,5,57,0,0,442,444,5,86,0,0,443,442,1,0,0,0,443,444,1, + 0,0,0,444,445,1,0,0,0,445,446,5,90,0,0,446,447,3,128,64,0,447,448,5,25, + 0,0,448,449,3,128,64,0,449,450,5,137,0,0,450,455,3,42,21,0,451,452,5,139, + 0,0,452,454,3,42,21,0,453,451,1,0,0,0,454,457,1,0,0,0,455,453,1,0,0,0, + 455,456,1,0,0,0,456,458,1,0,0,0,457,455,1,0,0,0,458,461,5,138,0,0,459, + 460,5,92,0,0,460,462,3,44,22,0,461,459,1,0,0,0,461,462,1,0,0,0,462,39, + 1,0,0,0,463,464,5,68,0,0,464,465,5,58,0,0,465,479,3,128,64,0,466,467,5, + 68,0,0,467,468,5,90,0,0,468,469,3,128,64,0,469,470,5,25,0,0,470,471,3, + 128,64,0,471,479,1,0,0,0,472,473,5,68,0,0,473,474,5,99,0,0,474,479,3,128, + 64,0,475,476,5,68,0,0,476,477,5,98,0,0,477,479,3,128,64,0,478,463,1,0, + 0,0,478,466,1,0,0,0,478,472,1,0,0,0,478,475,1,0,0,0,479,41,1,0,0,0,480, + 482,3,128,64,0,481,483,7,1,0,0,482,481,1,0,0,0,482,483,1,0,0,0,483,43, + 1,0,0,0,484,490,5,73,0,0,485,486,5,95,0,0,486,490,5,109,0,0,487,488,5, + 96,0,0,488,490,5,109,0,0,489,484,1,0,0,0,489,485,1,0,0,0,489,487,1,0,0, + 0,490,45,1,0,0,0,491,492,3,128,64,0,492,494,3,50,25,0,493,495,3,48,24, + 0,494,493,1,0,0,0,494,495,1,0,0,0,495,499,1,0,0,0,496,498,3,54,27,0,497, + 496,1,0,0,0,498,501,1,0,0,0,499,497,1,0,0,0,499,500,1,0,0,0,500,47,1,0, + 0,0,501,499,1,0,0,0,502,503,5,5,0,0,503,504,5,137,0,0,504,505,3,112,56, + 0,505,506,5,138,0,0,506,49,1,0,0,0,507,510,3,128,64,0,508,510,5,88,0,0, + 509,507,1,0,0,0,509,508,1,0,0,0,510,512,1,0,0,0,511,513,3,128,64,0,512, + 511,1,0,0,0,512,513,1,0,0,0,513,515,1,0,0,0,514,516,3,128,64,0,515,514, + 1,0,0,0,515,516,1,0,0,0,516,525,1,0,0,0,517,518,5,137,0,0,518,521,3,52, + 26,0,519,520,5,139,0,0,520,522,3,52,26,0,521,519,1,0,0,0,521,522,1,0,0, + 0,522,523,1,0,0,0,523,524,5,138,0,0,524,526,1,0,0,0,525,517,1,0,0,0,525, + 526,1,0,0,0,526,51,1,0,0,0,527,529,5,129,0,0,528,527,1,0,0,0,528,529,1, + 0,0,0,529,530,1,0,0,0,530,531,5,146,0,0,531,53,1,0,0,0,532,533,5,8,0,0, + 533,602,5,109,0,0,534,602,5,109,0,0,535,536,5,84,0,0,536,602,3,112,56, + 0,537,538,5,92,0,0,538,602,7,2,0,0,539,540,5,75,0,0,540,542,3,128,64,0, + 541,539,1,0,0,0,541,542,1,0,0,0,542,543,1,0,0,0,543,544,5,97,0,0,544,545, + 5,137,0,0,545,546,3,58,29,0,546,547,5,138,0,0,547,602,1,0,0,0,548,549, + 5,75,0,0,549,551,3,128,64,0,550,548,1,0,0,0,550,551,1,0,0,0,551,552,1, + 0,0,0,552,553,5,73,0,0,553,555,5,74,0,0,554,556,3,138,69,0,555,554,1,0, + 0,0,555,556,1,0,0,0,556,602,1,0,0,0,557,558,5,75,0,0,558,560,3,128,64, + 0,559,557,1,0,0,0,559,560,1,0,0,0,560,561,1,0,0,0,561,563,5,86,0,0,562, + 564,3,138,69,0,563,562,1,0,0,0,563,564,1,0,0,0,564,602,1,0,0,0,565,566, + 5,75,0,0,566,568,3,128,64,0,567,565,1,0,0,0,567,568,1,0,0,0,568,569,1, + 0,0,0,569,570,5,77,0,0,570,582,3,128,64,0,571,572,5,137,0,0,572,577,3, + 128,64,0,573,574,5,139,0,0,574,576,3,128,64,0,575,573,1,0,0,0,576,579, + 1,0,0,0,577,575,1,0,0,0,577,578,1,0,0,0,578,580,1,0,0,0,579,577,1,0,0, + 0,580,581,5,138,0,0,581,583,1,0,0,0,582,571,1,0,0,0,582,583,1,0,0,0,583, + 587,1,0,0,0,584,586,3,60,30,0,585,584,1,0,0,0,586,589,1,0,0,0,587,585, + 1,0,0,0,587,588,1,0,0,0,588,602,1,0,0,0,589,587,1,0,0,0,590,599,5,88,0, + 0,591,592,5,137,0,0,592,595,3,52,26,0,593,594,5,139,0,0,594,596,3,52,26, + 0,595,593,1,0,0,0,595,596,1,0,0,0,596,597,1,0,0,0,597,598,5,138,0,0,598, + 600,1,0,0,0,599,591,1,0,0,0,599,600,1,0,0,0,600,602,1,0,0,0,601,532,1, + 0,0,0,601,534,1,0,0,0,601,535,1,0,0,0,601,537,1,0,0,0,601,541,1,0,0,0, + 601,550,1,0,0,0,601,559,1,0,0,0,601,567,1,0,0,0,601,590,1,0,0,0,602,55, + 1,0,0,0,603,604,5,75,0,0,604,606,3,128,64,0,605,603,1,0,0,0,605,606,1, + 0,0,0,606,607,1,0,0,0,607,608,5,73,0,0,608,610,5,74,0,0,609,611,3,138, + 69,0,610,609,1,0,0,0,610,611,1,0,0,0,611,612,1,0,0,0,612,613,5,137,0,0, + 613,618,3,128,64,0,614,615,5,139,0,0,615,617,3,128,64,0,616,614,1,0,0, + 0,617,620,1,0,0,0,618,616,1,0,0,0,618,619,1,0,0,0,619,621,1,0,0,0,620, + 618,1,0,0,0,621,622,5,138,0,0,622,693,1,0,0,0,623,624,5,75,0,0,624,626, + 3,128,64,0,625,623,1,0,0,0,625,626,1,0,0,0,626,627,1,0,0,0,627,629,5,86, + 0,0,628,630,3,138,69,0,629,628,1,0,0,0,629,630,1,0,0,0,630,631,1,0,0,0, + 631,632,5,137,0,0,632,637,3,128,64,0,633,634,5,139,0,0,634,636,3,128,64, + 0,635,633,1,0,0,0,636,639,1,0,0,0,637,635,1,0,0,0,637,638,1,0,0,0,638, + 640,1,0,0,0,639,637,1,0,0,0,640,641,5,138,0,0,641,693,1,0,0,0,642,643, + 5,75,0,0,643,645,3,128,64,0,644,642,1,0,0,0,644,645,1,0,0,0,645,646,1, + 0,0,0,646,647,5,76,0,0,647,650,5,74,0,0,648,649,5,85,0,0,649,651,5,90, + 0,0,650,648,1,0,0,0,650,651,1,0,0,0,651,652,1,0,0,0,652,653,5,137,0,0, + 653,658,3,128,64,0,654,655,5,139,0,0,655,657,3,128,64,0,656,654,1,0,0, + 0,657,660,1,0,0,0,658,656,1,0,0,0,658,659,1,0,0,0,659,661,1,0,0,0,660, + 658,1,0,0,0,661,662,5,138,0,0,662,663,5,77,0,0,663,675,3,128,64,0,664, + 665,5,137,0,0,665,670,3,128,64,0,666,667,5,139,0,0,667,669,3,128,64,0, + 668,666,1,0,0,0,669,672,1,0,0,0,670,668,1,0,0,0,670,671,1,0,0,0,671,673, + 1,0,0,0,672,670,1,0,0,0,673,674,5,138,0,0,674,676,1,0,0,0,675,664,1,0, + 0,0,675,676,1,0,0,0,676,680,1,0,0,0,677,679,3,60,30,0,678,677,1,0,0,0, + 679,682,1,0,0,0,680,678,1,0,0,0,680,681,1,0,0,0,681,693,1,0,0,0,682,680, + 1,0,0,0,683,684,5,75,0,0,684,686,3,128,64,0,685,683,1,0,0,0,685,686,1, + 0,0,0,686,687,1,0,0,0,687,688,5,97,0,0,688,689,5,137,0,0,689,690,3,58, + 29,0,690,691,5,138,0,0,691,693,1,0,0,0,692,605,1,0,0,0,692,625,1,0,0,0, + 692,644,1,0,0,0,692,685,1,0,0,0,693,57,1,0,0,0,694,700,8,3,0,0,695,696, + 5,137,0,0,696,697,3,58,29,0,697,698,5,138,0,0,698,700,1,0,0,0,699,694, + 1,0,0,0,699,695,1,0,0,0,700,703,1,0,0,0,701,699,1,0,0,0,701,702,1,0,0, + 0,702,59,1,0,0,0,703,701,1,0,0,0,704,705,5,25,0,0,705,706,5,79,0,0,706, + 711,3,62,31,0,707,708,5,25,0,0,708,709,5,78,0,0,709,711,3,62,31,0,710, + 704,1,0,0,0,710,707,1,0,0,0,711,61,1,0,0,0,712,721,5,80,0,0,713,714,5, + 85,0,0,714,721,5,82,0,0,715,721,5,81,0,0,716,717,5,83,0,0,717,721,5,109, + 0,0,718,719,5,83,0,0,719,721,5,84,0,0,720,712,1,0,0,0,720,713,1,0,0,0, + 720,715,1,0,0,0,720,716,1,0,0,0,720,718,1,0,0,0,721,63,1,0,0,0,722,723, + 5,70,0,0,723,724,5,71,0,0,724,752,3,128,64,0,725,726,5,137,0,0,726,731, + 3,128,64,0,727,728,5,139,0,0,728,730,3,128,64,0,729,727,1,0,0,0,730,733, + 1,0,0,0,731,729,1,0,0,0,731,732,1,0,0,0,732,734,1,0,0,0,733,731,1,0,0, + 0,734,735,5,138,0,0,735,737,1,0,0,0,736,725,1,0,0,0,736,737,1,0,0,0,737, + 748,1,0,0,0,738,739,5,72,0,0,739,744,3,66,33,0,740,741,5,139,0,0,741,743, + 3,66,33,0,742,740,1,0,0,0,743,746,1,0,0,0,744,742,1,0,0,0,744,745,1,0, + 0,0,745,749,1,0,0,0,746,744,1,0,0,0,747,749,3,70,35,0,748,738,1,0,0,0, + 748,747,1,0,0,0,749,753,1,0,0,0,750,751,5,84,0,0,751,753,5,72,0,0,752, + 736,1,0,0,0,752,750,1,0,0,0,753,65,1,0,0,0,754,755,5,137,0,0,755,760,3, + 68,34,0,756,757,5,139,0,0,757,759,3,68,34,0,758,756,1,0,0,0,759,762,1, + 0,0,0,760,758,1,0,0,0,760,761,1,0,0,0,761,763,1,0,0,0,762,760,1,0,0,0, + 763,764,5,138,0,0,764,67,1,0,0,0,765,768,5,84,0,0,766,768,3,112,56,0,767, + 765,1,0,0,0,767,766,1,0,0,0,768,69,1,0,0,0,769,775,3,72,36,0,770,771,3, + 74,37,0,771,772,3,72,36,0,772,774,1,0,0,0,773,770,1,0,0,0,774,777,1,0, + 0,0,775,773,1,0,0,0,775,776,1,0,0,0,776,779,1,0,0,0,777,775,1,0,0,0,778, + 780,3,108,54,0,779,778,1,0,0,0,779,780,1,0,0,0,780,782,1,0,0,0,781,783, + 3,88,44,0,782,781,1,0,0,0,782,783,1,0,0,0,783,71,1,0,0,0,784,799,3,76, + 38,0,785,786,5,137,0,0,786,787,3,70,35,0,787,788,5,138,0,0,788,799,1,0, + 0,0,789,790,5,72,0,0,790,795,3,66,33,0,791,792,5,139,0,0,792,794,3,66, + 33,0,793,791,1,0,0,0,794,797,1,0,0,0,795,793,1,0,0,0,795,796,1,0,0,0,796, + 799,1,0,0,0,797,795,1,0,0,0,798,784,1,0,0,0,798,785,1,0,0,0,798,789,1, + 0,0,0,799,73,1,0,0,0,800,802,5,53,0,0,801,803,5,54,0,0,802,801,1,0,0,0, + 802,803,1,0,0,0,803,807,1,0,0,0,804,807,5,55,0,0,805,807,5,56,0,0,806, + 800,1,0,0,0,806,804,1,0,0,0,806,805,1,0,0,0,807,75,1,0,0,0,808,810,5,1, + 0,0,809,811,3,78,39,0,810,809,1,0,0,0,810,811,1,0,0,0,811,813,1,0,0,0, + 812,814,3,84,42,0,813,812,1,0,0,0,813,814,1,0,0,0,814,815,1,0,0,0,815, + 818,3,92,46,0,816,817,5,71,0,0,817,819,3,128,64,0,818,816,1,0,0,0,818, + 819,1,0,0,0,819,821,1,0,0,0,820,822,3,96,48,0,821,820,1,0,0,0,821,822, + 1,0,0,0,822,824,1,0,0,0,823,825,3,106,53,0,824,823,1,0,0,0,824,825,1,0, + 0,0,825,827,1,0,0,0,826,828,3,80,40,0,827,826,1,0,0,0,827,828,1,0,0,0, + 828,830,1,0,0,0,829,831,3,82,41,0,830,829,1,0,0,0,830,831,1,0,0,0,831, + 77,1,0,0,0,832,833,7,4,0,0,833,79,1,0,0,0,834,835,5,27,0,0,835,836,5,29, + 0,0,836,841,3,112,56,0,837,838,5,139,0,0,838,840,3,112,56,0,839,837,1, + 0,0,0,840,843,1,0,0,0,841,839,1,0,0,0,841,842,1,0,0,0,842,81,1,0,0,0,843, + 841,1,0,0,0,844,845,5,30,0,0,845,846,3,112,56,0,846,83,1,0,0,0,847,848, + 5,4,0,0,848,853,3,86,43,0,849,850,7,5,0,0,850,852,3,86,43,0,851,849,1, + 0,0,0,852,855,1,0,0,0,853,851,1,0,0,0,853,854,1,0,0,0,854,857,1,0,0,0, + 855,853,1,0,0,0,856,858,5,36,0,0,857,856,1,0,0,0,857,858,1,0,0,0,858,85, + 1,0,0,0,859,866,5,146,0,0,860,866,5,144,0,0,861,862,5,137,0,0,862,863, + 3,112,56,0,863,864,5,138,0,0,864,866,1,0,0,0,865,859,1,0,0,0,865,860,1, + 0,0,0,865,861,1,0,0,0,866,87,1,0,0,0,867,868,5,45,0,0,868,869,3,112,56, + 0,869,876,3,90,45,0,870,871,5,46,0,0,871,872,7,6,0,0,872,873,3,112,56, + 0,873,874,3,90,45,0,874,875,5,51,0,0,875,877,1,0,0,0,876,870,1,0,0,0,876, + 877,1,0,0,0,877,885,1,0,0,0,878,879,5,46,0,0,879,880,7,6,0,0,880,881,3, + 112,56,0,881,882,3,90,45,0,882,883,5,51,0,0,883,885,1,0,0,0,884,867,1, + 0,0,0,884,878,1,0,0,0,885,89,1,0,0,0,886,887,7,7,0,0,887,91,1,0,0,0,888, + 898,5,124,0,0,889,894,3,94,47,0,890,891,5,139,0,0,891,893,3,94,47,0,892, + 890,1,0,0,0,893,896,1,0,0,0,894,892,1,0,0,0,894,895,1,0,0,0,895,898,1, + 0,0,0,896,894,1,0,0,0,897,888,1,0,0,0,897,889,1,0,0,0,898,93,1,0,0,0,899, + 900,3,128,64,0,900,901,5,140,0,0,901,902,5,124,0,0,902,911,1,0,0,0,903, + 908,3,112,56,0,904,906,5,5,0,0,905,904,1,0,0,0,905,906,1,0,0,0,906,907, + 1,0,0,0,907,909,3,128,64,0,908,905,1,0,0,0,908,909,1,0,0,0,909,911,1,0, + 0,0,910,899,1,0,0,0,910,903,1,0,0,0,911,95,1,0,0,0,912,913,5,2,0,0,913, + 918,3,98,49,0,914,915,5,139,0,0,915,917,3,98,49,0,916,914,1,0,0,0,917, + 920,1,0,0,0,918,916,1,0,0,0,918,919,1,0,0,0,919,97,1,0,0,0,920,918,1,0, + 0,0,921,925,3,100,50,0,922,924,3,102,51,0,923,922,1,0,0,0,924,927,1,0, + 0,0,925,923,1,0,0,0,925,926,1,0,0,0,926,99,1,0,0,0,927,925,1,0,0,0,928, + 933,3,128,64,0,929,931,5,5,0,0,930,929,1,0,0,0,930,931,1,0,0,0,931,932, + 1,0,0,0,932,934,3,128,64,0,933,930,1,0,0,0,933,934,1,0,0,0,934,949,1,0, + 0,0,935,936,5,137,0,0,936,937,3,70,35,0,937,942,5,138,0,0,938,940,5,5, + 0,0,939,938,1,0,0,0,939,940,1,0,0,0,940,941,1,0,0,0,941,943,3,128,64,0, + 942,939,1,0,0,0,942,943,1,0,0,0,943,949,1,0,0,0,944,945,5,137,0,0,945, + 946,3,98,49,0,946,947,5,138,0,0,947,949,1,0,0,0,948,928,1,0,0,0,948,935, + 1,0,0,0,948,944,1,0,0,0,949,101,1,0,0,0,950,951,3,104,52,0,951,952,5,23, + 0,0,952,953,3,100,50,0,953,954,5,25,0,0,954,955,3,112,56,0,955,966,1,0, + 0,0,956,957,5,37,0,0,957,958,5,23,0,0,958,966,3,100,50,0,959,960,5,37, + 0,0,960,961,5,38,0,0,961,966,3,100,50,0,962,963,5,22,0,0,963,964,5,38, + 0,0,964,966,3,100,50,0,965,950,1,0,0,0,965,956,1,0,0,0,965,959,1,0,0,0, + 965,962,1,0,0,0,966,103,1,0,0,0,967,969,5,18,0,0,968,967,1,0,0,0,968,969, + 1,0,0,0,969,983,1,0,0,0,970,972,5,19,0,0,971,973,5,22,0,0,972,971,1,0, + 0,0,972,973,1,0,0,0,973,983,1,0,0,0,974,976,5,20,0,0,975,977,5,22,0,0, + 976,975,1,0,0,0,976,977,1,0,0,0,977,983,1,0,0,0,978,980,5,21,0,0,979,981, + 5,22,0,0,980,979,1,0,0,0,980,981,1,0,0,0,981,983,1,0,0,0,982,968,1,0,0, + 0,982,970,1,0,0,0,982,974,1,0,0,0,982,978,1,0,0,0,983,105,1,0,0,0,984, + 985,5,3,0,0,985,986,3,112,56,0,986,107,1,0,0,0,987,988,5,26,0,0,988,989, + 5,29,0,0,989,994,3,110,55,0,990,991,5,139,0,0,991,993,3,110,55,0,992,990, + 1,0,0,0,993,996,1,0,0,0,994,992,1,0,0,0,994,995,1,0,0,0,995,109,1,0,0, + 0,996,994,1,0,0,0,997,999,3,112,56,0,998,1000,7,1,0,0,999,998,1,0,0,0, + 999,1000,1,0,0,0,1000,111,1,0,0,0,1001,1002,6,56,-1,0,1002,1003,7,5,0, + 0,1003,1008,3,112,56,21,1004,1005,7,8,0,0,1005,1008,3,112,56,7,1006,1008, + 3,114,57,0,1007,1001,1,0,0,0,1007,1004,1,0,0,0,1007,1006,1,0,0,0,1008, + 1107,1,0,0,0,1009,1010,10,22,0,0,1010,1011,5,127,0,0,1011,1106,3,112,56, + 23,1012,1013,10,20,0,0,1013,1014,7,9,0,0,1014,1106,3,112,56,21,1015,1016, + 10,19,0,0,1016,1017,5,126,0,0,1017,1106,3,112,56,20,1018,1019,10,18,0, + 0,1019,1020,5,17,0,0,1020,1106,3,112,56,19,1021,1022,10,17,0,0,1022,1023, + 7,5,0,0,1023,1106,3,112,56,18,1024,1025,10,16,0,0,1025,1026,5,130,0,0, + 1026,1106,3,112,56,17,1027,1028,10,15,0,0,1028,1029,7,10,0,0,1029,1106, + 3,112,56,16,1030,1032,10,14,0,0,1031,1033,5,8,0,0,1032,1031,1,0,0,0,1032, + 1033,1,0,0,0,1033,1034,1,0,0,0,1034,1035,5,52,0,0,1035,1036,3,112,56,0, + 1036,1037,5,6,0,0,1037,1038,3,112,56,15,1038,1106,1,0,0,0,1039,1041,10, + 13,0,0,1040,1042,5,8,0,0,1041,1040,1,0,0,0,1041,1042,1,0,0,0,1042,1043, + 1,0,0,0,1043,1044,5,16,0,0,1044,1106,3,112,56,14,1045,1046,10,8,0,0,1046, + 1048,5,28,0,0,1047,1049,5,8,0,0,1048,1047,1,0,0,0,1048,1049,1,0,0,0,1049, + 1050,1,0,0,0,1050,1051,5,35,0,0,1051,1052,5,2,0,0,1052,1106,3,112,56,9, + 1053,1054,10,6,0,0,1054,1055,7,11,0,0,1055,1106,3,112,56,7,1056,1057,10, + 5,0,0,1057,1058,7,12,0,0,1058,1106,3,112,56,6,1059,1060,10,4,0,0,1060, + 1061,7,13,0,0,1061,1106,3,112,56,5,1062,1063,10,3,0,0,1063,1064,5,10,0, + 0,1064,1106,3,112,56,4,1065,1066,10,2,0,0,1066,1067,5,11,0,0,1067,1106, + 3,112,56,3,1068,1070,10,12,0,0,1069,1071,5,8,0,0,1070,1069,1,0,0,0,1070, + 1071,1,0,0,0,1071,1072,1,0,0,0,1072,1073,5,24,0,0,1073,1074,5,137,0,0, + 1074,1075,3,70,35,0,1075,1076,5,138,0,0,1076,1106,1,0,0,0,1077,1079,10, + 11,0,0,1078,1080,5,8,0,0,1079,1078,1,0,0,0,1079,1080,1,0,0,0,1080,1081, + 1,0,0,0,1081,1082,5,24,0,0,1082,1083,5,137,0,0,1083,1088,3,112,56,0,1084, + 1085,5,139,0,0,1085,1087,3,112,56,0,1086,1084,1,0,0,0,1087,1090,1,0,0, + 0,1088,1086,1,0,0,0,1088,1089,1,0,0,0,1089,1091,1,0,0,0,1090,1088,1,0, + 0,0,1091,1092,5,138,0,0,1092,1106,1,0,0,0,1093,1094,10,10,0,0,1094,1096, + 5,28,0,0,1095,1097,5,8,0,0,1096,1095,1,0,0,0,1096,1097,1,0,0,0,1097,1098, + 1,0,0,0,1098,1106,5,109,0,0,1099,1100,10,9,0,0,1100,1102,5,28,0,0,1101, + 1103,5,8,0,0,1102,1101,1,0,0,0,1102,1103,1,0,0,0,1103,1104,1,0,0,0,1104, + 1106,7,14,0,0,1105,1009,1,0,0,0,1105,1012,1,0,0,0,1105,1015,1,0,0,0,1105, + 1018,1,0,0,0,1105,1021,1,0,0,0,1105,1024,1,0,0,0,1105,1027,1,0,0,0,1105, + 1030,1,0,0,0,1105,1039,1,0,0,0,1105,1045,1,0,0,0,1105,1053,1,0,0,0,1105, + 1056,1,0,0,0,1105,1059,1,0,0,0,1105,1062,1,0,0,0,1105,1065,1,0,0,0,1105, + 1068,1,0,0,0,1105,1077,1,0,0,0,1105,1093,1,0,0,0,1105,1099,1,0,0,0,1106, + 1109,1,0,0,0,1107,1105,1,0,0,0,1107,1108,1,0,0,0,1108,113,1,0,0,0,1109, + 1107,1,0,0,0,1110,1130,3,130,65,0,1111,1130,3,116,58,0,1112,1130,3,120, + 60,0,1113,1130,3,124,62,0,1114,1130,5,144,0,0,1115,1130,5,143,0,0,1116, + 1117,5,31,0,0,1117,1118,5,137,0,0,1118,1119,3,70,35,0,1119,1120,5,138, + 0,0,1120,1130,1,0,0,0,1121,1122,5,137,0,0,1122,1123,3,70,35,0,1123,1124, + 5,138,0,0,1124,1130,1,0,0,0,1125,1126,5,137,0,0,1126,1127,3,112,56,0,1127, + 1128,5,138,0,0,1128,1130,1,0,0,0,1129,1110,1,0,0,0,1129,1111,1,0,0,0,1129, + 1112,1,0,0,0,1129,1113,1,0,0,0,1129,1114,1,0,0,0,1129,1115,1,0,0,0,1129, + 1116,1,0,0,0,1129,1121,1,0,0,0,1129,1125,1,0,0,0,1130,115,1,0,0,0,1131, + 1133,5,41,0,0,1132,1134,3,112,56,0,1133,1132,1,0,0,0,1133,1134,1,0,0,0, + 1134,1136,1,0,0,0,1135,1137,3,118,59,0,1136,1135,1,0,0,0,1137,1138,1,0, + 0,0,1138,1136,1,0,0,0,1138,1139,1,0,0,0,1139,1142,1,0,0,0,1140,1141,5, + 43,0,0,1141,1143,3,112,56,0,1142,1140,1,0,0,0,1142,1143,1,0,0,0,1143,1144, + 1,0,0,0,1144,1145,5,44,0,0,1145,117,1,0,0,0,1146,1147,5,42,0,0,1147,1148, + 3,112,56,0,1148,1149,5,33,0,0,1149,1150,3,112,56,0,1150,119,1,0,0,0,1151, + 1152,3,122,61,0,1152,1165,5,137,0,0,1153,1166,5,124,0,0,1154,1156,5,35, + 0,0,1155,1154,1,0,0,0,1155,1156,1,0,0,0,1156,1157,1,0,0,0,1157,1162,3, + 112,56,0,1158,1159,5,139,0,0,1159,1161,3,112,56,0,1160,1158,1,0,0,0,1161, + 1164,1,0,0,0,1162,1160,1,0,0,0,1162,1163,1,0,0,0,1163,1166,1,0,0,0,1164, + 1162,1,0,0,0,1165,1153,1,0,0,0,1165,1155,1,0,0,0,1165,1166,1,0,0,0,1166, + 1167,1,0,0,0,1167,1169,5,138,0,0,1168,1170,3,152,76,0,1169,1168,1,0,0, + 0,1169,1170,1,0,0,0,1170,1172,1,0,0,0,1171,1173,3,150,75,0,1172,1171,1, + 0,0,0,1172,1173,1,0,0,0,1173,1182,1,0,0,0,1174,1176,3,154,77,0,1175,1174, + 1,0,0,0,1175,1176,1,0,0,0,1176,1178,1,0,0,0,1177,1179,3,156,78,0,1178, + 1177,1,0,0,0,1178,1179,1,0,0,0,1179,1180,1,0,0,0,1180,1181,5,39,0,0,1181, + 1183,3,136,68,0,1182,1175,1,0,0,0,1182,1183,1,0,0,0,1183,121,1,0,0,0,1184, + 1191,3,128,64,0,1185,1191,5,19,0,0,1186,1191,5,20,0,0,1187,1191,5,103, + 0,0,1188,1191,5,48,0,0,1189,1191,5,40,0,0,1190,1184,1,0,0,0,1190,1185, + 1,0,0,0,1190,1186,1,0,0,0,1190,1187,1,0,0,0,1190,1188,1,0,0,0,1190,1189, + 1,0,0,0,1191,123,1,0,0,0,1192,1197,3,128,64,0,1193,1194,7,15,0,0,1194, + 1196,3,126,63,0,1195,1193,1,0,0,0,1196,1199,1,0,0,0,1197,1195,1,0,0,0, + 1197,1198,1,0,0,0,1198,125,1,0,0,0,1199,1197,1,0,0,0,1200,1203,3,128,64, + 0,1201,1203,3,148,74,0,1202,1200,1,0,0,0,1202,1201,1,0,0,0,1203,127,1, + 0,0,0,1204,1209,5,153,0,0,1205,1209,5,151,0,0,1206,1209,5,152,0,0,1207, + 1209,3,146,73,0,1208,1204,1,0,0,0,1208,1205,1,0,0,0,1208,1206,1,0,0,0, + 1208,1207,1,0,0,0,1209,129,1,0,0,0,1210,1220,5,146,0,0,1211,1220,5,147, + 0,0,1212,1220,5,145,0,0,1213,1220,5,148,0,0,1214,1220,5,149,0,0,1215,1220, + 5,150,0,0,1216,1220,7,16,0,0,1217,1220,7,17,0,0,1218,1220,5,109,0,0,1219, + 1210,1,0,0,0,1219,1211,1,0,0,0,1219,1212,1,0,0,0,1219,1213,1,0,0,0,1219, + 1214,1,0,0,0,1219,1215,1,0,0,0,1219,1216,1,0,0,0,1219,1217,1,0,0,0,1219, + 1218,1,0,0,0,1220,131,1,0,0,0,1221,1223,5,59,0,0,1222,1224,7,18,0,0,1223, + 1222,1,0,0,0,1223,1224,1,0,0,0,1224,1234,1,0,0,0,1225,1227,5,60,0,0,1226, + 1228,7,18,0,0,1227,1226,1,0,0,0,1227,1228,1,0,0,0,1228,1234,1,0,0,0,1229, + 1231,5,61,0,0,1230,1232,7,18,0,0,1231,1230,1,0,0,0,1231,1232,1,0,0,0,1232, + 1234,1,0,0,0,1233,1221,1,0,0,0,1233,1225,1,0,0,0,1233,1229,1,0,0,0,1234, + 133,1,0,0,0,1235,1236,3,112,56,0,1236,1237,5,0,0,1,1237,135,1,0,0,0,1238, + 1249,5,137,0,0,1239,1240,5,40,0,0,1240,1241,5,29,0,0,1241,1246,3,112,56, + 0,1242,1243,5,139,0,0,1243,1245,3,112,56,0,1244,1242,1,0,0,0,1245,1248, + 1,0,0,0,1246,1244,1,0,0,0,1246,1247,1,0,0,0,1247,1250,1,0,0,0,1248,1246, + 1,0,0,0,1249,1239,1,0,0,0,1249,1250,1,0,0,0,1250,1252,1,0,0,0,1251,1253, + 3,108,54,0,1252,1251,1,0,0,0,1252,1253,1,0,0,0,1253,1255,1,0,0,0,1254, + 1256,3,140,70,0,1255,1254,1,0,0,0,1255,1256,1,0,0,0,1256,1257,1,0,0,0, + 1257,1258,5,138,0,0,1258,137,1,0,0,0,1259,1260,7,19,0,0,1260,139,1,0,0, + 0,1261,1268,7,20,0,0,1262,1263,5,52,0,0,1263,1264,3,142,71,0,1264,1265, + 5,6,0,0,1265,1266,3,142,71,0,1266,1269,1,0,0,0,1267,1269,3,142,71,0,1268, + 1262,1,0,0,0,1268,1267,1,0,0,0,1269,1272,1,0,0,0,1270,1271,5,116,0,0,1271, + 1273,3,144,72,0,1272,1270,1,0,0,0,1272,1273,1,0,0,0,1273,141,1,0,0,0,1274, + 1275,5,112,0,0,1275,1282,7,21,0,0,1276,1277,5,115,0,0,1277,1282,5,50,0, + 0,1278,1279,3,112,56,0,1279,1280,7,21,0,0,1280,1282,1,0,0,0,1281,1274, + 1,0,0,0,1281,1276,1,0,0,0,1281,1278,1,0,0,0,1282,143,1,0,0,0,1283,1284, + 5,115,0,0,1284,1290,5,50,0,0,1285,1290,5,27,0,0,1286,1290,5,117,0,0,1287, + 1288,5,85,0,0,1288,1290,5,118,0,0,1289,1283,1,0,0,0,1289,1285,1,0,0,0, + 1289,1286,1,0,0,0,1289,1287,1,0,0,0,1290,145,1,0,0,0,1291,1292,7,22,0, + 0,1292,147,1,0,0,0,1293,1294,7,23,0,0,1294,149,1,0,0,0,1295,1296,5,123, + 0,0,1296,1297,5,137,0,0,1297,1298,5,3,0,0,1298,1299,3,112,56,0,1299,1300, + 5,138,0,0,1300,151,1,0,0,0,1301,1302,5,119,0,0,1302,1303,5,27,0,0,1303, + 1304,5,137,0,0,1304,1305,3,108,54,0,1305,1306,5,138,0,0,1306,153,1,0,0, + 0,1307,1308,5,2,0,0,1308,1309,7,24,0,0,1309,155,1,0,0,0,1310,1311,7,25, + 0,0,1311,1312,5,122,0,0,1312,157,1,0,0,0,165,159,175,178,184,204,213,216, + 224,233,237,249,257,261,269,274,277,285,292,302,309,323,328,337,348,358, + 361,368,377,384,391,396,405,432,439,443,455,461,478,482,489,494,499,509, + 512,515,521,525,528,541,550,555,559,563,567,577,582,587,595,599,601,605, + 610,618,625,629,637,644,650,658,670,675,680,685,692,699,701,710,720,731, + 736,744,748,752,760,767,775,779,782,795,798,802,806,810,813,818,821,824, + 827,830,841,853,857,865,876,884,894,897,905,908,910,918,925,930,933,939, + 942,948,965,968,972,976,980,982,994,999,1007,1032,1041,1048,1070,1079, + 1088,1096,1102,1105,1107,1129,1133,1138,1142,1155,1162,1165,1169,1172, + 1175,1178,1182,1190,1197,1202,1208,1219,1223,1227,1231,1233,1246,1249, + 1252,1255,1268,1272,1281,1289 }; 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 cfe66f1d..5d38631c 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs @@ -915,6 +915,12 @@ public interface IAccessSqlVisitor : IParseTreeVisitor { /// The visitor result. Result VisitColumnRef([NotNull] AccessSqlParser.ColumnRefContext context); /// + /// Visit a parse tree produced by . + /// + /// The parse tree. + /// The visitor result. + Result VisitMemberName([NotNull] AccessSqlParser.MemberNameContext context); + /// /// Visit a parse tree produced by . /// /// The parse tree. @@ -1047,6 +1053,12 @@ public interface IAccessSqlVisitor : IParseTreeVisitor { /// The visitor result. Result VisitNonReservedKeyword([NotNull] AccessSqlParser.NonReservedKeywordContext context); /// + /// Visit a parse tree produced by . + /// + /// The parse tree. + /// The visitor result. + Result VisitReservedKeyword([NotNull] AccessSqlParser.ReservedKeywordContext context); + /// /// Visit a parse tree produced by . /// /// The parse tree. diff --git a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs index 7dd74bcc..a9e709b2 100644 --- a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs +++ b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs @@ -91,8 +91,11 @@ private static DeleteStatement BuildDelete(DeleteStatementContext ctx) => private static UpdateStatement BuildUpdate(UpdateStatementContext ctx) { var assignments = ctx.assignment() - .Select(a => new Assignment( - OptionalIdentifier(a.target.qualifier), Identifier(a.target.name), BuildExpression(a.expression()))) + .Select(a => + { + ColumnReference target = BuildColumn(a.target); + return new Assignment(target.Table, target.Column, BuildExpression(a.expression())); + }) .ToList(); Expression? where = ctx.whereClause() is { } w ? BuildExpression(w.expression()) : null; return new UpdateStatement(BuildTableSources(ctx.tableSource()), assignments, where); @@ -554,16 +557,17 @@ private static ViewDefinition ActionBody(TableSourceContext[] sources, WhereClau } /// A declared parameter's name, with any leading @ stripped — Access stores the bare - /// name (e.g. @Beginning_Date is stored as Beginning_Date). + /// name (e.g. @Beginning_Date is stored as Beginning_Date). A chain naming a form control is + /// one name, as spells it. private static string ParamName(ProcParamContext p) => p.pname.PARAM() is { } at ? at.GetText().TrimStart('@') - : Identifier(p.pname.identifier()); + : ChainName(p.pname.columnRef()); /// A declared parameter's name as Access stores it: a name written in brackets keeps them - /// (verified vs ACE: [@firstName] is stored as [@firstName]), anything else as - /// reads it. + /// (verified vs ACE: [@firstName] is stored as [@firstName], and a form control as + /// [Forms]![f]![c]), anything else as reads it. private static string StoredParamName(ProcParamContext p) => - p.pname.identifier()?.GetText() is ['[', ..] bracketed ? bracketed : ParamName(p); + p.pname.columnRef()?.GetText() is ['[', ..] bracketed ? bracketed : ParamName(p); /// The declared type name of a data type — up to three words (e.g. "national character varying") /// joined by single spaces. @@ -572,7 +576,7 @@ private static string TypeName(DataTypeContext type) => string.Join(' ', .Concat(new[] { type.typeName, type.extra, type.extra2 }.Where(t => t is not null).Select(Identifier)) .Where(t => t is not null)); - // ---- PARAMETERS-clause lowering: unqualified references to a declared parameter become parameters ---- + // ---- PARAMETERS-clause lowering: references to a declared parameter become parameters ---- private static SqlStatement LowerParameters(SqlStatement s, HashSet names) => s switch { @@ -609,7 +613,12 @@ private static string TypeName(DataTypeContext type) => string.Join(' ', private static SelectStatement LowerSelect(SelectStatement sel, HashSet names) => sel with { - Projection = sel.Projection.Select(i => i with { Value = LowerExpr(i.Value, names) }).ToList(), + // A column that lowers to a parameter keeps the name it had as a column: its last part, unless a bang + // named it already (verified vs ACE: with PARAMETERS Forms!x, a bare Forms.x is named x; Forms!f.c is c). + Projection = sel.Projection.Select(i => LowerExpr(i.Value, names) is var value && i.Value is ColumnReference c + && value is ParameterExpression + ? i with { Value = value, Alias = i.Alias ?? c.Column[(c.Column.LastIndexOf('!') + 1)..] } + : i with { Value = value }).ToList(), From = LowerFrom(sel.From, names), Where = sel.Where is null ? null : LowerExpr(sel.Where, names), GroupBy = sel.GroupBy.Select(e => LowerExpr(e, names)).ToList(), @@ -633,6 +642,9 @@ private static SelectStatement LowerSelect(SelectStatement sel, HashSet private static Expression LowerExpr(Expression e, HashSet names) => e switch { ColumnReference { Table: null, Column: var c } when names.Contains(c) => new ParameterExpression(c), + // A two-part form control (Forms!ctl) reads as a table and a column. Its declaration still wins over a real + // column of that name (verified vs ACE: with PARAMETERS Customers!City Long, Customers!City is the parameter). + ColumnReference { Table: { } t, Column: var c } when names.Contains($"{t}!{c}") => new ParameterExpression($"{t}!{c}"), // A window function lowers like any other call — arguments AND the OVER clause, since a PARAMETERS name // can appear in a PARTITION BY or ORDER BY expression just as readily as in an argument. WindowFunction w => w with @@ -988,7 +1000,8 @@ Expression Operand(TopOperandContext o) => private static SelectItem BuildSelectItem(SelectItemContext ctx) => ctx switch { QualifiedStarSelectItemContext q => new SelectItem(new QualifiedStarExpression(Identifier(q.qualifier)), null), - ExpressionSelectItemContext e => new SelectItem(BuildExpression(e.expression()), OptionalIdentifier(e.alias)), + ExpressionSelectItemContext e => new SelectItem( + BuildExpression(e.expression()), OptionalIdentifier(e.alias) ?? BangColumnName(e.expression())), _ => throw new SqlParseException($"Unsupported select item: {ctx.GetText()}"), }; @@ -1258,14 +1271,60 @@ private static string FunctionName(FunctionNameContext ctx) => /// appear in an Access object name, which is what makes the split unambiguous: a dot inside the /// delimiters is always the qualifier. An undelimited a.b never reaches here as one name — the /// grammar has already split it — so this only ever rewrites what was bracketed or backticked. + /// A bang joins two parts exactly as a period does. A chain of three or more parts is one name — see + /// — that no column can have, so it resolves only as a declared parameter. /// private static ColumnReference BuildColumn(ColumnRefContext ctx) { - string? qualifier = OptionalIdentifier(ctx.qualifier); - string name = Identifier(ctx.name); - if (qualifier is null && name.IndexOf('.', StringComparison.Ordinal) is var dot && dot > 0 && dot < name.Length - 1) - return new ColumnReference(name[..dot], name[(dot + 1)..]); - return new ColumnReference(qualifier, name); + List parts = [ctx.first, .. ctx._rest]; + if (parts.Count == 1) + { + string name = Identifier(ctx.first); + if (name.IndexOf('.', StringComparison.Ordinal) is var dot && dot > 0 && dot < name.Length - 1) + return new ColumnReference(name[..dot], name[(dot + 1)..]); + return new ColumnReference(null, name); + } + + // ACE refuses a space either side of a bang (verified: 'Invalid use of '.', '!', or '()''). + foreach (var (bang, i) in ctx._separators.Select((s, i) => (s, i)).Where(s => s.s.Type == BANG)) + if (parts[i].Stop.StopIndex + 1 != bang.StartIndex || parts[i + 1].Start.StartIndex != bang.StopIndex + 1) + throw new SqlParseException( + $"Invalid use of '!' in '{OriginalText(ctx)}': a bang takes no space either side.", + bang.Line, bang.Column); + + return parts.Count == 2 + ? new ColumnReference(Identifier(ctx.first), MemberName(ctx._rest[0])) + : new ColumnReference(null, ChainName(ctx)); + } + + /// A name after a separator: an identifier as reads it, or a reserved word + /// as written. + private static string MemberName(MemberNameContext ctx) => + ctx.identifier() is { } id ? Identifier(id) : ctx.GetText(); + + /// A chain of names as one name: its parts undelimited and joined by bangs, whether a period or a bang + /// joined them — ACE binds either spelling to the same declared parameter (verified: with PARAMETERS + /// Forms!x, both Forms!x and Forms.x take its value, and [Forms]![f]![c] is + /// Forms!f!c). A single part is just that name. + private static string ChainName(ColumnRefContext ctx) => + string.Join('!', ctx._rest.Select(MemberName).Prepend(Identifier(ctx.first))); + + /// The name ACE gives an unaliased column written with a bang: the text after the chain's last period, + /// with its first and last characters dropped when they are a pair of delimiters (verified: + /// Customers!CustomerID is named Customers!CustomerID, [Customers]![CustomerID] + /// Customers]![CustomerID, [c]!CustomerID [c]!CustomerID, and Forms!f.c just + /// c). A derived table's column goes by that name too. Null when there is no bang after the last period, + /// which keeps the name the column's own. + private static string? BangColumnName(ExpressionContext ctx) + { + if (ctx is not PrimaryExprContext p || p.primary() is not ColumnPrimaryContext primary) return null; + ColumnRefContext column = primary.columnRef(); + int lastDot = column._separators.ToList().FindLastIndex(s => s.Type == DOT); + if (!column._separators.Skip(lastDot + 1).Any(s => s.Type == BANG)) return null; + + int start = lastDot < 0 ? column.first.Start.StartIndex : column._rest[lastDot].Start.StartIndex; + string text = column.Start.InputStream.GetText(Antlr4.Runtime.Misc.Interval.Of(start, column.Stop.StopIndex)); + return text is ['[', .., ']'] or ['`', .., '`'] ? text[1..^1] : text; } /// x IN (a, b, …) becomes a flat evaluated iteratively — NOT a diff --git a/src/LibRed/README.md b/src/LibRed/README.md index a8452ca4..a6303a1a 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -174,6 +174,11 @@ Treat the number as of its date — an EF Core version bump moves it. (`INSERT INTO … SELECT`, the append query) — `SELECT … INTO` (the make-table query), `UPDATE`, `DELETE`, `EXECUTE`, `IF … THEN`, and `SELECT` with `WHERE`, joins, `GROUP BY`/aggregates, `HAVING`, `ORDER BY`, `TOP [PERCENT]`, `ALL`/`DISTINCT`/`DISTINCTROW`, `UNION`/`INTERSECT`/`EXCEPT`, subqueries, and parameters. + Access's bang notation: `[Table]![Column]` is `Table.Column`, and an unaliased one is named as written, as + ACE names it. A form control such as `[Forms]![frmMenu]![txtCity]` is reachable only as a parameter the + query declares; unlike ACE, LibRed does not make an undeclared one an implicit parameter. `Yes`/`On` and + `No`/`Off` are `True` and `False`; any word, reserved or not, names a column after a `.` or `!` + (`qryHistory.Key`); and an unbracketed name may be in any script (`Bevételek.Datum`, `Номер`). `WITH COMPRESSION` on a Text column is honoured end to end — it maps onto `SupportsCompressedUnicode`, `TdefBuilder` writes the `0x01` extended-flag bit, and `JetTypeCodec` gates compressed encoding on it. Function arguments are arity-checked against a per-function range table, so a wrong count raises rather @@ -258,9 +263,6 @@ Format-level detail on each on-disk gap lives in `docs/format/`. kinds (INSERT…SELECT, UPDATE, DELETE, make-table, crosstab, UNION, pass-through) read back as a *named* refusal rather than executing, and a view carrying `HAVING` (attribute `0x0A`) is refused rather than rebuilt. The gap is the write-back, not the grammar: all of these parse and execute as plain statements. -- **`!` bang notation** (`[Table]![Col]`, `Forms![f]![ctl]`) — grammar gap, and the reason ~13% of the - stored queries LibRed rebuilds are then rejected by its own parser. Nearly all of those reference a form - control, which has no meaning outside Access anyway. - **Function surface** — the evaluator's whitelist isn't proven identical to ACE's JES, and `Format`'s named date/currency formats are locale-dependent by design (not byte-identical cross-locale). - **Non-unique index statistics** — only unique indexes advance the live unique-entry count (`+4`) today. diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index 371fb6bd..1d75970f 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -391,6 +391,14 @@ > to a declared name are lowered into engine parameters, so LibRed's own engine executes the stored procedure > when values are supplied. > + > **A form control is declared as a bang chain and stored as written.** `PARAMETERS [Forms]![frmMenu]![txtCity] + > Text ( 20 )`, saved through DAO, stores `Name1=[Forms]![frmMenu]![txtCity]` — each part in its own brackets, + > so a `Name1` that starts with `[` need not be one bracketed name. LibRed writes the same `Name1`, and the + > query runs in ACE with the value supplied. The name binds by its parts, undelimited and joined by `!` + > (`Forms!frmMenu!txtCity`): ACE takes `Forms!f!c`, `[Forms]![f]![c]` and `Forms!F!C` as one parameter, and + > a `.` between parts counts as a `!` — with `Forms!x` declared, `Forms.x` takes its value, and with + > `Forms!f!c` declared, so do `Forms!f.c` and `Forms.f!c`. + > > **A declared name wins over a column of the same name — and a `@` prefix does not distinguish them.** > Measured against ACE 12 on Northwind: *every* unqualified occurrence of a declared parameter name is the > parameter, whether written bare or as `@name`, even where the query's own table has a column by that name. diff --git a/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs b/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs new file mode 100644 index 00000000..06df664a --- /dev/null +++ b/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs @@ -0,0 +1,198 @@ +using System.Data.OleDb; +using System.Globalization; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// How ACE reads the names in a query, through ACE over OLE DB and through LibRed: what Customers!CustomerID +/// resolves to and what the column is called, a declared form control as a parameter and a stored query that declares +/// one, a reserved word naming a column after a period or a bang, a name in any script written unbracketed, and Yes, +/// No, On and Off as True and False. +/// +[Collection(AceCollection.Name)] +public class QueryNamesAccessTests(ITestOutputHelper output) : TempDatabaseTest +{ + // Every reserved word LibRed knows, each a column of table K. ACE refuses Union and When even qualified. + private static readonly string[] Keywords = + [ + "Select", "From", "Where", "Top", "As", "And", "Or", "Not", "Xor", "Eqv", "Imp", "Band", "Bor", "Bxor", "Bnot", + "Like", "Mod", "Inner", "Left", "Right", "Full", "Outer", "Join", "In", "On", "Order", "Group", "Is", "By", + "Having", "Exists", "If", "Then", "Distinctrow", "Distinct", "Percent", "Cross", "Apply", "Over", "Partition", + "Case", "Else", "End", "Offset", "Fetch", "Next", "First", "Rows", "Row", "Only", "Between", "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", "Clustered", "Identity", + "Nonclustered", "Index", "Temporary", "With", "Compression", "Comp", "Disallow", "Ignore", "Check", "View", + "Procedure", "Parameters", "Execute", "Exec", "Asc", "Desc", "True", "False", "Null", "Yes", "Off", + ]; + + // The operator words, which ACE refuses after a bang (as are Select and All). + private static readonly HashSet RefusedAfterBang = new(StringComparer.OrdinalIgnoreCase) + { + "Select", "All", "And", "Or", "Not", "Xor", "Eqv", "Imp", "Band", "Bor", "Bxor", "Bnot", "Like", "Mod", "In", + "Is", "Between", "Exists", + }; + + private static readonly string[] Setup = + [ + $"CREATE TABLE K ({string.Join(", ", Keywords.Select(k => $"[{k}] LONG"))})", + $"INSERT INTO K ({string.Join(", ", Keywords.Select(k => $"[{k}]"))}) VALUES ({string.Join(", ", Keywords.Select((_, i) => (i + 100).ToString(CultureInfo.InvariantCulture)))})", + "CREATE TABLE [Árú] ([Név] TEXT(10), [Номер] LONG, [名前] TEXT(10), [ΑΒΓ] LONG, [straße] LONG, [Ñandú] LONG, [n1] LONG)", + "INSERT INTO [Árú] VALUES ('x', 2, 'y', 3, 4, 5, 6)", + ]; + + public static TheoryData Queries + { + get + { + var queries = new TheoryData + { + "SELECT Customers!CustomerID, [Customers]![City], `Customers`!`Country` FROM Customers WHERE CustomerID = 'ALFKI'", + "SELECT Customers!CustomerID AS X FROM Customers WHERE CustomerID = 'ALFKI'", + "SELECT c!CustomerID FROM Customers AS c WHERE c!City = 'Berlin'", + "SELECT [c]!CustomerID FROM Customers AS [c] WHERE [c]![City] = 'Berlin'", + "SELECT City, COUNT(*) AS N FROM Customers GROUP BY Customers!City HAVING Customers!City > 'S' ORDER BY Customers!City DESC", + "SELECT TOP 3 CustomerID FROM Customers ORDER BY Customers!CustomerID DESC", + "SELECT CustomerID, (SELECT COUNT(*) FROM Orders WHERE Orders!CustomerID = Customers!CustomerID) AS N FROM Customers WHERE Country = 'Germany'", + "SELECT d!X FROM (SELECT CustomerID AS X FROM Customers WHERE Country = 'Germany') AS d ORDER BY d!X", + "SELECT * FROM (SELECT Customers!City, [Customers]![Country] FROM Customers WHERE CustomerID = 'ALFKI')", + "SELECT [Customers!City] FROM (SELECT Customers!City FROM Customers WHERE CustomerID = 'ALFKI')", + "SELECT Customers!City FROM Customers WHERE CustomerID = 'ALFKI' UNION SELECT Customers!City FROM Customers WHERE CustomerID = 'ANATR'", + "SELECT MAX(Customers!CustomerID) AS M FROM Customers", + "SELECT Név, Номер, 名前, ΑΒΓ, straße, Ñandú, n1 FROM Árú", + "SELECT Árú.Név, Árú!Номер FROM Árú", + "SELECT a.Név FROM Árú AS a WHERE a.ΑΒΓ = 3", + // Through CInt and IIF: ACE types True and False, and so these, as Int16 where LibRed has a Boolean. + "SELECT CInt(Yes) AS A, CInt(No) AS B, CInt(On) AS C, CInt(Off) AS D, CInt(True) AS E, CInt(False) AS F FROM K", + "SELECT Yes + 1 AS A, IIF(Not No, 1, 2) AS B, IIF(Yes AND No, 1, 2) AS C, IIF(Yes = True, 1, 2) AS D, IIF(Off = False, 1, 2) AS E FROM K", + "SELECT CustomerID FROM Customers WHERE (CustomerID = 'ALFKI') = Yes", + "SELECT CustomerID FROM Customers WHERE On AND CustomerID < 'B'", + // Unqualified, a name that is also a constant is the constant; qualified, it is the column. + "SELECT CInt(Yes) AS A, K.Yes AS B, CInt(No) AS C, K.No AS D, CInt(Off) AS E, K.Off AS F, K.On AS G FROM K", + }; + foreach (string k in Keywords) + { + queries.Add($"SELECT K.{k} FROM K"); + if (!RefusedAfterBang.Contains(k)) queries.Add($"SELECT K!{k} FROM K"); + } + return queries; + } + } + + // A declared parameter, bound by position in ACE and by name in LibRed. + public static TheoryData Declared => new() + { + { "PARAMETERS [Forms]![f]![c] Long; SELECT CustomerID, Forms!f!c FROM Customers WHERE CustomerID = 'ALFKI'", "Forms!f!c" }, + { "PARAMETERS Forms!f!c Long; SELECT [Forms]![f]![c] FROM Customers WHERE CustomerID = 'ALFKI'", "Forms!f!c" }, + { "PARAMETERS Forms!x Long; SELECT Forms!x, Forms.x FROM Customers WHERE CustomerID = 'ALFKI'", "Forms!x" }, + { "PARAMETERS Forms!f!c Long; SELECT Forms!f.c, Forms.f!c FROM Customers WHERE CustomerID = 'ALFKI'", "Forms!f!c" }, + { "PARAMETERS Customers!City Long; SELECT Customers!City FROM Customers WHERE CustomerID = 'ALFKI'", "Customers!City" }, + { "PARAMETERS p Long; SELECT p FROM Customers WHERE CustomerID = 'ALFKI'", "p" }, + { "PARAMETERS [Forms]![f]![c] Long; SELECT CustomerID FROM Orders WHERE OrderID = Forms!f!c + 10247", "Forms!f!c" }, + }; + + [Theory] + [MemberData(nameof(Queries))] + public void A_query_reads_and_names_as_ace_does(string query) => + Matches(query, command => { }, null); + + [Theory] + [MemberData(nameof(Declared))] + public void A_declared_form_control_is_a_parameter_as_in_ace(string query, string name) => + Matches(query, command => command.Parameters.AddWithValue("?", 1), new Dictionary { [name] = 1 }); + + // A stored query declaring a form control, saved by Access, reads back and runs through LibRed; made by LibRed, it + // stores the same declaration and runs in ACE. ACE's own CREATE PROCEDURE will not take a chain as a parameter + // name, so Access's copy is saved through DAO, as the Access UI saves one. + [Fact] + public void A_stored_query_declaring_a_form_control_round_trips_with_ace() + { + const string body = "SELECT CustomerID FROM Customers WHERE City = Forms!frmMenu!txtCity"; + + string byAce = Copy(), byLibRed = Copy(); + try + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + object database = Invoke(engine!, "OpenDatabase", byAce, false, false, "")!; + Invoke(database, "CreateQueryDef", "ByCity", $"PARAMETERS [Forms]![frmMenu]![txtCity] Text ( 20 ); {body}"); + Invoke(database, "Close"); + + using (var db = JetDatabase.Open(byLibRed, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery($"CREATE PROCEDURE ByCity [Forms]![frmMenu]![txtCity] Text(20) AS {body}"); + + string aceSql, libredSql; + using (var db = JetDatabase.Open(byAce, readOnly: true)) + { + aceSql = db.Catalog.Views["ByCity"]; + Assert.Equal("Forms!frmMenu!txtCity", db.Catalog.QueryParameters["ByCity"].Single().Name); + Assert.Equal(["ALFKI"], new QueryEngine(db).ExecuteQuery("EXECUTE ByCity 'Berlin'").Rows.Select(r => r[0])); + } + using (var db = JetDatabase.Open(byLibRed, readOnly: true)) + libredSql = db.Catalog.Views["ByCity"]; + output.WriteLine($"ACE {aceSql}\nLibRed {libredSql}"); + Assert.Equal(aceSql, libredSql); + + using OleDbConnection ace = AceTestDatabase.Open(byLibRed); + using OleDbCommand run = ace.CreateCommand(); + run.CommandText = "EXECUTE ByCity 'Berlin'"; + Assert.Equal("ALFKI", run.ExecuteScalar()); + } + finally + { + TemporaryDatabase.Delete(byAce); + TemporaryDatabase.Delete(byLibRed); + } + } + + /// Runs the query through ACE and through LibRed, over a copy of Northwind with tables K and Árú made + /// by ACE, and compares each column's name and type and every value. + private void Matches(string query, Action bind, IReadOnlyDictionary? parameters) + { + string path = Copy(); + try + { + string ace; + using (OleDbConnection connection = AceTestDatabase.Open(path)) + { + foreach (string statement in Setup) + { + using OleDbCommand setup = connection.CreateCommand(); + setup.CommandText = statement; + setup.ExecuteNonQuery(); + } + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = query; + bind(command); + using OleDbDataReader reader = command.ExecuteReader(); + var columns = Enumerable.Range(0, reader.FieldCount).Select(i => $"{reader.GetName(i)}:{reader.GetFieldType(i).Name}").ToList(); + var rows = new List(); + while (reader.Read()) + rows.Add([.. Enumerable.Range(0, reader.FieldCount).Select(i => reader.IsDBNull(i) ? null : reader.GetValue(i))]); + ace = Describe(columns, rows); + } + + string libred; + using (var database = JetDatabase.Open(path, readOnly: true)) + { + var result = new QueryEngine(database).ExecuteQuery(query, parameters); + libred = Describe([.. result.ColumnNames.Zip(result.ColumnTypes, (n, t) => $"{n}:{t.Name}")], result.Rows); + } + + output.WriteLine($"ACE {ace}\nLibRed {libred}"); + Assert.Equal(ace, libred); + } + finally { TemporaryDatabase.Delete(path); } + } + + private static object? Invoke(object target, string member, params object?[] args) => + target.GetType().InvokeMember(member, System.Reflection.BindingFlags.InvokeMethod, null, target, args); + + private static string Copy() => + TemporaryDatabase.CopyPath(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "names-"); + + private static string Describe(IReadOnlyList columns, IEnumerable rows) => + $"[{string.Join(", ", columns)}] " + string.Join(" | ", rows.Select(row => + string.Join(", ", row.Select(v => v is null ? "NULL" : Convert.ToString(v, CultureInfo.InvariantCulture))))); +} diff --git a/test/LibRed.Engine.Tests/QueryNamesTests.cs b/test/LibRed.Engine.Tests/QueryNamesTests.cs new file mode 100644 index 00000000..118f96f0 --- /dev/null +++ b/test/LibRed.Engine.Tests/QueryNamesTests.cs @@ -0,0 +1,165 @@ +using LibRed; +using LibRed.Engine; +using LibRed.Sql.Parsing; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// How LibRed reads the names in a query as Access writes them: the bang, where Customers!CustomerID is +/// Customers.CustomerID and a longer chain such as Forms!frmMenu!cmbGroup names a form control, which a +/// query reaches only as a parameter it declares; a reserved word naming a column after a period or a bang; a name in +/// any script written unbracketed; and Yes, No, On and Off as True and False. ACE's own answers are in +/// QueryNamesAccessTests. +/// +public class QueryNamesTests : TempDatabaseTest +{ + private static QueryEngine Northwind(bool readOnly = true) + { + string path = TemporaryDatabase.CopyPath(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "names-"); + return new QueryEngine(TemporaryDatabase.OpenTracked(path, readOnly)); + } + + /// Northwind with a table K whose columns are named by reserved words and constants, and a table Árú + /// named, like its columns, outside ASCII. + private static QueryEngine Seeded() + { + QueryEngine engine = Northwind(readOnly: false); + foreach (string sql in new[] + { + "CREATE TABLE K ([Key] LONG, [Select] LONG, [Yes] LONG, [No] LONG, [On] LONG, [Off] LONG)", + "INSERT INTO K VALUES (1, 2, 3, 4, 5, 6)", + "CREATE TABLE [Árú] ([Név] TEXT(10), [Номер] LONG, [名前] TEXT(10), [ΑΒΓ] LONG)", + "INSERT INTO [Árú] VALUES ('x', 2, 'y', 3)", + }) + engine.ExecuteNonQuery(sql); + return engine; + } + + private static List Column(QueryEngine e, string sql, IReadOnlyDictionary? parameters = null) => + e.ExecuteQuery(sql, parameters).Rows.Select(r => r[0]).ToList(); + + private static object?[] Row(QueryEngine e, string sql) => e.ExecuteQuery(sql).Rows.Single(); + + [Theory] + [InlineData("SELECT Customers!CustomerID FROM Customers", "SELECT Customers.CustomerID FROM Customers")] + [InlineData("SELECT [Customers]![City] FROM Customers", "SELECT Customers.City FROM Customers")] + [InlineData("SELECT `Customers`!`City` FROM Customers", "SELECT Customers.City FROM Customers")] + [InlineData("SELECT c!CustomerID FROM Customers AS c WHERE c!City = 'London'", + "SELECT c.CustomerID FROM Customers AS c WHERE c.City = 'London'")] + [InlineData("SELECT City FROM Customers GROUP BY Customers!City HAVING Customers!City > 'M' ORDER BY Customers!City DESC", + "SELECT City FROM Customers GROUP BY Customers.City HAVING Customers.City > 'M' ORDER BY Customers.City DESC")] + [InlineData("SELECT o!OrderID FROM Customers AS c INNER JOIN Orders AS o ON c!CustomerID = o!CustomerID", + "SELECT o.OrderID FROM Customers AS c INNER JOIN Orders AS o ON c.CustomerID = o.CustomerID")] + [InlineData("SELECT (SELECT COUNT(*) FROM Orders WHERE Orders!CustomerID = Customers!CustomerID) FROM Customers", + "SELECT (SELECT COUNT(*) FROM Orders WHERE Orders.CustomerID = Customers.CustomerID) FROM Customers")] + [InlineData("SELECT d!X FROM (SELECT CustomerID AS X FROM Customers) AS d", "SELECT d.X FROM (SELECT CustomerID AS X FROM Customers) AS d")] + public void Two_parts_joined_by_a_bang_are_table_and_column(string bang, string dot) + { + QueryEngine e = Northwind(); + var viaBang = Column(e, bang); + Assert.NotEmpty(viaBang); + Assert.Equal(Column(e, dot), viaBang); + } + + [Theory] + [InlineData("SELECT Customers!CustomerID FROM Customers", "Customers!CustomerID")] + [InlineData("SELECT [Customers]![CustomerID] FROM Customers", "Customers]![CustomerID")] + [InlineData("SELECT Customers!CustomerID AS X FROM Customers", "X")] + [InlineData("SELECT Customers.CustomerID FROM Customers", "CustomerID")] + public void An_unaliased_bang_column_is_named_as_written(string sql, string name) => + Assert.Equal(name, Northwind().ExecuteQuery(sql).ColumnNames.Single()); + + // The name is the column's, so an outer query reaches it by that name and not by the column's own. + [Fact] + public void A_derived_tables_bang_column_goes_by_its_written_name() + { + QueryEngine e = Northwind(); + const string inner = "(SELECT Customers!City FROM Customers WHERE CustomerID = 'ALFKI')"; + Assert.Equal(["Berlin"], Column(e, $"SELECT [Customers!City] FROM {inner}")); + Assert.Throws(() => Column(e, $"SELECT City FROM {inner}")); + } + + [Theory] + [InlineData("SELECT Customers ! CustomerID FROM Customers")] + [InlineData("SELECT Customers !CustomerID FROM Customers")] + [InlineData("SELECT Customers! CustomerID FROM Customers")] + public void A_bang_takes_no_space_either_side(string sql) => + Assert.Throws(() => Northwind().ExecuteQuery(sql)); + + // A form control is nothing a query can reach unless the query declares it. + [Fact] + public void An_undeclared_form_control_is_not_found() + { + var ex = Assert.Throws( + () => Column(Northwind(), "SELECT CustomerID FROM Customers WHERE CustomerID = Forms!frmMenu!cmbCustomer")); + Assert.Contains("Forms!frmMenu!cmbCustomer", ex.Message, StringComparison.Ordinal); + } + + [Theory] + [InlineData("PARAMETERS [Forms]![frmMenu]![cmbCustomer] Text(5); SELECT CustomerID FROM Customers WHERE CustomerID = Forms!frmMenu!cmbCustomer", + "Forms!frmMenu!cmbCustomer")] + [InlineData("PARAMETERS Forms!frmMenu!cmbCustomer Text(5); SELECT CustomerID FROM Customers WHERE CustomerID = [Forms]![frmMenu]![cmbCustomer]", + "Forms!frmMenu!cmbCustomer")] + // A period in the chain counts as a bang, as the Form property of a subform control is written. + [InlineData("PARAMETERS [Forms]![frmMenu]![sub].[Form]![cmbCustomer] Text(5); SELECT CustomerID FROM Customers WHERE CustomerID = Forms!frmMenu!sub!Form!cmbCustomer", + "Forms!frmMenu!sub!Form!cmbCustomer")] + public void A_declared_form_control_is_a_parameter(string sql, string name) => + Assert.Equal(["ALFKI"], Column(Northwind(), sql, new Dictionary { [name] = "ALFKI" })); + + // A two-part control reads as a table and a column, and its declaration wins over a real column of that name. + [Theory] + [InlineData("PARAMETERS Forms!txtCity Text(20); SELECT CustomerID FROM Customers WHERE City = Forms!txtCity", "Forms!txtCity")] + [InlineData("PARAMETERS Forms!txtCity Text(20); SELECT CustomerID FROM Customers WHERE City = Forms.txtCity", "Forms!txtCity")] + [InlineData("PARAMETERS Customers!City Text(20); SELECT CustomerID FROM Customers WHERE City = Customers!City", "Customers!City")] + public void A_declared_two_part_control_is_a_parameter(string sql, string name) => + Assert.Equal(["ALFKI"], Column(Northwind(), sql, new Dictionary { [name] = "Berlin" })); + + [Fact] + public void An_update_may_set_a_column_written_with_a_bang() + { + QueryEngine e = Northwind(readOnly: false); + Assert.Equal(1, e.ExecuteNonQuery("UPDATE Customers SET Customers!City = 'Ankh-Morpork' WHERE Customers!CustomerID = 'ALFKI'")); + Assert.Equal(["Ankh-Morpork"], Column(e, "SELECT City FROM Customers WHERE CustomerID = 'ALFKI'")); + } + + // Access stores a declared form control as written, brackets and all, and binds the stored query's EXECUTE + // arguments to it by position. + [Fact] + public void A_stored_query_declaring_a_form_control_executes() + { + string path = TemporaryDatabase.CopyPath(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "names-proc-"); + using (var db = JetDatabase.Open(path, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery( + "CREATE PROCEDURE ByCity [Forms]![frmMenu]![txtCity] Text(20) AS " + + "SELECT CustomerID FROM Customers WHERE City = Forms!frmMenu!txtCity"); + + using (var db = JetDatabase.Open(path, readOnly: true)) + { + Assert.Equal("Forms!frmMenu!txtCity", db.Catalog.QueryParameters["ByCity"].Single().Name); + Assert.Equal(["ALFKI"], Column(new QueryEngine(db), "EXECUTE ByCity 'Berlin'")); + } + } + + [Fact] + public void A_reserved_word_names_a_column_after_a_period_or_a_bang() => + Assert.Equal([1, 2, 1, 2], Row(Seeded(), "SELECT K.Key, K.Select, K!Key, K!Select FROM K")); + + [Fact] + public void A_name_in_any_script_needs_no_brackets() + { + QueryEngine e = Seeded(); + Assert.Equal(["x", 2, "y", 3], Row(e, "SELECT Név, Номер, 名前, ΑΒΓ FROM Árú")); + Assert.Equal(["x"], Row(e, "SELECT a.Név FROM Árú AS a WHERE a.ΑΒΓ = 3")); + } + + [Fact] + public void Yes_no_on_and_off_are_true_and_false() => + Assert.Equal([true, false, true, false], Row(Seeded(), "SELECT Yes, No, On, Off FROM K")); + + // Unqualified, the constant wins over a column of that name; qualified, it is the column. + [Fact] + public void A_qualified_yes_is_the_column() => + Assert.Equal([true, 3, false, 4, true, 5, false, 6], + Row(Seeded(), "SELECT Yes, K.Yes, No, K.No, On, K.On, Off, K!Off FROM K")); +} From 4d01ba9005738e0c8bee2224ad1b8590ad66cd34 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sat, 26 Sep 2026 16:59:32 +0800 Subject: [PATCH 19/88] Keep a property's flag byte and its block's type through the property blob The flag byte is a bit field, not a DDL boolean: Access writes 0x80 on its own account, and every stored query of one example database carries it. Reading refused anything but 0x00/0x01, which would stop a table carrying such a property from loading; the byte is now kept whole and written back. A value block's type says what owns it, and an index's block (0x0002, by mdbtools' account) is named for the index, usually its column's name. Every block was read as a column's and written back as one, so a column's Required or DefaultValue could come from an index, and a DROP, RENAME or ALTER of the column moved the index's properties with it. Each property now keeps its block type, and the column and table accessors match only their own block. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Catalog/PropertyBlob.cs | 92 ++++++++++++------- .../LibRed.Core/Storage/TableCreator.cs | 13 +-- src/LibRed/docs/format/appendix-structures.md | 2 +- src/LibRed/docs/format/system-catalog.md | 27 +++--- .../PropertyBlobRoundTripTests.cs | 50 +++++++++- 5 files changed, 125 insertions(+), 59 deletions(-) diff --git a/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs b/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs index 3e411530..cca4d90b 100644 --- a/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs +++ b/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs @@ -11,9 +11,10 @@ namespace LibRed.Catalog; /// Layout (verified against an ACE-created table, §11): a 4-byte signature (MR2\0 for ACE, /// KKD\0 for older MDB) followed by blocks. Each block is [int length][short type][body], /// the length covering the whole block. Type 0x80 is the property-name pool -/// ([short len][UTF-16 name] repeated); other blocks are a per-owner value map: +/// ([short len][UTF-16 name] repeated); other blocks are a per-owner value map, whose type says what +/// owns it — 0x0000 the table, 0x0001 a column, and by mdbtools' account 0x0002 an index: /// [short ownerRecLen][short 0][short nameLen][owner name] then property entries -/// [short entryLen][byte DDL flag][byte dataType][short nameIndex][short valueLen][UTF-16 value]. +/// [short entryLen][byte flags][byte dataType][short nameIndex][short valueLen][UTF-16 value]. /// public static class PropertyBlob { @@ -45,16 +46,31 @@ public static class PropertyBlob /// DecimalPlaces, a designer ValidationRule/Format, …) round-trips byte-for-byte even /// though its string is only a best-effort UTF-16 decode. It is null for a /// property LibRed constructs, which is then encoded from /. - /// preserves the entry's DDL-property flag. DDL properties are protected as - /// part of the object's definition and some are only recognised correctly by Access when flagged. It - /// defaults to because the properties LibRed currently creates are schema - /// properties such as DefaultValue, Required, and CheckConstraints. + /// is the entry's flag byte, kept whole and written back unchanged. It is a bit + /// field: 0x01 marks a DDL property, protected as part of the object's definition, and Access writes + /// 0x80 on its own account (seen on stored queries). It defaults to 0x01 because the properties + /// LibRed creates are schema properties such as DefaultValue, Required and + /// CheckConstraints. + /// is the type of the value block the property was read from — by mdbtools' account an + /// index's properties sit in a block of their own, under the index's name, which is usually its column's — and + /// is written back as that type. It is null for a property LibRed constructs, which goes in the table's block or its column's. #pragma warning disable CA1716 // "Property" is the format's own name for this record, and it is nested in PropertyBlob. public readonly record struct Property( string Owner, string Name, string Value, JetDataType Type = JetDataType.Memo, byte[]? RawValue = null) { - /// Whether this entry is a DDL/property-definition property. - public bool IsDdl { get; init; } = true; + /// The entry's flag byte. + public byte Flags { get; init; } = DdlFlag; + + /// The type of the value block the property was read from; null for a constructed one. + public ushort? Block { get; init; } + + /// Whether this is a property of the column named , or of the table for + /// "" — and not of an index that shares the name. + public bool IsOwnedBy(string owner) => + BlockType == (owner.Length == 0 ? TableBlock : ColumnBlock) + && string.Equals(Owner, owner, StringComparison.OrdinalIgnoreCase); + + internal ushort BlockType => Block ?? (Owner.Length == 0 ? TableBlock : ColumnBlock); } #pragma warning restore CA1716 @@ -82,7 +98,7 @@ public static byte[] Write(IReadOnlyList properties, ReadOnlySpan blob, string owner, foreach (string n in names) AppendString(namesBody, n); AppendBlock(result, NameListBlock, namesBody); foreach (byte[] b in otherBlocks) result.AddRange(b); - AppendOwnerBlock(result, owner, newProps, nameIndex); + AppendOwnerBlock(result, owner.Length == 0 ? TableBlock : ColumnBlock, owner, newProps, nameIndex); return [.. result]; } - /// Appends one owner's value block: the owner record then a property entry per property. - private static void AppendOwnerBlock(List blob, string owner, IEnumerable props, Dictionary nameIndex) + /// Appends one owner's value block of the given type: the owner record then a property entry per + /// property. + private static void AppendOwnerBlock( + List blob, ushort type, string owner, IEnumerable props, Dictionary nameIndex) { Property[] propertyArray = props.ToArray(); ValidateOwnerProperties(owner, propertyArray, nameIndex); @@ -140,15 +158,14 @@ private static void AppendOwnerBlock(List blob, string owner, IEnumerable< byte[] value = PropertyValue(p); var entry = new List(); AppendUInt16(entry, (ushort)(2 + 1 + 1 + 2 + 2 + value.Length)); // entry length - entry.Add(p.IsDdl ? DdlFlag : (byte)0x00); + entry.Add(p.Flags); entry.Add((byte)p.Type); AppendUInt16(entry, (ushort)nameIndex[p.Name]); AppendUInt16(entry, (ushort)value.Length); entry.AddRange(value); body.AddRange(entry); } - // A table-level map (empty owner) uses block type 0x00; a column map uses 0x01. - AppendBlock(blob, owner.Length == 0 ? TableBlock : ColumnBlock, body); + AppendBlock(blob, type, body); } /// @@ -168,9 +185,9 @@ public static byte[] RemoveOwner(ReadOnlySpan blob, string owner) result.AddRange(blob[..4]); // signature foreach (ParsedBlock block in parsed.Blocks) { - bool drop = false; - if (block.Type != NameListBlock) - drop = string.Equals(ReadOwner(block.Body), owner, StringComparison.OrdinalIgnoreCase); + // Only the column's own block: an index's can carry the same name. + bool drop = block.Type == ColumnBlock + && string.Equals(ReadOwner(block.Body), owner, StringComparison.OrdinalIgnoreCase); if (!drop) result.AddRange(block.Raw); } return [.. result]; @@ -193,7 +210,7 @@ public static byte[] RenameOwner(ReadOnlySpan blob, string oldOwner, strin result.AddRange(blob[..4]); // signature foreach (ParsedBlock block in parsed.Blocks) { - if (block.Type == NameListBlock + if (block.Type != ColumnBlock || !string.Equals(ReadOwner(block.Body), oldOwner, StringComparison.OrdinalIgnoreCase)) { result.AddRange(block.Raw); @@ -239,7 +256,7 @@ public static IReadOnlyDictionary ReadColumnDefaults(IReadOnlyLi { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (Property p in properties) - if (p.Owner.Length > 0 && p.Name == DefaultValueProperty) + if (p.BlockType == ColumnBlock && p.Name == DefaultValueProperty) result[p.Owner] = p.Value; return result; } @@ -250,7 +267,7 @@ public static IReadOnlySet ReadRequiredColumns(IReadOnlyList p { var result = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (Property p in properties) - if (p.Owner.Length > 0 && p.Name == RequiredProperty && p.Value == "1") + if (p.BlockType == ColumnBlock && p.Name == RequiredProperty && p.Value == "1") result.Add(p.Owner); return result; } @@ -264,7 +281,7 @@ public static (string? Rule, string? Text) ReadValidation(IReadOnlyList 0 ? p.Value : null; else if (p.Name == ValidationTextProperty) text = p.Value.Length > 0 ? p.Value : null; } @@ -281,7 +298,7 @@ public static (string? Expression, JetDataType? ResultType) ReadCalculated(IRead JetDataType? resultType = null; foreach (Property p in properties) { - if (!string.Equals(p.Owner, owner, StringComparison.OrdinalIgnoreCase)) continue; + if (!p.IsOwnedBy(owner)) continue; if (p.Name == ExpressionProperty) expression = p.Value.Length > 0 ? p.Value : null; else if (p.Name == ResultTypeProperty && p.RawValue is { Length: > 0 } raw) resultType = (JetDataType)raw[0]; @@ -295,7 +312,7 @@ public static (string? Expression, JetDataType? ResultType) ReadCalculated(IRead public static IReadOnlyList<(string Name, string Expression)> ReadCheckConstraints(IReadOnlyList properties) { foreach (Property p in properties) - if (p.Owner.Length == 0 && p.Name == CheckConstraintsProperty) + if (p.IsOwnedBy("") && p.Name == CheckConstraintsProperty) return ParseCheckList(p.Value); return []; } @@ -364,7 +381,7 @@ private static ParsedBlob Parse(ReadOnlySpan blob) var properties = new List(); foreach (ParsedBlock block in blocks) - if (block.Type != NameListBlock) ReadProperties(block.Body, names, properties); + if (block.Type != NameListBlock) ReadProperties(block.Type, block.Body, names, properties); return new ParsedBlob(blocks, names, properties); } @@ -398,7 +415,7 @@ private static string ReadOwner(ReadOnlySpan body) return Encoding.Unicode.GetString(body.Slice(6, ownerLength)); } - private static void ReadProperties(ReadOnlySpan body, List names, List properties) + private static void ReadProperties(ushort block, ReadOnlySpan body, List names, List properties) { string owner = ReadOwner(body); int pos = BinaryPrimitives.ReadUInt16LittleEndian(body[..2]); @@ -412,9 +429,6 @@ private static void ReadProperties(ReadOnlySpan body, List names, if (entryLength < 8 || entryLength > body.Length - pos || valueLength != entryLength - 8) throw new InvalidDataException( $"Property entry at {pos} has inconsistent entry/value lengths {entryLength}/{valueLength}."); - byte ddlFlag = body[pos + 2]; - if (ddlFlag is not (0x00 or DdlFlag)) - throw new InvalidDataException($"Property entry at {pos} has unsupported flag 0x{body[pos + 2]:X2}."); if (nameIndex >= names.Count) throw new InvalidDataException( $"Property entry at {pos} names pool index {nameIndex}, but the pool has {names.Count} entries."); @@ -424,7 +438,11 @@ private static void ReadProperties(ReadOnlySpan body, List names, string value = dataType == JetDataType.Boolean ? (raw.Length > 0 && raw[0] != 0 ? "1" : "0") : Encoding.Unicode.GetString(raw); - properties.Add(new Property(owner, names[nameIndex], value, dataType, raw.ToArray()) { IsDdl = ddlFlag != 0 }); + properties.Add(new Property(owner, names[nameIndex], value, dataType, raw.ToArray()) + { + Flags = body[pos + 2], + Block = block, + }); pos += entryLength; } } @@ -481,16 +499,20 @@ private static byte[] PropertyValue(Property property) => property.RawValue ? [(byte)(property.Value is "1" or "true" or "True" ? 1 : 0)] : Encoding.Unicode.GetBytes(property.Value)); - private static IEnumerable<(string Owner, List Properties)> GroupByOwnerPreservingOrder(IReadOnlyList properties) + /// The properties grouped into value blocks — one per block type and owner, since an index's block + /// can carry the same name as a column's — in the order each block first appears. + private static IEnumerable<(ushort Block, string Owner, List Properties)> GroupByOwnerPreservingOrder( + IReadOnlyList properties) { - var order = new List(); - var byOwner = new Dictionary>(); + var order = new List<(ushort, string)>(); + var byOwner = new Dictionary<(ushort, string), List>(); foreach (Property p in properties) { - if (!byOwner.TryGetValue(p.Owner, out var list)) { byOwner[p.Owner] = list = []; order.Add(p.Owner); } + var key = (p.BlockType, p.Owner); + if (!byOwner.TryGetValue(key, out var list)) { byOwner[key] = list = []; order.Add(key); } list.Add(p); } - return order.Select(o => (o, byOwner[o])); + return order.Select(k => (k.Item1, k.Item2, byOwner[k])); } private static void AppendBlock(List blob, ushort type, List body) diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 2c2b5e48..27ab2805 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -366,7 +366,7 @@ public void Create( foreach (string version in CalculatedVersionProperties) yield return new PropertyBlob.Property( column.Name, version, CalculatedMinimumVersion, JetDataType.Text) - { IsDdl = false }; + { Flags = 0 }; } private static readonly string[] CalculatedVersionProperties = @@ -1981,8 +1981,7 @@ private static void RemoveLongValueMapEntry(TdefParts parts, int columnId) public void SetColumnDefault(string tableName, string columnName, string defaultSql) => MutateLvPropForColumn(tableName, columnName, props => { - props.RemoveAll(p => string.Equals(p.Owner, columnName, StringComparison.OrdinalIgnoreCase) - && p.Name == PropertyBlob.DefaultValueProperty); + props.RemoveAll(p => p.IsOwnedBy(columnName) && p.Name == PropertyBlob.DefaultValueProperty); props.Add(new PropertyBlob.Property(columnName, PropertyBlob.DefaultValueProperty, defaultSql)); }); @@ -1991,8 +1990,7 @@ public void SetColumnDefault(string tableName, string columnName, string default /// Required (NOT NULL) property survive — ACE-verified. A no-op if the column had no default. public void DropColumnDefault(string tableName, string columnName) => MutateLvPropForColumn(tableName, columnName, props => - props.RemoveAll(p => string.Equals(p.Owner, columnName, StringComparison.OrdinalIgnoreCase) - && p.Name == PropertyBlob.DefaultValueProperty)); + props.RemoveAll(p => p.IsOwnedBy(columnName) && p.Name == PropertyBlob.DefaultValueProperty)); /// Sets or clears a column's Required (NOT NULL) property in the table's /// MSysObjects.LvProp blob — ALTER TABLE … ALTER COLUMN … NOT NULL / NULL. A required column carries @@ -2003,8 +2001,7 @@ public void DropColumnDefault(string tableName, string columnName) public void SetColumnRequired(string tableName, string columnName, bool required) => MutateLvPropForColumn(tableName, columnName, props => { - props.RemoveAll(p => string.Equals(p.Owner, columnName, StringComparison.OrdinalIgnoreCase) - && p.Name == PropertyBlob.RequiredProperty); + props.RemoveAll(p => p.IsOwnedBy(columnName) && p.Name == PropertyBlob.RequiredProperty); if (required) props.Add(PropertyBlob.Bool(columnName, PropertyBlob.RequiredProperty, true)); }); @@ -2245,7 +2242,7 @@ or JetDataType.FixedPoint private static byte[] ReplaceTableProperty(byte[] blob, string name, string? value) { var props = PropertyBlob.Read(blob).ToList(); - props.RemoveAll(p => p.Owner.Length == 0 && p.Name == name); + props.RemoveAll(p => p.IsOwnedBy("") && p.Name == name); if (value is not null) props.Add(new PropertyBlob.Property("", name, value)); return PropertyBlob.Write(props, blob.Length >= 4 ? blob.AsSpan(0, 4) : default); diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index 8f04af14..3ae0b77d 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -260,7 +260,7 @@ Global maps, located by page 0: free pages at `0x18` (page 1 row 0 as ACE writes - **MSysACEs** (4 cols): `ObjectId`, `SID`, `ACM`, `FInheritable` — two rows per object. - **MSysQueries** (8 cols): `ObjectId`, `Attribute`, `Flag`, `Name1`, `Name2`, `Expression`, `Order`, `LvExtra`; PK `(ObjectId, Attribute, Order)`. - **MSysRelationships**: `szRelationship`, `szObject`, `szColumn`, `szReferencedObject`, `szReferencedColumn`, `icolumn`, `ccolumn`, `grbit` (`0x02` don't-enforce, `0x100` cascade-update, `0x1000` cascade-delete, `0x2000` delete-set-null). -- **LvProp blob:** `MR2\0` signature, then `[int len][short type][body]` blocks; type `0x80` = name pool; per-owner entries are `[short entryLen][byte DDL flag][byte dataType][short nameIndex][short valueLen][value]`. DDL flag `0x01` marks a definition-protected/schema property and `0x00` an ordinary property; it is preserved per entry. Values include `DefaultValue`, `Required`, and `CheckConstraints`. +- **LvProp blob:** `MR2\0` signature, then `[int len][short type][body]` blocks; type `0x80` = name pool; value-block type `0x00` = table, `0x01` = column (`0x02` = index, unverified); per-owner entries are `[short entryLen][byte flags][byte dataType][short nameIndex][short valueLen][value]`. The flag byte is a bit field — `0x01` marks a definition-protected/schema property, and `0x80` also occurs in Access-written files — preserved whole per entry. Values include `DefaultValue`, `Required`, and `CheckConstraints`. --- diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index 1d75970f..9f9a3129 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -67,22 +67,27 @@ > length covering the whole block. Type `0x80` is the **property-name pool** (`[short len][UTF-16 > name]` repeated, indexed 0,1,…). Other blocks are a **per-owner value map** (owner = a column name, > or `""` for the table): `[short ownerRecLen][short 0][short nameLen][owner name]` then property - > entries `[short entryLen][byte DDL flag][byte dataType][short nameIndex][short valueLen][value]`. + > entries `[short entryLen][byte flags][byte dataType][short nameIndex][short valueLen][value]`. > The owner record's second field is **always zero** (verified). - > The per-entry flag is `0x01` for a **DDL/property-definition property** and `0x00` for an ordinary - > property. A set flag makes the property definition-protected (`dbSecWriteDef` permission is needed to - > change/delete it), and Access only recognises some properties when the classification is correct - > (both unverified against ACE). Observed in files: `DefaultValue`, `Required`, `CheckConstraints`, `GUID`, - > and `ResultType` are `0x01`, while `Title`, `Author`, `AccessVersion`, and datasheet-layout properties - > are `0x00`. `ValidationRule`/`ValidationText` are classed as DDL and `Caption`/`Description` as ordinary - > (unverified). The flag is independent per entry; it is not a file-version, encryption, owner, or - > data-type marker. LibRed accepts the two observed values, preserves both the flag and raw value read for every property, - > and defaults newly constructed schema properties to `0x01`. The + > The per-entry flag byte is a **bit field**. Bit `0x01` marks a **DDL/property-definition property**; an + > ordinary property has it clear. A set bit makes the property definition-protected (`dbSecWriteDef` + > permission is needed to change/delete it), and Access only recognises some properties when the + > classification is correct (both unverified against ACE). Observed in files: `DefaultValue`, `Required`, + > `CheckConstraints`, `GUID`, and `ResultType` are `0x01`, while `Title`, `Author`, `AccessVersion`, and + > datasheet-layout properties are `0x00`. `ValidationRule`/`ValidationText` are classed as DDL and + > `Caption`/`Description` as ordinary (unverified). Bit **`0x80`** also occurs in Access-written files: every + > stored query of one example database carries a `0x80` entry in its own (`0x00`) block. mdbtools reads it as + > "store the value without running the property's handler", set by Access itself and by no DAO call + > (unverified). The flag is independent per entry; it is not a file-version, encryption, owner, or data-type + > marker. LibRed accepts any flag byte, writes it back unchanged, and defaults newly constructed schema + > properties to `0x01`. The > `dataType` is an ordinary **`JetDataType` code** (the same byte used by column descriptors and > MSysQueries): **`0x0C`** (Memo) for a text value stored as **UTF-16**, **`0x01`** (Boolean) for a single > **0/1 byte**, and — on the `MSysDb` object's UI/nav settings only — `0x0A` (Text), `0x02`/`0x03`/`0x04` > (Byte/Int16/Int32). The value-block **type** is `0x01` for a column-owned map and `0x00` for the - > table-owned map (empty owner name). A `DefaultValue` (column property) is the expression's **source + > table-owned map (empty owner name). mdbtools gives `0x02` as an **index**-owned map, named for the index — + > which Access usually names for its column — though no example file has one (unverified). LibRed keeps each + > block's type as read, so a column's properties are only ever those in a `0x01` block of its name. A `DefaultValue` (column property) is the expression's **source > text** (e.g. `42`, `'hi'`) — its evaluation semantics (what an expression may contain, the > DDL-parser-vs-expression-service split) are in [page-02c-default-values.md](page-02c-default-values.md); > table-level `CHECK` constraints are a single **table** property named `CheckConstraints` whose value is a diff --git a/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs b/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs index efb2e76b..533ee84e 100644 --- a/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs +++ b/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs @@ -7,8 +7,8 @@ namespace LibRed.Core.Tests; // The LvProp property blob must round-trip EVERY property faithfully — including ones LibRed does not model // (a numeric DecimalPlaces, a designer ValidationRule/Format) — because an ALTER that edits a table's defaults // or nullability rewrites the whole blob (TableCreator.MutateLvPropForColumn does Read -> Write). Property.RawValue -// and IsDdl preserve each entry's exact stored value bytes and DDL classification, so an unmodelled property -// survives rather than being mangled or silently converted into a definition-protected property. +// and Flags preserve each entry's exact stored value bytes and flag byte, so an unmodelled property survives +// rather than being mangled or silently converted into a definition-protected property. public class PropertyBlobRoundTripTests { [Fact] @@ -20,7 +20,7 @@ public void Read_write_preserves_an_unmodelled_numeric_property_and_survives_an_ new("Price", "DecimalPlaces", "", JetDataType.Byte, [2]), // unmodelled, numeric new("Price", "Format", "Currency", JetDataType.Text, Encoding.Unicode.GetBytes("Currency")), // unmodelled, text new("Price", "Caption", "Retail price", JetDataType.Memo, - Encoding.Unicode.GetBytes("Retail price")) { IsDdl = false }, // ordinary designer property + Encoding.Unicode.GetBytes("Retail price")) { Flags = 0 }, // ordinary designer property PropertyBlob.Bool("Price", PropertyBlob.RequiredProperty, true), // modelled }; byte[] blob = PropertyBlob.Write(original); @@ -43,7 +43,7 @@ public void Read_write_preserves_an_unmodelled_numeric_property_and_survives_an_ Assert.Equal("Currency", format.Value); PropertyBlob.Property caption = Assert.Single(after, p => p.Name == "Caption"); - Assert.False(caption.IsDdl); + Assert.Equal(0, caption.Flags); Assert.Equal(Encoding.Unicode.GetBytes("Retail price"), caption.RawValue); Assert.Equal("42", Assert.Single(after, p => p.Name == PropertyBlob.DefaultValueProperty).Value); @@ -79,4 +79,46 @@ public void Replacing_the_check_property_keeps_the_tables_other_properties() Assert.Contains(PropertyBlob.Read(updated), p => p.Owner == "V" && p.Value == "0"); Assert.Equal(blob[..4], updated[..4]); // and the signature is carried across, not restamped } + + // The flag byte is a bit field, not a DDL boolean: Access writes 0x80 on its own account (every stored query + // in one of the example databases carries it). And an index's properties sit in a block of type 0x0002 under + // the index's name, which is usually its column's. Reading such a blob used to throw on the flag, and writing + // it back re-typed the index's block as the column's, where the column's accessors — and a DROP or RENAME of + // the column — took it for the column's own. + [Fact] + public void A_flag_byte_and_an_index_block_round_trip_and_stay_the_indexs() + { + const ushort IndexBlock = 0x0002; + byte[] blob = PropertyBlob.Write( + [ + new PropertyBlob.Property("", "Replicable", "T", JetDataType.Text) { Flags = 0x80 }, + new PropertyBlob.Property("CustomerID", PropertyBlob.DefaultValueProperty, "'X'"), + new PropertyBlob.Property("CustomerID", "Caption", "Customer", JetDataType.Text) { Flags = 0x81 }, + PropertyBlob.Bool("CustomerID", PropertyBlob.RequiredProperty, true) with { Block = IndexBlock }, + ]); + + IReadOnlyList read = PropertyBlob.Read(blob); + Assert.Equal(0x80, Assert.Single(read, p => p.Name == "Replicable").Flags); + Assert.Equal(0x81, Assert.Single(read, p => p.Name == "Caption").Flags); + Assert.Equal(IndexBlock, Assert.Single(read, p => p.Name == PropertyBlob.RequiredProperty).Block); + Assert.Equal(blob, PropertyBlob.Write([.. read], blob.AsSpan(0, 4))); + + // The index's Required is not the column's. + Assert.Empty(PropertyBlob.ReadRequiredColumns(read)); + Assert.Equal("'X'", PropertyBlob.ReadColumnDefaults(read)["CustomerID"]); + + // Clearing the column's Required, as ALTER COLUMN … NULL does, leaves the index's alone. + var edited = read.ToList(); + edited.RemoveAll(p => p.IsOwnedBy("CustomerID") && p.Name == PropertyBlob.RequiredProperty); + Assert.Equal(blob, PropertyBlob.Write(edited, blob.AsSpan(0, 4))); + + // Dropping or renaming the column moves only the column's block. + IReadOnlyList dropped = PropertyBlob.Read(PropertyBlob.RemoveOwner(blob, "CustomerID")); + Assert.DoesNotContain(dropped, p => p.Name == PropertyBlob.DefaultValueProperty); + Assert.Contains(dropped, p => p.Owner == "CustomerID" && p.Block == IndexBlock); + + IReadOnlyList renamed = PropertyBlob.Read(PropertyBlob.RenameOwner(blob, "CustomerID", "CustID")); + Assert.Equal("CustID", Assert.Single(renamed, p => p.Name == PropertyBlob.DefaultValueProperty).Owner); + Assert.Equal("CustomerID", Assert.Single(renamed, p => p.Block == IndexBlock).Owner); + } } From 1110c660db0f343fce522eb63deb0c359ce038b8 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sat, 26 Sep 2026 17:49:39 +0800 Subject: [PATCH 20/88] Document the catalog column flags and write them only where Access does Column flag 0x10 is set on every column of MSysObjects, MSysACEs, MSysQueries, MSysRelationships and MSysComplexColumns and on no other, and 0x20 on the two security-identifier columns, MSysObjects.Owner and MSysACEs.SID. The spec listed neither and said the unlisted bits were zero in every file. The extended flags gain the attachment value column's 0x10, and the 0x04/0x08 a complex column's flat table sets. A new database gave the MSysComplexType_* templates the catalog flag, which Access never does; the flag was also what zeroed their 0x09. That is now its own switch, and the attachment template carries its extended flag, so every system column a new database writes matches Access's byte for byte. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/ColumnDef.cs | 13 +++++-- src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs | 27 ++++++++++----- .../LibRed.Core/Formats/JetFormatBase.cs | 4 ++- .../LibRed.Core/Storage/DatabaseCreator.cs | 34 +++++++++++-------- src/LibRed/docs/format/appendix-structures.md | 4 +-- src/LibRed/docs/format/page-02b-columns.md | 31 +++++++++++++---- .../docs/format/page-02d-constraints.md | 2 +- .../LibRed.Core.Tests/DatabaseCreatorTests.cs | 31 +++++++++++++++++ 8 files changed, 110 insertions(+), 36 deletions(-) diff --git a/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs b/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs index b9f041a8..2ffd536b 100644 --- a/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs +++ b/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs @@ -123,7 +123,16 @@ public sealed class ColumnDef /// them (the faithful round-trip rule). Null for a freshly-built (never-read) column. public byte[]? RawDescriptor { get; init; } - /// Undocumented flag bits (byte 0x0F) to force-set — the system-catalog column marker (0x10) and - /// security-identifier marker (0x20) Access sets on MSys* columns. 0 for ordinary columns. + /// Catalog flag bits (byte 0x0F) to force-set on a created column — the system-catalog column marker + /// (0x10) and security-identifier marker (0x20) Access sets on the columns of its own catalog tables. 0 for + /// every other column. public byte SystemFlags { get; init; } + + /// Whether a created column belongs to a table the engine makes for itself, which writes 0x09 — the + /// second copy of the column id — as zero. + internal bool IsEngineColumn { get; init; } + + /// Extended flag bits (byte 0x10) LibRed does not model, to set on a created column — the attachment + /// value column marker (0x10). 0 for every other column. + internal byte ExtendedFlags { get; init; } } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs b/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs index bd6165d8..e06fab30 100644 --- a/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs +++ b/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs @@ -24,9 +24,10 @@ public sealed record ColumnSpec( // so a rebuild preserves unmodeled bytes. Both null for an ordinary CREATE/ADD column. See ColumnDef.RawDescriptor. int? ColumnId = null, byte[]? RawDescriptor = null, - // Undocumented flag bits (0x0F) Access sets on system-table columns: 0x10 marks a system-catalog column, - // 0x20 additionally marks a security-identifier column (MSysObjects.Owner, MSysACEs.SID). User-table - // columns leave these clear. Verified against real files; the desktop engine expects them on MSys* columns. + // The catalog flag bits (0x0F) Access sets on the columns of its own catalog — MSysObjects, MSysACEs, + // MSysQueries, MSysRelationships and MSysComplexColumns: 0x10 on every one, and 0x20 as well on the two that + // hold a security identifier (MSysObjects.Owner, MSysACEs.SID). Every other column leaves them clear, the + // other MSys* tables' included (verified; see page-02b-columns.md). byte SystemFlags = 0, // WITH COMPRESSION on a Text/Memo column: the 0x10 extended flag bit 0x01. Off unless asked for, which // is what ACE does for a column declared without it (LongTextStorageAccessTests). @@ -35,7 +36,14 @@ public sealed record ColumnSpec( // ordinary column. They are separate from Type because Type carries the descriptor's *promoted* storage // type, which need not be the result type at all — build one with Calculated() rather than by hand. string? CalculatedExpression = null, - JetDataType? CalculatedResultType = null) + JetDataType? CalculatedResultType = null, + // A column of a table the engine creates for itself, which leaves 0x09 — the second copy of the column id — + // zero. A catalog column (SystemFlags) is one already; this marks the rest, such as the MSysComplexType_* + // templates, which carry no catalog flag. + bool IsEngineColumn = false, + // Extended flag bits (0x10) LibRed does not model, to set on a created column: 0x10 on an attachment value + // column (FileData, FileFlags, FileName, FileTimeStamp, FileType, FileURL). + byte ExtendedFlags = 0) { /// A calculated column of computing . /// ACE stores one **always variable-length**, with the descriptor carrying the *promoted* type and a @@ -568,6 +576,8 @@ int VarTableIndex(int i) => CalculatedExpression = s.CalculatedExpression, CalculatedResultType = s.CalculatedResultType, SystemFlags = s.SystemFlags, + IsEngineColumn = s.IsEngineColumn || s.SystemFlags != 0, + ExtendedFlags = s.ExtendedFlags, Precision = EffectivePrecision(s), Scale = s.Scale, // Numeric columns carry no collation (their 0x0B/0x0C bytes are precision/scale); every @@ -615,14 +625,15 @@ public static byte[] BuildColumnDescriptor(ColumnDef c, JetFormatBase format) // tracking 0x05 once an ALTER COLUMN type change burns a new id there (§3.8). if (c.RawDescriptor is null or { Length: 0 }) BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnSecondaryNumberOffset, 2), - (ushort)(c.SystemFlags != 0 ? 0 : c.ColumnId)); + (ushort)(c.IsEngineColumn ? 0 : c.ColumnId)); // Offset 7 = variable-table index (count of variable columns with a smaller id), stored on fixed columns // too. Prefer the precomputed value; fall back to the legacy rule (0 for fixed) when unset (ADD COLUMN). BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnVariableIndexOffset, 2), (ushort)(c.VariableTableIndex >= 0 ? c.VariableTableIndex : (c.IsFixedLength ? 0 : c.VariableIndex))); WriteLocaleUnion(d, c.Type, c.Precision, c.Scale, c.Collation, format); - // 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). + // Compose the flag byte (0x0F) from every bit a user column models, plus the catalog bits a created system + // column asks for; the rest — an existing column's catalog bits — survive from the original. Likewise the + // extended-flag byte (0x10), whose unmodelled bits the complex columns' flat tables set. byte flags = (byte)( (c.IsUpdatable ? JetFormatBase.ColumnFlagUpdatable : 0) | (c.IsFixedLength ? JetFormatBase.ColumnFlagFixedLength : 0) @@ -634,7 +645,7 @@ public static byte[] BuildColumnDescriptor(ColumnDef c, JetFormatBase format) byte extFlags = (byte)( (c.SupportsCompressedUnicode ? JetFormatBase.ColumnExtFlagCompressedUnicode : 0) | (c.IsCalculated ? JetFormatBase.ColumnExtFlagCalculated : 0)); - d[format.ColumnExtendedFlagsOffset] = (byte)((d[format.ColumnExtendedFlagsOffset] & ~JetFormatBase.ColumnExtFlagsDocumented) | extFlags); + d[format.ColumnExtendedFlagsOffset] = (byte)((d[format.ColumnExtendedFlagsOffset] & ~JetFormatBase.ColumnExtFlagsDocumented) | extFlags | c.ExtendedFlags); BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnFixedOffsetOffset, 2), (ushort)c.FixedOffset); BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnLengthOffset, 2), (ushort)c.Length); diff --git a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs index 888ee1ac..d8929722 100644 --- a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs +++ b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs @@ -245,7 +245,9 @@ public abstract class JetFormatBase public const byte ColumnFlagGuidAutoNumber = 0x40; /// Column flag: a hyperlink (a Memo column presented as a hyperlink). public const byte ColumnFlagHyperlink = 0x80; - /// Mask of the documented flag bits — the complement is undocumented and preserved from raw. + /// Mask of the flag bits a user column's models. The complement — the + /// catalog bits 0x10/0x20 of a system column, and 0x08, never seen set — is preserved from + /// raw. public const byte ColumnFlagsDocumented = ColumnFlagFixedLength | ColumnFlagUpdatable | ColumnFlagAutoNumber | ColumnFlagGuidAutoNumber | ColumnFlagHyperlink; diff --git a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs index 56ffb4c7..e08318cc 100644 --- a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs @@ -95,6 +95,9 @@ public static byte[] BuildDefinitionPage( // System-column flag bits: Sys = 0x10 (system-catalog column), SysSid = 0x10|0x20 (also a security id). private const byte Sys = 0x10, SysSid = 0x30; + // Extended flag 0x10: an attachment value column, in the attachment template as in every attachment flat table. + private const byte AttachmentValue = 0x10; + // MSysObjects — the system catalog. Declared in the real physical (alphabetical) descriptor order with // explicit canonical ColumnIds and system flags, exactly as an Access-created file stores it. private static readonly ColumnSpec[] MSysObjectsColumns = @@ -178,25 +181,26 @@ public static byte[] BuildDefinitionPage( ]; /// 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. + /// Value column of its element type; Attachment is the exception, carrying the file metadata. + /// Engine tables, but not part of the catalog, so their columns carry no catalog flag. 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_UnsignedByte", [new("Value", JetDataType.Byte, 1, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_Short", [new("Value", JetDataType.Int16, 2, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_Long", [new("Value", JetDataType.Int32, 4, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_IEEESingle", [new("Value", JetDataType.Single, 4, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_IEEEDouble", [new("Value", JetDataType.Double, 8, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_GUID", [new("Value", JetDataType.Guid, 16, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_Decimal", [new("Value", JetDataType.FixedPoint, 9, false, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_Text", [new("Value", JetDataType.Text, 510, false, ColumnId: 0, IsEngineColumn: true)]), ("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), + new("FileData", JetDataType.Ole, 0, false, ColumnId: 3, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + new("FileFlags", JetDataType.Int32, 4, true, ColumnId: 5, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + new("FileName", JetDataType.Text, 510, false, ColumnId: 1, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + new("FileTimeStamp", JetDataType.DateTime, 8, true, ColumnId: 4, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + new("FileType", JetDataType.Text, 510, false, ColumnId: 2, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + new("FileURL", JetDataType.Memo, 0, false, ColumnId: 0, IsEngineColumn: true, ExtendedFlags: AttachmentValue), ]), ]; diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index 3ae0b77d..c2db89a9 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -120,8 +120,8 @@ Variable section (`varOffsetTable`+`numVar`) omitted only when the table has nev | `0x0C` | 1 | Scale (Decimal) — else collation LANGID high byte (`0x04` en-US) | | `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 | +| `0x0F` | 1 | Flags: `0x01` fixed, `0x02` updatable, `0x04` auto-number, `0x10` system-catalog column, `0x20` security-identifier column, `0x40` auto-number GUID, `0x80` hyperlink | +| `0x10` | 1 | Extended flags: `0x01` compressed-Unicode capable, `0x10` attachment value column, `0xC0` calculated; `0x04`/`0x08` on a complex flat table's columns | | `0x11` | 4 | Unknown (zero) | | `0x15` | 2 | Fixed-data offset within the row's fixed region | | `0x17` | 2 | Length (bytes) | diff --git a/src/LibRed/docs/format/page-02b-columns.md b/src/LibRed/docs/format/page-02b-columns.md index 326e95c9..0e61e663 100644 --- a/src/LibRed/docs/format/page-02b-columns.md +++ b/src/LibRed/docs/format/page-02b-columns.md @@ -17,13 +17,29 @@ | `0x0D` | 1 | Collation **sort id** — the LCID's high word; `0` except for an alternate sort order (see the note below) | | `0x0E` | 1 | Collation **sort-order version**: `0` = General Legacy (Access 2000–2007), `1` = the "General" order Access 2010+ made default (a different key encoding, §10.4) | | `0x0F` | 1 | Flags (see below) | -| `0x10` | 1 | Extended flags: `0x01` compressed-Unicode capable, `0xC0` calculated column | +| `0x10` | 1 | Extended flags: `0x01` compressed-Unicode capable, `0x10` attachment value column, `0xC0` calculated column; a complex column's flat table also sets `0x04` and `0x08` (see below) | | `0x11` | 4 | Unknown (zero observed) | | `0x15` | 2 | Fixed-data offset within the row's fixed region | | `0x17` | 2 | Length (bytes) | **Flags (`0x0F`):** `0x01` fixed-length, `0x02` updatable, `0x04` auto-number, -`0x40` auto-number GUID, `0x80` hyperlink (on a Memo column). +`0x10` system-catalog column, `0x20` security-identifier column, `0x40` auto-number GUID, `0x80` hyperlink +(on a Memo column). `0x08` is set on no column seen. + +> **`0x10` and `0x20` mark the engine's own catalog (verified).** `0x10` is set on every column of +> `MSysObjects`, `MSysACEs`, `MSysQueries`, `MSysRelationships` and `MSysComplexColumns`, and on no other — +> not on the other `MSys*` tables Access creates (`MSysAccessStorage`, `MSysResources`, `MSysNavPane*`, +> `MSysNameMap`, the `MSysComplexType_*` templates) and not on any user column. `0x20` is set on exactly the +> two columns that hold a Windows SID, `MSysObjects.Owner` and `MSysACEs.SID`, always together with `0x10`. +> mdbtools also gives `0x10` to replication columns (`s_…`, `Gen_…`), which no file seen has (unverified). + +> **Extended flags beyond `0x01`/`0xC0` belong to the complex columns (verified where set).** `0x10` is set on +> exactly the six attachment value columns — `FileData`, `FileFlags`, `FileName`, `FileTimeStamp`, `FileType`, +> `FileURL` — in the `MSysComplexType_Attachment` template and in every attachment flat table, and on no other +> column. A flat table (`f__…`) also sets `0x08` on its `_` column and `0x04` on its +> `_` column. mdbtools reads `0x08` as the flat table's foreign key to the complex id, which +> Access refuses to open a flat table without; what `0x04` means is unknown (both unverified). mdbtools also +> gives `0x20` to a version-history column, which no file seen has. > **A `Complex` column (type `0x12`) is an AutoNumber, and its `0x0B` names its `MSysComplexColumns` row.** > Verified on all six complex columns across two files: `0x0F` = `0x07` on every one — fixed `0x01`, @@ -90,12 +106,13 @@ > Every sort order Access offers has a primary id below `0xFF`, so "low byte" and Windows' `PRIMARYLANGID` > (mask `0x3FF`) cannot be told apart here. -> **Every documented flag is modelled — nothing rides through raw except the reserved/unknown.** LibRed reads -> each `0x0F` bit and the whole `0x10` byte into `ColumnDef` (`IsUpdatable`/`IsGuidAutoNumber`/`IsHyperlink`, +> **Every flag a user column carries is modelled; the rest ride through raw.** LibRed reads the user-column +> bits of `0x0F` and `0x10` into `ColumnDef` (`IsUpdatable`/`IsGuidAutoNumber`/`IsHyperlink`, > `SupportsCompressedUnicode`/`IsCalculated`) and composes them back on write, so they round-trip explicitly. -> The only bytes preserved verbatim through `ColumnDef.RawDescriptor` are the genuinely reserved/unknown ones: -> the reserved words at `0x03` and `0x11`, and any *undocumented* bits of `0x0F`/`0x10` (zero in every file -> observed). +> It writes the catalog bits `0x10`/`0x20` on the system tables it creates. Everything else is preserved +> verbatim through `ColumnDef.RawDescriptor` on a rewrite: the reserved words at `0x03` and `0x11`, the catalog +> bits of an existing column, and the `0x10` bits it does not model, which the complex columns' flat tables +> set. > **Nullability, defaults and checks are *not* in the descriptor.** The column's *Required* (NOT NULL) > property is **not** encoded anywhere in the 25-byte descriptor — verified: a nullable column and a diff --git a/src/LibRed/docs/format/page-02d-constraints.md b/src/LibRed/docs/format/page-02d-constraints.md index 3d44f1f2..4e2e28b7 100644 --- a/src/LibRed/docs/format/page-02d-constraints.md +++ b/src/LibRed/docs/format/page-02d-constraints.md @@ -73,7 +73,7 @@ count, and it is gated on the total: | `0x22` | 1 | Usage-map row | | `0x23` | 3 | Usage-map page | | `0x26` | 4 | **B-tree root page** | -| `0x2A` | 4 | Unknown / reserved (zero observed). mdbtools places a 1-byte index-flags field at `+0x2A`, but ACE's effective flags are at `0x2E` and this is zero in every file checked | +| `0x2A` | 4 | Unknown / reserved — zero in every file checked. mdbtools calls it uninitialised page residue (unverified) | | `0x2E` | 2 | Flags: `0x01` unique, `0x02` ignore-nulls (`WITH IGNORE NULL` — null-keyed rows excluded from the index), `0x08` required (`WITH DISALLOW NULL` / part of a primary key), `0x80` always-set (Access 2000+). Verified vs ACE: a plain index is `0x0080`, `IGNORE NULL` `0x0082`, `DISALLOW NULL` `0x0088`, a PK `0x0089`. There is **no clustered flag**: `CLUSTERED`/`NONCLUSTERED` after `PRIMARY KEY` or `UNIQUE` in a constraint is accepted and stores nothing (the file is byte-identical without it), and DAO's `Index.Clustered` reads `False` even on an index created with it set. | | `0x30` | 4 | Unknown / reserved (zero observed) — trailing bytes of the 52-byte block | diff --git a/test/LibRed.Core.Tests/DatabaseCreatorTests.cs b/test/LibRed.Core.Tests/DatabaseCreatorTests.cs index 34f6be9f..d88c9b48 100644 --- a/test/LibRed.Core.Tests/DatabaseCreatorTests.cs +++ b/test/LibRed.Core.Tests/DatabaseCreatorTests.cs @@ -28,6 +28,37 @@ public void Synthesized_page0_header_matches_a_real_file(string fixture) Assert.Equal(real[0x00..0xA0], synth[0x00..0xA0]); } + // Every column of the system tables a new database carries is described byte-for-byte as Access describes it: + // the catalog flags 0x10/0x20 on exactly the catalog tables' columns (and not on the complex templates, which + // once carried them), the attachment template's extended flag 0x10, and the zero 0x09 of an engine table. + [Fact] + public void Synthesized_system_columns_match_a_real_file() + { + string path = TemporaryDatabase.CreatePath("libred_syscols_"); + try + { + DatabaseCreator.CreateEmpty(path); + using var created = JetDatabase.Open(path); + using var real = JetDatabase.Open(TestDatabases.NorthwindAccdb); + + string[] systemTables = + [ + "MSysObjects", "MSysACEs", "MSysQueries", "MSysRelationships", "MSysComplexColumns", + "MSysComplexType_Long", "MSysComplexType_Text", "MSysComplexType_Attachment", + ]; + foreach (string name in systemTables) + { + TableDef expected = real.Catalog.FindTable(name)!; + TableDef actual = created.Catalog.FindTable(name)!; + foreach (ColumnDef column in expected.Columns) + Assert.True(column.RawDescriptor!.AsSpan().SequenceEqual(actual.FindColumn(column.Name)!.RawDescriptor), + $"{name}.{column.Name}: Access {Convert.ToHexString(column.RawDescriptor!)}, " + + $"LibRed {Convert.ToHexString(actual.FindColumn(column.Name)!.RawDescriptor!)}"); + } + } + finally { if (File.Exists(path)) TemporaryDatabase.Delete(path); } + } + [Fact] public void Creates_an_empty_database_that_round_trips_a_user_table() { From a08c9eb3afefad0c587e7ac6c09fd578892805d6 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sat, 26 Sep 2026 19:03:11 +0800 Subject: [PATCH 21/88] Write what ACE writes across a hundred-statement run, found by whole-file diffing A new probe runs the same statements through ACE and LibRed on copies of one DAO-created database and compares the files byte for byte, both at the end of one session and statement by statement. What it found, each now verified and fixed: - A table's property blob is stored like any long value: inline up to 64 bytes, on a page above that. LibRed always used a page. - A new object's MSysACEs rows are its container's inheritable grants, the Creator's becoming the owner's, so the masks follow the database rather than the object class the three constant pairs assumed. - A foreign key's index root is allocated after its relationship's rows. - A statement's deletes run in the order the rows were found, which decides the bytes left in the space they free. - An index built over rows writes each leaf at the prefix its filling reached, not the largest its keys share. - An UPDATE moving an entry lowers the index's total and holds its unique count to it, on an index built over rows. - A long-value page leaves the free map with 257 bytes free or fewer. Both measurements now come out identical: all 104 statements, and the whole run's 74 pages. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/JetCatalog.cs | 27 +- .../LibRed.Core/Storage/CatalogWriter.cs | 39 +- .../LibRed.Core/Storage/DatabaseCreator.cs | 2 +- src/LibRed/LibRed.Core/Storage/IndexWriter.cs | 18 +- .../LibRed.Core/Storage/LongValueWriter.cs | 5 +- src/LibRed/LibRed.Core/Storage/RowInserter.cs | 43 ++- src/LibRed/LibRed.Core/Storage/Table.cs | 10 +- .../LibRed.Core/Storage/TableCreator.cs | 46 ++- src/LibRed/LibRed.Core/Storage/ViewCreator.cs | 28 +- .../Execution/StatementExecutor.cs | 5 +- src/LibRed/README.md | 1 - src/LibRed/docs/format/appendix-structures.md | 6 +- src/LibRed/docs/format/long-values.md | 14 +- src/LibRed/docs/format/page-00-database.md | 4 +- .../docs/format/page-01-data-and-rows.md | 5 + .../docs/format/page-02d-constraints.md | 23 +- .../docs/format/page-03-04-index-btree.md | 4 + src/LibRed/docs/format/system-catalog.md | 31 +- .../IndexUniqueEntryCountAccessTests.cs | 44 +++ .../LongValueFreeMapAccessTests.cs | 71 ++++ .../LvPropStorageAccessTests.cs | 102 +++++ .../PermissionRowsAccessTests.cs | 93 +++++ .../WholeFileParityProbeTest.cs | 364 ++++++++++++++++++ 23 files changed, 877 insertions(+), 108 deletions(-) create mode 100644 test/LibRed.Engine.AccessTests/LongValueFreeMapAccessTests.cs create mode 100644 test/LibRed.Engine.AccessTests/LvPropStorageAccessTests.cs create mode 100644 test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs create mode 100644 test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs diff --git a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs index 5c226663..c9344657 100644 --- a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs +++ b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs @@ -39,28 +39,29 @@ public sealed class JetCatalog(PageChannel channel, int catalogPage = 2) private Dictionary? _actionQueries; private Dictionary>? _queryParameters; private List? _complexColumns; - private (byte[] Users, byte[] Admin)? _securitySids; + private (byte[] Users, byte[] Admin, byte[] Creator)? _securitySids; private long _seenSchemaGeneration = channel.SchemaGeneration; /// All tables in the database (user and system). public IReadOnlyList Tables { get { EnsureFresh(); return _tables ??= LoadTables(); } } /// - /// The two account SIDs as this database masks them: the owner a new object takes (the Users - /// group) and the administrator its second permission row grants. Every on-disk SID is a workgroup - /// account SID XOR'd with a mask that differs per file and is stored nowhere (page-00 §2.3) — but - /// MSysObjects is owned by the Engine account in every file, ACE-written and LibRed-written alike, - /// so its own catalog row gives the mask away. An object written into a file has to carry that file's - /// SIDs, or its owner decodes to no account at all. + /// The account SIDs as this database masks them: the owner a new object takes (the Users group), the + /// administrator, and the Creator placeholder a container's inheritable grant names in place of whoever + /// creates an object in it. Every on-disk SID is a workgroup account SID XOR'd with a mask that differs per + /// file and is stored nowhere (page-00 §2.3) — but MSysObjects is owned by the Engine account in every + /// file, ACE-written and LibRed-written alike, so its own catalog row gives the mask away. An object written + /// into a file has to carry that file's SIDs, or its owner decodes to no account at all. /// - /// Falls back to the pair bakes in, which is what a file - /// this engine created carries, when the row cannot be read. - public (byte[] Users, byte[] Admin) SecuritySids + /// Falls back to the SIDs bakes in, which is what a file this + /// engine created carries, when the row cannot be read. + public (byte[] Users, byte[] Admin, byte[] Creator) SecuritySids { get { _ = Tables; // reading the catalog captures the mask from MSysObjects' own row - return _securitySids ?? (Storage.DatabaseCreator.SidUsers, Storage.DatabaseCreator.SidAdmin); + return _securitySids + ?? (Storage.DatabaseCreator.SidUsers, Storage.DatabaseCreator.SidAdmin, Storage.DatabaseCreator.SidCreator); } } @@ -68,6 +69,7 @@ public sealed class JetCatalog(PageChannel channel, int catalogPage = 2) private static readonly byte[] EngineAccount = [0x03, 0x03]; private static readonly byte[] UsersAccount = [0x02, 0x01]; private static readonly byte[] AdminAccount = [0x03, 0x01]; + private static readonly byte[] CreatorAccount = [0x03, 0x04]; private static byte[] AsMaskedInThisFile(byte[] engineOnDisk, byte[] account) => [ @@ -216,7 +218,8 @@ private List LoadTables() // MSysObjects' own row carries the Engine account's SID as this file masks it — the one anchor // for every SID a new object in this file must be written with (see SecuritySids). if (definitionPage == _catalogPage && row[ownerIndex] is byte[] { Length: 2 } engine) - _securitySids = (AsMaskedInThisFile(engine, UsersAccount), AsMaskedInThisFile(engine, AdminAccount)); + _securitySids = (AsMaskedInThisFile(engine, UsersAccount), AsMaskedInThisFile(engine, AdminAccount), + AsMaskedInThisFile(engine, CreatorAccount)); uint flags = unchecked((uint)(int)row[flagsIndex]!); // A table is "system" (excluded from the user-table list, as Access's own schema view diff --git a/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs b/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs index 50ae1ac1..387f45a8 100644 --- a/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs @@ -5,8 +5,8 @@ namespace LibRed.Storage; /// /// Writes the catalog rows every new object gets whatever it is: its MSysObjects entry, and the -/// MSysACEs pair granting the Users and Admin accounts access to it (system-catalog §11). A table, a -/// stored query and a relationship differ only in the type, container, flags and masks they pass. +/// MSysACEs rows its container's inheritable grants give it (system-catalog §11). A table, a stored query +/// and a relationship differ only in the type, container and flags they pass. /// /// /// writes the same two tables and does not use this: it is populating them @@ -43,8 +43,8 @@ public void AddObjectRow( var inserter = new RowInserter(channel, msysObjects); if (properties is { Count: > 0 }) { - // Access reads object properties only from an LVAL-page long value, not an inline one, so store - // the blob on a page (packed onto a shared LvProp page as Access does) and keep the descriptor. + // Stored as any long value is: inline up to 64 bytes, else packed onto a shared LvProp page as + // Access does. byte[] reference = inserter.StorePackedLongValue( msysObjects.RequireColumn("LvProp").ColumnId, PropertyBlob.Write(properties)); Set(msysObjects, values, "LvProp", new LongValueDescriptor(reference)); @@ -53,14 +53,33 @@ public void AddObjectRow( inserter.Insert(values, updateIndexes: true); } - /// Inserts the object's two MSysACEs rows, Users then Admin, maintaining the ObjectId - /// index so Access's security check finds them — without them it warns about permissions on opening the - /// object. The two masks are per object class, so they come from the caller. - public void AddPermissionRows(int objectId, int usersAcm, int adminAcm) + /// Inserts the object's MSysACEs rows, maintaining the ObjectId index so Access's security + /// check finds them — without them it warns about permissions on opening the object. + /// The grants are the ones the object's container passes down, derived as ACE derives them + /// (verified): the Creator account's inheritable grant becomes the owner's — the Users group — and every + /// other inheritable grant is copied for its own account, OR'd into the owner's row when it names the owner's + /// account. The owner's row comes first, the rest in the container's order. So the masks follow the database: + /// a table's owner gets 0xF00FE where the Tables container grants the Creator that alone, and 0xFFEFF where it + /// also grants Users 0xFFEFF, as Northwind's does. + public void AddPermissionRows(int objectId, int containerId) { TableDef msysAces = catalog.RequireTable("MSysACEs"); - (byte[] users, byte[] admin) = catalog.SecuritySids; - foreach ((byte[] sid, int acm) in new[] { (users, usersAcm), (admin, adminAcm) }) + int idIndex = msysAces.RequireColumn("ObjectId").Index, sidIndex = msysAces.RequireColumn("SID").Index; + int acmIndex = msysAces.RequireColumn("ACM").Index, inheritIndex = msysAces.RequireColumn("FInheritable").Index; + (byte[] users, _, byte[] creator) = catalog.SecuritySids; + + var grants = new List<(byte[] Sid, int Acm)>(); + foreach (object?[] row in new Table(channel, msysAces).Rows()) + { + if (row[idIndex] is not int id || id != containerId || row[inheritIndex] is not true) continue; + byte[] sid = (byte[])row[sidIndex]!; + if (sid.AsSpan().SequenceEqual(creator)) sid = users; + int at = grants.FindIndex(g => g.Sid.AsSpan().SequenceEqual(sid)); + if (at < 0) grants.Add((sid, (int)row[acmIndex]!)); + else grants[at] = (grants[at].Sid, grants[at].Acm | (int)row[acmIndex]!); + } + + foreach ((byte[] sid, int acm) in grants.OrderBy(g => g.Sid.AsSpan().SequenceEqual(users) ? 0 : 1)) { var values = new object?[msysAces.Columns.Count]; Set(msysAces, values, "ACM", acm); diff --git a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs index e08318cc..4450b270 100644 --- a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs @@ -230,7 +230,7 @@ private static readonly (string Name, ColumnSpec[] Columns)[] MSysComplexTypeTab internal static readonly byte[] SidUsers = Masked(0x02, 0x01); // Users group — read grantee / owner of user tables internal static readonly byte[] SidAdmin = Masked(0x03, 0x01); // admin user — full grantee private static readonly byte[] SidEngine = Masked(0x03, 0x03); // Engine (user form) — owner of system tables + DAO containers - private static readonly byte[] SidCreator = Masked(0x03, 0x04); // Creator (user form) — inheritable container grant + internal static readonly byte[] SidCreator = Masked(0x03, 0x04); // Creator (user form) — inheritable container grant /// /// Creates a new, empty database at from scratch — no DAO/ADOX. Hand-builds the diff --git a/src/LibRed/LibRed.Core/Storage/IndexWriter.cs b/src/LibRed/LibRed.Core/Storage/IndexWriter.cs index 33148b32..4635eeed 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexWriter.cs @@ -257,7 +257,7 @@ private bool UnlinkEmptyLeaf(IndexDef index, List path, CheckedIndexPage le return true; } - private static bool HasNullKey(IndexDef index, object?[] values) => + internal static bool HasNullKey(IndexDef index, object?[] values) => index.Columns.Any(c => values[c.Column.Index] is null or DBNull); /// Descends to the leaf that should hold the key, recording the path from the root. @@ -581,15 +581,16 @@ private CheckedIndexPage ReadMutationPage(int pageNumber, PageType expectedType) return page; } - /// Repoints the index-data block's B-tree root (0x26) after the root grows a level. Walks - /// stats → column descriptors → column names → data blocks to the index's block. + /// Repoints the index-data block's B-tree root (0x26) — after the root grows a level, or when a new + /// table's foreign-key index is given its root. Walks stats → column descriptors → column names → data blocks + /// to the index's block. /// /// A wide table's definition spans continuation pages, and the data blocks sit past the column names — /// well beyond the first page for a 255-column table. The walk therefore runs over the stitched /// definition (the absolute coordinate space the descriptors use), and only the 4 root bytes are written /// back, mapped to whichever page actually holds them. Nothing changes length, so no re-split is needed. /// - private void UpdateIndexRoot(IndexDef index, int newRoot) + internal void UpdateIndexRoot(IndexDef index, int newRoot) { (_, IReadOnlyList continuations, int block) = LocateIndexBlock(index); WriteInt32IntoDefinition(continuations, block + IndexBlockFormat.RootPageOffset, newRoot); @@ -809,7 +810,7 @@ internal void BulkBuild(IndexDef index, List<(byte[] Key, int Pointer, bool Null current.RemoveAt(current.Count - 1); keyBytes -= key.Length; int next = AllocateIndexPage(index); - WriteOrThrow(page, Build(PageType.LeafIndexPage, previous, next, tail: 0, level: 0, current)); + WriteOrThrow(page, Build(PageType.LeafIndexPage, previous, next, tail: 0, level: 0, current, Prefix())); separators.Add((WithTrailer(current[^1].Key, current[^1].Trailer), page)); previous = page; @@ -819,7 +820,7 @@ internal void BulkBuild(IndexDef index, List<(byte[] Key, int Pointer, bool Null compressed = 0; } - WriteOrThrow(page, Build(PageType.LeafIndexPage, previous, next: 0, tail: 0, level: 0, current)); + WriteOrThrow(page, Build(PageType.LeafIndexPage, previous, next: 0, tail: 0, level: 0, current, Prefix())); // --- node levels, until one page covers the level -------------------------------------------------- int level = 1; @@ -831,6 +832,11 @@ internal void BulkBuild(IndexDef index, List<(byte[] Key, int Pointer, bool Null UpdateIndexRoot(index, page); index.RootPage = page; } + + // A leaf is written at the prefix its filling reached — compressed only once it had to be, as the + // incremental path writes it — and no further than its entries share (verified: ACE's CREATE INDEX over + // eleven keys sharing ten bytes writes them whole, at prefix 0). + int Prefix() => current.Count <= 1 ? 0 : Math.Min(compressed, CommonPrefixLength(current[0].Key, current[^1].Key)); } /// Whether entries totalling of key data fit diff --git a/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs b/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs index 8c57857b..08bd4c57 100644 --- a/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs @@ -32,8 +32,7 @@ public sealed record LongValueResult(byte[] Descriptor, IReadOnlyList Owned /// /// /// An LVAL page is a data page (type 0x01) whose owner field is the ASCII marker "LVAL"; a chunk is -/// stored as its row 0. Access's property loader (and general long-value reads) require this page form — -/// an inline value is not recognised for object properties (LvProp). +/// stored as its row 0. A value up to 64 bytes never comes here: it is inline in its row. /// public sealed class LongValueWriter(PageChannel channel) { @@ -124,7 +123,7 @@ public int WriteNewPage(byte[] row) // exactly as an index entry addresses a data row — so the same 256-slot ceiling applies, and passing it // would alias one value's descriptor onto another's row with no error. Today it is unreachable: a // payload of 64 bytes or less inlines and never arrives here, and a page leaves the free-pages map once - // it has under MinLvalRow bytes left, which caps a page at roughly 108 rows even for the smallest thing + // it has under MinLvalRow bytes left, which caps a page at 104 rows even for the smallest thing // that can reach it (a 33-character memo compressed to 35 bytes — compression is applied AFTER the // inline test, so the floor is lower than the 65-byte inline limit suggests). That margin is emergent, // not stated: it moves if the inline limit or the free-map threshold changes. Refusing the page here diff --git a/src/LibRed/LibRed.Core/Storage/RowInserter.cs b/src/LibRed/LibRed.Core/Storage/RowInserter.cs index 9b794a63..7ab5ff37 100644 --- a/src/LibRed/LibRed.Core/Storage/RowInserter.cs +++ b/src/LibRed/LibRed.Core/Storage/RowInserter.cs @@ -389,6 +389,28 @@ public void Delete(RowId id) finally { ArrayPool.Shared.Return(tdef); } } + /// Counts an UPDATE's key change in 's statistics as ACE does (verified): the + /// total drops by one, as on a delete, and the unique count is then held to no more than the total. Whether the + /// old key had another holder does not matter, and the new key advances nothing. A total already reading 0 is + /// left alone, as a delete leaves it. + public void CountKeyMoved(IndexDef index) + { + JetFormatBase format = _channel.Format; + byte[] tdef = ArrayPool.Shared.Rent(format.PageSize); + try + { + _channel.ReadPage(_table.DefinitionPage, tdef); + int at = format.TdefRealIndexBlockOffset + index.RealIndexOrdinal * format.RealIndexEntrySize; + int total = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(at, 4)); + if (total == 0) return; + BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(at, 4), total - 1); + int unique = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(at + 4, 4)); + BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(at + 4, 4), Math.Min(unique, total - 1)); + _channel.WritePage(_table.DefinitionPage, tdef.AsSpan(0, format.PageSize)); + } + finally { ArrayPool.Shared.Return(tdef); } + } + /// /// The real-index ordinals for which the row at holds the only copy of its key, so /// removing the row also removes a distinct key. An index the row is excluded from (IgnoreNulls with a @@ -1064,10 +1086,11 @@ private void MaterializeLongValues(object?[] values) } } - // A page is dropped from the free-pages map once it cannot hold the smallest long value (a 65-byte - // payload — anything up to 64 inlines — plus its 2-byte row-directory entry). The largest such row is - // 4076 bytes on a Jet 4 page (Jackcess MAX_LONG_VALUE_ROW_SIZE), which nothing here needs to name. - private const int MinLvalRow = 65 + 2; + // A page is dropped from the free-pages map once it cannot hold a 256-byte value and its 2-byte row-directory + // entry (verified vs ACE, memo and OLE alike: a page left with 257 bytes free leaves the map, one with 258 + // stays). Not the smallest long value a page could still take — anything over 64 bytes is one — but ACE's + // own cut-off, below which it stops offering the page. + private const int MinLvalRow = 256 + 2; /// Rejects a caller-supplied value for a calculated column, as ACE does — it refuses both an /// INSERT naming one and an UPDATE setting one, with "Cannot update 'x'; field not updateable." @@ -1097,13 +1120,15 @@ private void RejectExplicitCalculatedValues(object?[] values, IReadOnlySet? private byte[] SpillCalculated(ColumnDef column, byte[] envelope) => StorePackedLongValue(column.ColumnId, envelope); - /// Stores on an LVAL page for long-value column - /// — packing onto a free page as usual — and returns the in-row descriptor. - /// For a caller that must use a page regardless of size (the MSysObjects LvProp property blob, - /// which Access reads only from a page, never inline). Call before - /// so the row carries the returned descriptor as a . + /// Stores for long-value column and returns + /// the in-row descriptor: inline up to 64 bytes, as any long value is, else on an LVAL page — packing onto a + /// free page as usual. The MSysObjects LvProp property blob takes the same rule (verified: ACE inlines + /// a 63-byte blob and puts a 65-byte one on a page). Call before so the + /// row carries the returned descriptor as a . public byte[] StorePackedLongValue(int columnId, byte[] payload) { + if (payload.Length <= LongValueFormat.MaxInlineValue) return Types.JetTypeCodec.EncodeInlineLongValue(payload); + TableDefinitionPage definition = ReadDefinition(); definition.LongValueOwnedMaps.TryGetValue(columnId, out (int Row, int Page) owned); definition.LongValueFreeMaps.TryGetValue(columnId, out (int Row, int Page) free); diff --git a/src/LibRed/LibRed.Core/Storage/Table.cs b/src/LibRed/LibRed.Core/Storage/Table.cs index 307e3807..4158b047 100644 --- a/src/LibRed/LibRed.Core/Storage/Table.cs +++ b/src/LibRed/LibRed.Core/Storage/Table.cs @@ -102,9 +102,15 @@ public void Update(RowId id, object?[] values) => Update(id, values, new HashSet(System.Linq.Enumerable.Range(0, values.Length))); /// Moves a row's entry in one index when its key changes (remove old key, add new; row id - /// unchanged). Used by UPDATE of an indexed column. - public void MoveIndexEntry(IndexDef index, object?[] oldValues, object?[] newValues, RowId id) => + /// unchanged), and counts the move in the index's statistics as ACE does. Used by UPDATE of an indexed + /// column. + public void MoveIndexEntry(IndexDef index, object?[] oldValues, object?[] newValues, RowId id) + { new IndexWriter(Channel, Definition).MoveEntry(index, oldValues, newValues, id); + // An IGNORE NULL index the row was absent from never counted it, so has nothing to count out. + if (!(index.IgnoreNulls && IndexWriter.HasNullKey(index, oldValues))) + new RowInserter(Channel, Definition).CountKeyMoved(index); + } /// Whether ' key already exists in for a row /// other than — used to enforce a UNIQUE/PRIMARY index on UPDATE. diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 27ab2805..eaa0b4e8 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -230,17 +230,13 @@ public void Create( } // Each index is an empty leaf root, populated as rows are inserted. Its usage map is on the primary - // page, at the row the declaration order above gave it. + // page, at the row the declaration order above gave it. A foreign key's root is allocated later, after + // its relationship's rows (below); until then the definition names page 0. var indexes = new List(indexPlans.Count); for (int i = 0; i < indexPlans.Count; i++) { var plan = indexPlans[i]; - int rootPage = _allocator.Allocate(); - WriteEmptyLeafIndexPage(format, rootPage, owner: tdefPage); - // Record the root in the index's own pages usage map — Access does this at CREATE, before any - // row exists (verified: a freshly created empty index has exactly its root bit set). As the tree - // grows, IndexWriter adds each page it allocates, so the map covers the whole B-tree. - new UsageMapWriter(_channel).SetBit(indexRows[i], usageMapPage, rootPage, set: true); + int rootPage = plan.Fk is null ? AllocateIndexRoot(format, tdefPage, indexRows[i], usageMapPage) : 0; indexes.Add(new IndexSpec(plan.Name, plan.Columns, plan.IsPk, plan.IsUnique, rootPage, UsageMapRow: indexRows[i], UsageMapPage: usageMapPage)); } @@ -339,12 +335,33 @@ public void Create( AddCatalogRow(name, tdefPage, columnProps, checkConstraints); AddPermissionRows(tdefPage); - foreach (RelationshipSpec fk in relationships) + + // Each foreign key's index is built after its relationship's rows, as ACE builds it (verified: a database's + // first relationship takes MSysRelationships' first data page, and the key's index root the page after). + for (int i = 0; i < indexPlans.Count; i++) + { + if (indexPlans[i].Fk is not { } fk) continue; AddRelationshipRows(name, fk); + int rootPage = AllocateIndexRoot(format, tdefPage, indexRows[i], usageMapPage); + _catalog.Invalidate(); + TableDef created = _catalog.RequireTable(name); + new IndexWriter(_channel, created).UpdateIndexRoot(created.Indexes.First(ix => ix.RealIndexOrdinal == i), rootPage); + } foreach (IncomingRelationship inc in incoming) AddIncomingRelationshipBlock(inc); } + /// Allocates an index's root — an empty leaf — and records it in the index's own pages usage map, as + /// Access does at CREATE, before any row exists (verified: a freshly created empty index has exactly its root + /// bit set). As the tree grows, IndexWriter adds each page it allocates, so the map covers the whole B-tree. + private int AllocateIndexRoot(JetFormatBase format, int tdefPage, int mapRow, int mapPage) + { + int rootPage = _allocator.Allocate(); + WriteEmptyLeafIndexPage(format, rootPage, owner: tdefPage); + new UsageMapWriter(_channel).SetBit(mapRow, mapPage, rootPage, set: true); + return rootPage; + } + /// The property-blob entries that make a column calculated (§3.4a), or nothing for an ordinary /// one. All three pieces have to agree or Access reads the payload wrongly: Expression is the text /// the engine evaluates, ResultType is the authority on the payload's encoding, and the three @@ -1448,8 +1465,8 @@ private void RenameColumnProperties(int tdefPage, string oldName, string newName } /// Replaces an object's extended-property blob with . Not an - /// call, because LvProp is a Memo: the blob has to be stored on its - /// own page first and the row given the descriptor that names it. + /// call, because LvProp is a long value: the blob has to be stored first + /// — inline or on a page — and the row given the descriptor for it. private void WriteObjectProperties(int objectId, byte[] properties) { (TableDef msys, Table table, int idIdx, ColumnDef lvProp) = ObjectProperties(); @@ -3647,14 +3664,9 @@ private void AddCatalogRow(string name, int tdefPage, name, tdefPage, CatalogFormat.ObjectTypeTable, CatalogFormat.ObjectContainerParentId, flags: 0, props); } - // A new user table's two permission masks. Both rows get the same full access — measured against ACE's own - // CREATE TABLE, row for row, and matching every table in Northwind (system-catalog §11). The 0x0F00FE the - // Users row used to take is the QUERY owner's mask, which is a different object class. - private const int UserTableUsersMask = 0x0FFEFF; - private const int UserTableAdminMask = 0x0FFEFF; - + // A new table's permission rows: what the Tables container grants what it creates (system-catalog §11). private void AddPermissionRows(int objectId) => - new CatalogWriter(_channel, _catalog).AddPermissionRows(objectId, UserTableUsersMask, UserTableAdminMask); + new CatalogWriter(_channel, _catalog).AddPermissionRows(objectId, CatalogFormat.ObjectContainerParentId); /// An object's MSysACEs rows, whole. A table need not carry only the pair /// writes: a workgroup-secured database grants to as many accounts as it diff --git a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs index 996e8f8b..a4f861c4 100644 --- a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs @@ -23,20 +23,6 @@ public sealed class ViewCreator(PageChannel channel, JetCatalog catalog) private const int AppendFlags = 0x10000040; // an INSERT (append) query private const int MakeTableFlags = 0x10000050; private const int DataDefinitionFlags = 0x10000060; // a CREATE/DROP TABLE (data-definition) query - // The owner and the administrator grantee, read from the file being written: an on-disk SID is masked per - // file (page-00 §2.3), so a pair baked in here would name no account in any other file. - - // MSysACEs permission rows a QUERY/VIEW object gets. ACE's own CREATE VIEW writes full access in both rows, - // as its CREATE TABLE does — measured row for row against it. (Northwind's views, made in the Access UI, - // carry 0xF00FE in the owner row instead: the UI grants a narrower set than the SQL path. Access reads - // either.) Without these rows at all, Access opens the file but warns about permissions on the query. - private const int QueryOwnerAcm = 0xFFEFF; // 1048319 - private const int QueryAdminAcm = 0xFFEFF; // 1048319 - - // A relationship object's MSysACEs rows (verified vs ACE): owner 0xF00FE as a query's, admin 0xFFFFF. - private const int RelationshipOwnerAcm = 0xF00FE; // 983294 - private const int RelationshipAdminAcm = 0xFFFFF; // 1048575 - private readonly PageChannel _channel = channel; private readonly JetCatalog _catalog = catalog; @@ -70,20 +56,18 @@ public void CreateAction(string name, ActionQuerySpec spec) /// another relationship has, as ACE does; a table or query may share it. /// public void CreateRelationshipObject(string name) => - AllocateObject(name, CatalogFormat.ObjectTypeRelationship, CatalogFormat.RelationshipContainerParentId, flags: 0, - RelationshipOwnerAcm, RelationshipAdminAcm); + AllocateObject(name, CatalogFormat.ObjectTypeRelationship, CatalogFormat.RelationshipContainerParentId, flags: 0); private int AllocateQueryObject(string name, int flags) { JetName.Validate(name, "query name"); - return AllocateObject(name, StoredQueryFormat.ObjectTypeQuery, CatalogFormat.ObjectContainerParentId, flags, - QueryOwnerAcm, QueryAdminAcm); + return AllocateObject(name, StoredQueryFormat.ObjectTypeQuery, CatalogFormat.ObjectContainerParentId, flags); } /// Reserves the next free high-bit object id, checks the name is free, and writes - /// the MSysObjects row and its two MSysACEs rows. For a query the distinguish view / - /// append / data-definition. - private int AllocateObject(string name, short type, int parentId, int flags, int ownerAcm, int adminAcm) + /// the MSysObjects row and the MSysACEs rows its container grants it. For a query the + /// distinguish view / append / data-definition. + private int AllocateObject(string name, short type, int parentId, int flags) { TableDef msysObjects = _catalog.RequireTable("MSysObjects"); int idIndex = msysObjects.RequireColumn("Id").Index; @@ -107,7 +91,7 @@ private int AllocateObject(string name, short type, int parentId, int flags, int var writer = new CatalogWriter(_channel, _catalog); writer.AddObjectRow(name, nextId, type, parentId, flags); - writer.AddPermissionRows(nextId, ownerAcm, adminAcm); + writer.AddPermissionRows(nextId, parentId); return nextId; } diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index acdc1af6..8cd9067f 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -268,7 +268,10 @@ private void CascadeDelete(Table rootTable, IEnumerable<(RowId Id, object?[] Val var order = new List<(Table Table, RowId Id, object?[] Values)>(); var stack = new Stack<(Table Table, RowId Id, object?[] Values, bool ChildrenExpanded)>(); - foreach (var (id, values) in roots) + // Pushed last first, so the roots come off the stack — and are deleted — in the order they were found, as + // ACE deletes them. The order shows in the file: a delete slides the rows below it up and leaves the old + // bytes in the space it frees, so deleting the same rows the other way round leaves different bytes. + foreach (var (id, values) in roots.Reverse()) if (scheduled.Add((rootTable.Name, id))) stack.Push((rootTable, id, values, false)); diff --git a/src/LibRed/README.md b/src/LibRed/README.md index a6303a1a..6ea6fe9b 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -265,7 +265,6 @@ Format-level detail on each on-disk gap lives in `docs/format/`. rebuilt. The gap is the write-back, not the grammar: all of these parse and execute as plain statements. - **Function surface** — the evaluator's whitelist isn't proven identical to ACE's JES, and `Format`'s named date/currency formats are locale-dependent by design (not byte-identical cross-locale). -- **Non-unique index statistics** — only unique indexes advance the live unique-entry count (`+4`) today. - **Single-writer concurrency** — LibRed is a **single-writer engine that merely tolerates extra open handles**, not a concurrent multi-user one. `PageChannel.Open` opens the file `FileShare.ReadWrite` (a Jet/ACE file is a shared-file database — Access/ODBC/OLE DB all open it with multiple handles, and diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index c2db89a9..d451276c 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -152,8 +152,8 @@ descriptor. | Offset | Size | Meaning | | --- | --- | --- | -| `0x00` | 4 | Total entry count — compact-time only (`0` in a live-edited file) | -| `0x04` | 4 | Unique entry count — maintained live, cumulative, never decremented | +| `0x00` | 4 | Total entry count — set when the index is built or the file compacted; lowered by a delete and by an update that moves the entry, never raised by an insert | +| `0x04` | 4 | Unique entry count — raised by an insert bringing a new key; lowered by a delete of a key's last holder, and held to the total by an update | | `0x08` | 4 | Reserved (zero) | ## Index-data block — 52 bytes, one per real index → [page-02d](page-02d-constraints.md) @@ -296,7 +296,7 @@ table records how each ceiling is actually held, not merely that it exists. | Narrow field | Ceiling it imposes | How it is held | | --- | --- | --- | | Index leaf entry addresses a row as `page << 8 \| row` — 1 byte of slot | **255 rows per data page**: the pointer allows 256 but **ACE writes at most 255**, not for space (a filled page keeps ~2,297 of 4,096 bytes free) and it drops the page from the free-pages map on reaching it | **Enforced at ACE's 255.** `FindPageWithRoom` refuses a page at `RowPointer.MaxRowsPerPage`, covering both the insert path and `WriteHiddenRow` (a relocation target is named by the same pointer). Overfilling costs more than indexed reads — ACE parses the full 16-bit count but caps at 256 slots, so it silently cannot see the rest of the page's rows | -| Long-value descriptor names its row in 1 byte (`d[4]`) | **256 rows per LVAL page** | **Enforced** in `TryAppend`. Unreachable in practice — a payload ≤ 64 bytes inlines, and the free-map drop at `MinLvalRow` caps a page near 108 rows even for the smallest thing that can arrive (a 33-character memo compressed to 35 bytes; compression is applied *after* the inline test, so the floor is below the 65 bytes the inline limit suggests) | +| Long-value descriptor names its row in 1 byte (`d[4]`) | **256 rows per LVAL page** | **Enforced** in `TryAppend`. Unreachable in practice — a payload ≤ 64 bytes inlines, and the free-map drop at `MinLvalRow` (258 bytes free) caps a page at 104 rows even for the smallest thing that can arrive (a 33-character memo compressed to 35 bytes; compression is applied *after* the inline test, so the floor is below the 65 bytes the inline limit suggests) | | Page numbers are 3 bytes in the TDEF usage-map pointer, the long-value descriptor (`d[5..7]`) and an LVAL chunk's next-pointer | **page < 2²⁴** (16,777,216) | **Safe with 32× headroom**, because `PageChannel.WritePage` enforces the 2 GiB file limit at 524,288 pages. The 24-bit fields are never the binding constraint | | Usage-map pointer names its record row in 1 byte | **256 records per usage-map page** | **Safe by a louder guard.** A record is 69 bytes, so `AppendEmptyUsageMapRow`'s space check admits 57 and refuses the 58th — 4.5× tighter than the byte — and it throws rather than truncating | | Reference usage map holds 17 bitmap-page slots | **~2.28 GB of page coverage** | **Enforced** — `NotSupportedException` on both the set-bit and inline→reference conversion paths. Just past the 2 GiB file limit, by design | diff --git a/src/LibRed/docs/format/long-values.md b/src/LibRed/docs/format/long-values.md index 96b17824..8a6a37e7 100644 --- a/src/LibRed/docs/format/long-values.md +++ b/src/LibRed/docs/format/long-values.md @@ -222,12 +222,16 @@ the long values: > by column id) and the inline bitmap bit is set. **Pages are packed like Access:** a value up to one row > is appended to the first **free-map** page with room (many small values share a page as separate rows); > only when none has room is a fresh page allocated (owned + free). A page is dropped from the free map -> once it can't hold the smallest long value (65-byte payload + its 2-byte slot). This reproduces Access's +> once it can't hold a **256-byte** value and its 2-byte slot — **257 bytes free or fewer** (verified vs ACE, +> memo and OLE alike: 257 free leaves the map, 258 stays) — not once it can't hold the smallest long value; a +> page is also kept when a value too large for it goes elsewhere. This reproduces Access's > layout — a column **owns** every page it has filled but **frees** only the current append target, so -> medium memos share a few pages (full ones owned-only, the current one owned+free), not one each. The same -> packing is used for the MSysObjects **LvProp** property blob (via `RowInserter.StorePackedLongValue`) — -> but always to a page, never inline (Access reads object properties only from a page), so two tables' -> DEFAULT/CHECK blobs share one LvProp page. A chained value uses dedicated pages. A page outside the inline +> medium memos share a few pages (full ones owned-only, the current one owned+free), not one each. The +> MSysObjects **LvProp** property blob follows the same rules as any long value (via +> `RowInserter.StorePackedLongValue`) — **inline up to 64 bytes**, packed onto a page above that, so two +> tables' larger DEFAULT/CHECK blobs share one LvProp page. Verified against ACE: a table's blob of 51–63 +> bytes (a single `Required` or `DefaultValue`) is inline in its MSysObjects row, and one of 65 bytes or more +> is on a page. A chained value uses dedicated pages. A page outside the inline > map's window is handled by the shared `UsageMapWriter.SetBit`, which grows the inline record in place and converts it to a reference map when it no longer fits — a long-value column's maps are not a special case, and `MapPages` reads either form back. diff --git a/src/LibRed/docs/format/page-00-database.md b/src/LibRed/docs/format/page-00-database.md index 3b592916..2fca9d3f 100644 --- a/src/LibRed/docs/format/page-00-database.md +++ b/src/LibRed/docs/format/page-00-database.md @@ -132,7 +132,9 @@ TDEF page). LibRed reads `0x20` into `DatabaseDefinitionPage.CatalogRootPage` an > **How the reference engine lays out a new file** (DAO-created ACE 12, 42 pages). > Per table the allocation order is **TDEF → usage-map page → one page per index root**, in table-creation > order; both usage maps share one page (owned = row 0, free = row 1, inline), which is what every TDEF's -> `0x37`/`0x3B` pointers show. **Data pages are allocated lazily on first insert**, so they appear out of +> `0x37`/`0x3B` pointers show. A **foreign key's** index root comes after its relationship's +> `MSysRelationships` rows (verified: a database's first relationship takes that table's first data page, and +> the key's index root the page after). **Data pages are allocated lazily on first insert**, so they appear out of > sequence and an empty table has none at all. > > | pages | contents | diff --git a/src/LibRed/docs/format/page-01-data-and-rows.md b/src/LibRed/docs/format/page-01-data-and-rows.md index 50f65e21..f87fa07a 100644 --- a/src/LibRed/docs/format/page-01-data-and-rows.md +++ b/src/LibRed/docs/format/page-01-data-and-rows.md @@ -76,6 +76,11 @@ bytes for good. The **slot directory** is not reclaimed by either engine: a tombstoned slot is never reused, so a page that has seen thirteen rows carries thirteen slots whatever is live. Only the row bytes come back. +**The freed bytes are not cleared, so the delete order shows (verified).** The space a delete frees keeps +whatever the slide left in it — the old bytes of the lowest row that moved. A statement deleting several rows +of a page deletes them **in the order it finds them**, lowest slot first; deleting the same rows the other way +round leaves the same live rows and directory but different bytes in the free space. + **A relocated row is reclaimed on both pages.** When a slot carries the overflow flag it holds a 4-byte forward pointer rather than the row, and the row itself sits on another page flagged deleted (§ relocation below). Deleting it reclaims the target first and then the pointer: ACE brings the target's page back to a diff --git a/src/LibRed/docs/format/page-02d-constraints.md b/src/LibRed/docs/format/page-02d-constraints.md index 4e2e28b7..b16c4e6d 100644 --- a/src/LibRed/docs/format/page-02d-constraints.md +++ b/src/LibRed/docs/format/page-02d-constraints.md @@ -16,7 +16,8 @@ statistics: **These two fields are maintained very differently (verified vs ACE):** - **Total entry count (`+0`) is *not* maintained on insert.** Access leaves it unchanged through live - inserts and updates — but **a delete decrements it** (see "On delete" below). It is written when the index + inserts — but **a delete decrements it** (see "On delete" below), and so does an **update that changes the + row's key** in that index (see "On update"). It is written when the index is **built over the rows present** — `CREATE INDEX` (unique or not), a foreign key's backing index, and the rebuild of an index whose column an `ALTER COLUMN` changes — to the **number of entries the index then holds** (the rows, less those an @@ -32,7 +33,8 @@ statistics: collation-equal text (`'a'`, `'A'`) is one key; a **Null is a key** like any other (a second Null adds nothing), except in an **IGNORE NULL** index, which does not hold it and so never counts it. A key whose last row was deleted **counts again** when it returns. A multi-column index compares the whole tuple. - - An **UPDATE never advances it**, even one that gives a row a key no other row has. + - An **UPDATE never advances it**, even one that gives a row a key no other row has (it can lower it — see + "On update"). - It is **one count per real index**: a relationship's logical index sharing a real index (a parent's primary key) does not advance it a second time. - **Building the index** — the same builds as `+0` above — sets it to the index's **distinct keys among @@ -59,7 +61,22 @@ count, and it is gated on the total: > `3` after it: its total is `0`, so the delete skips it. The rule is visible only on an index that *was* > built over rows, where both fields move together. - LibRed maintains both this way — inserts and deletes in `RowInserter`, builds in `TableCreator`'s index +**On update (verified).** An UPDATE that changes a row's key in an index — its entry moves — counts there +differently from a delete: + +- **A block whose total reads `0` is left alone**, as on a delete. +- **Otherwise the total drops by one** for each row whose entry moves, and **the unique count is then held to + no more than the total** — `unique = min(unique, total)`. Whether the old key had another holder does not + matter, and the new key advances nothing. So an index at 6/4 reads 5/4 after one row's key changes (to a key + it already held or to one it did not), one at 11/11 reads 6/6 after five rows' keys change, and changing + every key of a 6/4 index takes it to 0/0. +- An index the row is absent from before the update — a Null key in an IGNORE NULL index — is not touched, + even when the update brings the row in. + +> The update rule could not be seen from an index built empty: its zero total leaves the pair alone whatever the +> update does, which is how "an UPDATE changes neither count" looked right. + + LibRed maintains both this way — inserts, deletes and updates in `RowInserter`, builds in `TableCreator`'s index back-fill, and the Memo/OLE retype (which LibRed does by rebuilding the whole table) restores every other index's counts afterwards — and exposes `+4` as `IndexDef.UniqueEntryCount`. diff --git a/src/LibRed/docs/format/page-03-04-index-btree.md b/src/LibRed/docs/format/page-03-04-index-btree.md index 49fd31d8..56654714 100644 --- a/src/LibRed/docs/format/page-03-04-index-btree.md +++ b/src/LibRed/docs/format/page-03-04-index-btree.md @@ -132,6 +132,10 @@ omits. Reconstruct: `fullEntry = prefix ++ stored`. > End state alone misleads: a leaf that has never filled is uncompressed under ACE too. And recomputing the > prefix on split while keeping it on append is wrong — pages then fill uncompressed and split without ever > being compressed, giving 4 leaves and 11,820 bytes against ACE's 3 and 11,334. +> +> **Building an index follows the same rule** (verified): `CREATE INDEX` over a table's rows writes each leaf +> at the prefix its filling reached, so an index whose entries all fit one page uncompressed is written +> uncompressed — eleven text keys sharing ten bytes land at prefix `0` — however much they share. ### 10.4 Key encoding (order-preserving) diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index 9f9a3129..a952b7ec 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -41,14 +41,21 @@ > `ObjectId` (Int32, the object's id), `SID` (Binary, a security id), `ACM` (Int32, an access mask), and > `FInheritable` (Boolean). Each row sets `ObjectId` = the object id, `SID` = a 2-byte binary security id, > `ACM` = an access mask, `FInheritable` = false, and the object's `ObjectId` index must be maintained so - > Access's security check finds them. Access writes **two** rows per object — one for the **Users** account - > (which also owns the object) and one for **admin** — and the masks depend on the object and on the route - > that created it (verified): - > - **Table** (`CREATE TABLE`): both rows `ACM = 0xFFEFF` (1048319), full access. - > - **Query/view** (`CREATE VIEW`): both rows `0xFFEFF` as well. - > - **Relationship** (`ADD CONSTRAINT … FOREIGN KEY`): Users `0xF00FE` (983294), admin `0xFFFFF`. - > - A view created in the **Access UI** instead carries `0xF00FE` in its Users row. The narrower query mask - > is the UI's, not the SQL path's, and Access reads either. + > Access's security check finds them. + > + > **A new object's rows are its container's inheritable grants (verified).** Each container — + > `Tables` (`0x0F000001`, which holds queries too) and `Relationships` (`0x0F000003`) — carries `MSysACEs` + > rows of its own, some marked `FInheritable`. A new object gets one row per account those inheritable rows + > name: the **Creator** account's grant becomes the grant to the object's **owner**, the Users account, and + > every other inheritable grant is copied for its own account — OR'd into the owner's row when it names + > Users too. The owner's row comes first. So the masks are the database's, not the object class's: + > - A fresh DAO/ACE database's `Tables` container grants Creator `0xF00FE` and admin `0xFFEFF` (its Users + > row, `0x60001`, is not inheritable), so a new table, view or query gets Users `0xF00FE`, admin `0xFFEFF`. + > - Northwind's `Tables` container also grants Users `0xFFEFF`, inheritable, so there both rows are `0xFFEFF`. + > - The `Relationships` container grants Creator `0xF00FE` and admin `0xFFFFF` in both, so a relationship + > gets Users `0xF00FE`, admin `0xFFFFF`. + > + > The route does not matter: ACE's SQL DDL and the Access UI give the same rows in the same database. > > (System-table `MSysACEs` rows in an existing file carry restricted masks like `0x60000`/`0x14` and a long > per-database owner SID; those are the pre-existing catalog's, not what a writer emits for a new object.) @@ -273,8 +280,8 @@ - **`Id`** is the next negative synthetic id: one past the highest in the file, from the sequence queries draw on, so relationships and queries interleave (`0x8000002C` relationship, `0x8000002D` view, `0x8000002E` relationship), and a dropped relationship's id is taken by the next object. - - **Two `MSysACEs` rows**: the Users SID with ACM `0xF00FE`, and admin's with `0xFFFFF` — the one object - class whose two masks differ from a table's (verified against ACE's `ADD CONSTRAINT`). + - **`MSysACEs` rows** from the `Relationships` container's inheritable grants (§11): the Users SID with ACM + `0xF00FE` and admin's with `0xFFFFF` in the databases examined (verified against ACE's `ADD CONSTRAINT`). - **Dropping it** — `DROP CONSTRAINT`, or `DROP TABLE` of the referencing table — removes the object and its two `MSysACEs` rows. - **Its name** must differ from every other relationship's (*"There is already a relationship named '…' in @@ -534,8 +541,8 @@ cascading both update and delete stores `grbit = 0x1100`. > **Writing a relationship.** An **enforced** relationship is `MSysRelationships` rows, an `MSysObjects` - > row of its own (`Type` = 8, under the relationship container, with the `MSysACEs` pair `0xF00FE` / - > `0xFFFFF` of §11) **and** a non-unique index on the + > row of its own (`Type` = 8, under the relationship container, with the `MSysACEs` rows that container + > grants, §11) **and** a non-unique index on the > child table's FK column(s) — enforcement requires the child FK to be indexed and the parent key to be > uniquely indexed (the parent PK). LibRed writes all of it, including the byte-faithful relationship > logical-index linkage in *both* tables' TDEFs (§3.6: outgoing block on the child, incoming block on the diff --git a/test/LibRed.Engine.AccessTests/IndexUniqueEntryCountAccessTests.cs b/test/LibRed.Engine.AccessTests/IndexUniqueEntryCountAccessTests.cs index 66b5fca5..c5279ee7 100644 --- a/test/LibRed.Engine.AccessTests/IndexUniqueEntryCountAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/IndexUniqueEntryCountAccessTests.cs @@ -142,6 +142,50 @@ public void An_index_built_over_existing_rows_is_counted_from_them(string sql) } } + // An UPDATE that changes a row's key drops the index's total by one and then holds the unique count to no more + // than the total — whether the old key had another holder does not matter, and the new key advances nothing. + // Visible only on an index built over rows: one built empty has a zero total, which is left alone. + [Theory] + [InlineData("UPDATE S SET A = 9 WHERE Id = 6")] // the last row holding A = 8 + [InlineData("UPDATE S SET A = 9 WHERE Id = 3")] // A = 6 is held by row 4 too + [InlineData("UPDATE S SET T = 'D' WHERE Id = 6")] // a collation-equal key: 'd' and 'D' are one + [InlineData("UPDATE S SET N = NULL WHERE Id = 3")] // leaves the IGNORE NULL index + [InlineData("UPDATE S SET N = 5 WHERE Id = 6")] // enters it from Null + [InlineData("UPDATE S SET A = A + 10")] + public void An_update_counts_the_old_key_out_as_a_delete_does(string sql) + { + string northwind = Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"); + string start = TemporaryDatabase.CopyPath(northwind, "uniqcount-upd-start-"); + string ace = "", libred = ""; + try + { + foreach (string statement in (string[]) + [ + "CREATE TABLE S (Id LONG CONSTRAINT pkS PRIMARY KEY, A LONG, T TEXT(10), N LONG)", + "INSERT INTO S VALUES (1, 5, 'a', NULL)", "INSERT INTO S VALUES (2, 5, 'A', NULL)", + "INSERT INTO S VALUES (3, 6, 'b', 1)", "INSERT INTO S VALUES (4, 6, 'b', 1)", + "INSERT INTO S VALUES (5, 7, 'c', 2)", "INSERT INTO S VALUES (6, 8, 'd', NULL)", + "CREATE INDEX ixA ON S (A)", "CREATE INDEX ixT ON S (T)", + "CREATE INDEX ixNi ON S (N) WITH IGNORE NULL", "CREATE INDEX ixAN ON S (A, N)", + ]) + Ace(start, statement); + + ace = TemporaryDatabase.CopyPath(start, "uniqcount-upd-ace-"); + libred = TemporaryDatabase.CopyPath(start, "uniqcount-upd-lib-"); + Ace(ace, sql); + LibRed(libred, sql); + string before = Statistics(start), aceStats = Statistics(ace), libredStats = Statistics(libred); + output.WriteLine($"{sql}\n before {before}\n ACE {aceStats}\n LibRed {libredStats}"); + Assert.Equal(aceStats, libredStats); + } + finally + { + TemporaryDatabase.Delete(start); + if (ace.Length > 0) TemporaryDatabase.Delete(ace); + if (libred.Length > 0) TemporaryDatabase.Delete(libred); + } + } + // A retype to or from Memo/OLE, which LibRed does by rebuilding the whole table, leaves the counts as ACE's ALTER // does: only an index over the changed column is rebuilt (a primary key included), every other index here keeps // its cumulative counts, and so does the foreign-key index of a table referencing this one. diff --git a/test/LibRed.Engine.AccessTests/LongValueFreeMapAccessTests.cs b/test/LibRed.Engine.AccessTests/LongValueFreeMapAccessTests.cs new file mode 100644 index 00000000..89eaa3e7 --- /dev/null +++ b/test/LibRed.Engine.AccessTests/LongValueFreeMapAccessTests.cs @@ -0,0 +1,71 @@ +using System.Data.OleDb; +using LibRed.Pages; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// A long-value page leaves its column's free-pages map once it cannot hold a 256-byte value and its slot — 257 +/// bytes free or fewer — the same through ACE and LibRed (docs/format/long-values.md). Two values fill the page to +/// just either side of the line: a 2000-byte one, then one that leaves remaining bytes. +/// +[Collection(AceCollection.Name)] +public class LongValueFreeMapAccessTests(ITestOutputHelper output) : TempDatabaseTest +{ + [Theory] + [InlineData(256)] + [InlineData(257)] + [InlineData(258)] + [InlineData(259)] + public void A_page_leaves_the_free_map_where_ace_takes_it_out(int remaining) + { + string[] statements = + [ + "CREATE TABLE L (Id LONG, M LONGBINARY)", + $"INSERT INTO L VALUES (1, 0x{new string('A', 4000)})", + $"INSERT INTO L VALUES (2, 0x{new string('B', 2 * (2078 - remaining))})", + ]; + string northwind = Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"); + string ace = TemporaryDatabase.CopyPath(northwind, "lvfree-ace-"), libred = TemporaryDatabase.CopyPath(northwind, "lvfree-lib-"); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(ace)) + foreach (string sql in statements) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = sql; + command.ExecuteNonQuery(); + } + using (var db = JetDatabase.Open(libred, readOnly: false)) + { + var engine = new QueryEngine(db); + foreach (string sql in statements) engine.ExecuteNonQuery(sql); + } + + string expected = Maps(ace), actual = Maps(libred); + output.WriteLine($"ACE {expected}, LibRed {actual}"); + Assert.Equal(expected, actual); + } + finally + { + TemporaryDatabase.Delete(ace); + TemporaryDatabase.Delete(libred); + } + } + + /// The column's long-value page, its free space, and whether its free-pages map still names it. + private static string Maps(string path) + { + using var db = JetDatabase.Open(path, readOnly: true); + Table table = db.OpenTable("L"); + var tdef = new TableDefinitionPage(); + tdef.Read(table.Channel.ReadPage(table.Definition.DefinitionPage), table.Channel.Format); + int id = table.Definition.RequireColumn("M").ColumnId; + var maps = new UsageMap(table.Channel, table.Definition); + int page = maps.PagesInMap(tdef.LongValueOwnedMaps[id].Row, tdef.LongValueOwnedMaps[id].Page).Single(); + bool free = maps.PagesInMap(tdef.LongValueFreeMaps[id].Row, tdef.LongValueFreeMaps[id].Page).Contains(page); + int space = BitConverter.ToUInt16(table.Channel.ReadPage(page).Span[2..4]); + return $"{space} bytes free, {(free ? "in the free map" : "out of it")}"; + } +} diff --git a/test/LibRed.Engine.AccessTests/LvPropStorageAccessTests.cs b/test/LibRed.Engine.AccessTests/LvPropStorageAccessTests.cs new file mode 100644 index 00000000..5b87026e --- /dev/null +++ b/test/LibRed.Engine.AccessTests/LvPropStorageAccessTests.cs @@ -0,0 +1,102 @@ +using System.Data.OleDb; +using LibRed.Catalog; +using LibRed.Pages; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// Where a table's property blob (MSysObjects.LvProp) is stored: inline in the row up to 64 bytes and on an +/// LVAL page above that, the rule every long value follows — through ACE and through LibRed alike. Each property adds +/// its owner's name to the blob, so the length of a NOT NULL column's name walks the blob across the line. +/// +[Collection(AceCollection.Name)] +public class LvPropStorageAccessTests(ITestOutputHelper output) : TempDatabaseTest +{ + [Theory] + [InlineData("CREATE TABLE W (Id LONG, Cxxxxx LONG NOT NULL)")] // 61 bytes + [InlineData("CREATE TABLE W (Id LONG, Cxxxxxx LONG NOT NULL)")] // 63 bytes + [InlineData("CREATE TABLE W (Id LONG, Cxxxxxxx LONG NOT NULL)")] // 65 bytes + [InlineData("CREATE TABLE W (Id LONG, Cxxxxxxxx LONG NOT NULL)")] // 67 bytes + [InlineData("CREATE TABLE W (Id LONG, A LONG DEFAULT 7)")] // 60 bytes + [InlineData("CREATE TABLE W (Id LONG, A LONG DEFAULT 7, B LONG DEFAULT 8)")] // 84 bytes + public void A_property_blob_is_stored_where_ace_stores_it(string sql) + { + string ace = Stored(sql, AceRun), libred = Stored(sql, LibRedRun); + output.WriteLine($"ACE {ace}, LibRed {libred}"); + Assert.Equal(ace, libred); + } + + // What an inline blob is for: ACE reads it. A NOT NULL written inline by LibRed refuses a null in ACE, and a + // DEFAULT written inline fills an omitted column. + private static readonly string Northwind = Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"); + + [Fact] + public void Ace_honours_a_property_blob_libred_wrote_inline() + { + string path = TemporaryDatabase.CopyPath(Northwind, "lvpropinline-"); + try + { + LibRedRun(path, "CREATE TABLE W (Id LONG, A LONG NOT NULL)"); + LibRedRun(path, "CREATE TABLE V (Id LONG, A LONG DEFAULT 7)"); + Assert.StartsWith("inline", Stored(path, "W"), StringComparison.Ordinal); + Assert.StartsWith("inline", Stored(path, "V"), StringComparison.Ordinal); + + using OleDbConnection connection = AceTestDatabase.Open(path); + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = "INSERT INTO W (Id) VALUES (1)"; + Assert.Throws(() => command.ExecuteNonQuery()); + command.CommandText = "INSERT INTO V (Id) VALUES (1)"; + command.ExecuteNonQuery(); + command.CommandText = "SELECT A FROM V"; + Assert.Equal(7, command.ExecuteScalar()); + } + finally { TemporaryDatabase.Delete(path); } + } + + private static string Stored(string sql, Action create) + { + string path = TemporaryDatabase.CopyPath(Northwind, "lvpropstore-"); + try + { + create(path, sql); + return Stored(path, "W"); + } + finally { TemporaryDatabase.Delete(path); } + } + + /// How table 's blob is stored, and its length. + private static string Stored(string path, string table) + { + using var database = JetDatabase.Open(path, readOnly: true); + Table objects = database.OpenTable("MSysObjects"); + TableDef definition = objects.Definition; + int name = definition.RequireColumn("Name").Index, lvProp = definition.RequireColumn("LvProp").Index; + foreach ((RowId id, object?[] values) in objects.Rows().WithIds()) + { + if (values[name] as string != table) continue; + var page = new DataPage(); + page.Read(objects.Channel.ReadPageShared(id.Page), objects.Channel.Format); + byte[] descriptor = RowDecoder.LongValueDescriptors( + definition.Columns, objects.Channel.Format, page.GetRow(id.Row))[lvProp]; + string form = descriptor[3] switch { 0x80 => "inline", 0x40 => "on a page", _ => $"0x{descriptor[3]:X2}" }; + return $"{form}, {((byte[])values[lvProp]!).Length} bytes"; + } + throw new InvalidOperationException($"No MSysObjects row for {table}."); + } + + private static void AceRun(string path, string sql) + { + using OleDbConnection connection = AceTestDatabase.Open(path); + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = sql; + command.ExecuteNonQuery(); + } + + private static void LibRedRun(string path, string sql) + { + using var database = JetDatabase.Open(path, readOnly: false); + new QueryEngine(database).ExecuteNonQuery(sql); + } +} diff --git a/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs b/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs new file mode 100644 index 00000000..fd8b43b2 --- /dev/null +++ b/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs @@ -0,0 +1,93 @@ +using System.Data.OleDb; +using System.Reflection; +using LibRed.Catalog; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// The MSysACEs rows a new object gets are its container's inheritable grants, the Creator's becoming the +/// owner's (system-catalog §11) — so they differ from database to database. Each is checked in two whose Tables +/// containers grant differently: a fresh DAO database, whose owner row comes out 0xF00FE, and Northwind, +/// whose Tables container also grants Users 0xFFEFF. +/// +[Collection(AceCollection.Name)] +public class PermissionRowsAccessTests(ITestOutputHelper output) : TempDatabaseTest +{ + private static readonly string[] Statements = + [ + "CREATE TABLE PermT (Id LONG CONSTRAINT pkPermT PRIMARY KEY)", + "CREATE VIEW PermV AS SELECT Id FROM PermT", + "CREATE TABLE PermC (Id LONG, P LONG, CONSTRAINT fkPerm FOREIGN KEY (P) REFERENCES PermT (Id))", + ]; + + [Theory] + [InlineData("fresh")] + [InlineData("northwind")] + public void A_new_objects_permission_rows_are_the_ones_ace_writes(string database) + { + string origin = database == "fresh" + ? CreateEmptyThroughDao() + : TemporaryDatabase.CopyPath(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "perm-nw-"); + string ace = TemporaryDatabase.CopyPath(origin, "perm-ace-"), libred = TemporaryDatabase.CopyPath(origin, "perm-libred-"); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(ace)) + foreach (string sql in Statements) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = sql; + command.ExecuteNonQuery(); + } + using (var db = JetDatabase.Open(libred, readOnly: false)) + { + var engine = new QueryEngine(db); + foreach (string sql in Statements) engine.ExecuteNonQuery(sql); + } + + string expected = Grants(ace), actual = Grants(libred); + output.WriteLine($"ACE {expected}\nLibRed {actual}"); + Assert.Equal(expected, actual); + } + finally + { + foreach (string path in new[] { origin, ace, libred }) TemporaryDatabase.Delete(path); + } + } + + /// Each new object's MSysACEs rows in the order they are stored: its name, then each row's account, + /// mask and inheritability. + private static string Grants(string path) + { + using var db = JetDatabase.Open(path, readOnly: true); + Table objects = db.OpenTable("MSysObjects"); + int idCol = objects.Definition.RequireColumn("Id").Index, nameCol = objects.Definition.RequireColumn("Name").Index; + var names = objects.Rows() + .Where(r => r[nameCol] is string n && (n.StartsWith("Perm", StringComparison.Ordinal) || n.Contains("fkPerm", StringComparison.Ordinal))) + .ToDictionary(r => (int)r[idCol]!, r => (string)r[nameCol]!); + + Table aces = db.OpenTable("MSysACEs"); + TableDef def = aces.Definition; + int oid = def.RequireColumn("ObjectId").Index, sid = def.RequireColumn("SID").Index, + acm = def.RequireColumn("ACM").Index, inherit = def.RequireColumn("FInheritable").Index; + return string.Join("; ", aces.Rows() + .Where(r => names.ContainsKey((int)r[oid]!)) + .GroupBy(r => names[(int)r[oid]!]) + .OrderBy(g => g.Key, StringComparer.Ordinal) + .Select(g => $"{g.Key}: " + string.Join(", ", g.Select(r => + $"{Convert.ToHexString((byte[])r[sid]!)} 0x{(int)r[acm]!:X6}{((bool)r[inherit]! ? " inheritable" : "")}")))); + } + + private static string CreateEmptyThroughDao() + { + object engine = AceTestDatabase.CreateDaoEngine() ?? throw new InvalidOperationException("DAO is not registered."); + string path = TemporaryDatabase.CreatePath("perm-fresh-"); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + Invoke(Invoke(workspace, "CreateDatabase", path, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!, "Close"); + return path; + } + + private static object? Invoke(object target, string member, params object?[] args) => + target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); +} diff --git a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs new file mode 100644 index 00000000..7463487c --- /dev/null +++ b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs @@ -0,0 +1,364 @@ +using System.Buffers.Binary; +using System.Data.OleDb; +using System.Globalization; +using System.Reflection; +using System.Text; +using LibRed; +using LibRed.Catalog; +using LibRed.Pages; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// A hundred statements run through ACE over OLE DB and through LibRed on copies of one ACE-created empty database, +/// and the files compared byte for byte. It reports how far apart they are rather than asserting they match. +/// +/// +/// Two measurements. The whole run applies every statement in one session per engine and compares the two files at +/// the end — how close LibRed comes over a realistic workload. Step by step applies one statement at a time, both +/// engines starting from the same file — ACE's result of the step before — so each statement is compared on its own +/// and one early divergence does not leave every later page misaligned. +/// Only what differs for reasons other than the format is masked: page 0's user commit-byte table, which +/// counts a lock-file user's writes and LibRed does not keep; the wall-clock DateCreate and DateUpdate of each +/// MSysObjects row; and a data page's write stamp at 0x08. A copy opened and closed through ACE with nothing +/// done to it shows what ACE writes merely for holding a session; a byte that alone changes is counted apart rather +/// than as a difference. Calculated and complex columns are left out — ACE's OLE DB DDL cannot create either. +/// +[Collection(AceCollection.Name)] +public class WholeFileParityProbeTest(ITestOutputHelper output) +{ + private const int PageSize = 4096; + + [Fact] + public void The_whole_run_leaves_libred_and_ace_with_the_same_file() + { + string[] statements = Statements(); + string? origin = CreateEmptyThroughDao(); + Assert.SkipWhen(origin is null, "DAO is not registered in this bitness."); + string ace = TemporaryDatabase.CopyPath(origin, "wholefile-ace-"); + string noise = TemporaryDatabase.CopyPath(origin, "wholefile-noise-"); + string libred = TemporaryDatabase.CopyPath(origin, "wholefile-libred-"); + try + { + string?[] aceErrors = RunThroughAce(ace, statements); + using (AceTestDatabase.Open(noise)) { } + string?[] libredErrors = RunThroughLibRed(libred, statements); + + var report = new StringBuilder(); + report.AppendLine(CultureInfo.InvariantCulture, $"{statements.Length} statements"); + for (int i = 0; i < statements.Length; i++) + if (aceErrors[i] is not null || libredErrors[i] is not null) + report.AppendLine(CultureInfo.InvariantCulture, + $" #{i + 1} {statements[i]}\n ACE: {aceErrors[i] ?? "ok"}\n LibRed: {libredErrors[i] ?? "ok"}"); + report.Append(Compare(origin, ace, noise, libred).Report); + output.WriteLine(report.ToString()); + } + finally + { + foreach (string path in new[] { origin, ace, noise, libred }) TemporaryDatabase.Delete(path); + } + } + + [Fact] + public void Each_statement_leaves_libred_and_ace_with_the_same_file() + { + string[] statements = Statements(); + string? origin = CreateEmptyThroughDao(); + Assert.SkipWhen(origin is null, "DAO is not registered in this bitness."); + string current = origin; + var summary = new StringBuilder(); + var details = new StringBuilder(); + int same = 0; + try + { + for (int i = 0; i < statements.Length; i++) + { + string ace = TemporaryDatabase.CopyPath(current, $"stepfile-ace{i}-"); + string noise = TemporaryDatabase.CopyPath(current, $"stepfile-noise{i}-"); + string libred = TemporaryDatabase.CopyPath(current, $"stepfile-libred{i}-"); + string? aceError = RunThroughAce(ace, [statements[i]])[0]; + using (AceTestDatabase.Open(noise)) { } + string? libredError = RunThroughLibRed(libred, [statements[i]])[0]; + + (string report, int pages) = Compare(current, ace, noise, libred); + string outcome = aceError is not null || libredError is not null + ? $"ACE: {aceError ?? "ok"} | LibRed: {libredError ?? "ok"}" + : pages == 0 ? "identical" : $"{pages} page(s) differ"; + if (outcome == "identical") same++; + summary.AppendLine(CultureInfo.InvariantCulture, $"#{i + 1,-3} {outcome,-22} {Abbreviate(statements[i])}"); + if (pages > 0) + details.AppendLine(CultureInfo.InvariantCulture, $"==== #{i + 1} {statements[i]}").Append(report); + + // The next statement starts from ACE's result, so each is measured on its own. + if (current != origin) TemporaryDatabase.Delete(current); + current = ace; + TemporaryDatabase.Delete(noise); + TemporaryDatabase.Delete(libred); + } + + output.WriteLine($"{same} of {statements.Length} statements left identical files\n{summary}\n{details}"); + } + finally + { + if (current != origin) TemporaryDatabase.Delete(current); + TemporaryDatabase.Delete(origin); + } + } + + /// A hundred statements over six tables: creates with every ordinary column type and each kind of + /// constraint, inserts, updates and deletes, more inserts into the space the deletes freed, a column added, + /// retyped and dropped, indexes made and dropped, and a table dropped and another made after it. + private static string[] Statements() + { + var s = new List + { + "CREATE TABLE Customer (Id COUNTER CONSTRAINT pkCustomer PRIMARY KEY, Name TEXT(50) NOT NULL, City TEXT(30), " + + "Joined DATETIME, Credit CURRENCY, Active BIT)", + "CREATE TABLE Product (Code TEXT(10) CONSTRAINT pkProduct PRIMARY KEY, Title TEXT(100), Price DOUBLE, " + + "Weight REAL, Stock SHORT, Rating BYTE, Notes MEMO)", + "CREATE TABLE Orders (OrderId LONG CONSTRAINT pkOrders PRIMARY KEY, CustomerId LONG, Placed DATETIME, " + + "Total DECIMAL(18,4), Ref GUID, CONSTRAINT fkOrderCustomer FOREIGN KEY (CustomerId) REFERENCES Customer (Id))", + "CREATE TABLE OrderLine (OrderId LONG, LineNo SHORT, ProductCode TEXT(10), Qty LONG, " + + "CONSTRAINT pkOrderLine PRIMARY KEY (OrderId, LineNo))", + "CREATE TABLE Scratch (Id LONG, Data VARBINARY(100), Flag BIT, Label TEXT(20))", + "CREATE INDEX ixCustomerCity ON Customer (City)", + "CREATE UNIQUE INDEX ixProductTitle ON Product (Title)", + "CREATE INDEX ixOrderPlaced ON Orders (Placed DESC)", + }; + + string[] cities = ["London", "Paris", "Berlin", "Madrid", "Rome", "Oslo", "Vienna", "Prague", "Lisbon", "Dublin"]; + for (int i = 1; i <= 12; i++) + s.Add($"INSERT INTO Customer (Name, City, Joined, Credit, Active) VALUES ('Customer {i:D2}', " + + $"'{cities[i % cities.Length]}', #2020-{(i % 12) + 1:D2}-{(i % 27) + 1:D2} {i % 24:D2}:15:00#, {i * 125.5m}, {(i % 3 == 0 ? "FALSE" : "TRUE")})"); + + for (int i = 1; i <= 10; i++) + s.Add($"INSERT INTO Product (Code, Title, Price, Weight, Stock, Rating, Notes) VALUES ('P{i:D3}', 'Product number {i}', " + + $"{i * 9.99}, {i * 0.25}, {i * 7}, {i * 20 % 256}, '{Notes(i)}')"); + + for (int i = 1; i <= 12; i++) + s.Add($"INSERT INTO Orders (OrderId, CustomerId, Placed, Total, Ref) VALUES ({1000 + i}, {(i % 9) + 1}, " + + $"#2021-{(i % 12) + 1:D2}-15#, {i * 101.1234m}, {{guid {{{new Guid(i, 0, 0, [1, 2, 3, 4, 5, 6, 7, (byte)i])}}}}})"); + + for (int i = 1; i <= 16; i++) + s.Add($"INSERT INTO OrderLine (OrderId, LineNo, ProductCode, Qty) VALUES ({1000 + (i % 8) + 1}, {i}, 'P{(i % 10) + 1:D3}', {i % 6})"); + + for (int i = 1; i <= 6; i++) + s.Add($"INSERT INTO Scratch (Id, Data, Flag, Label) VALUES ({i}, 0x{Convert.ToHexString([.. Enumerable.Range(i, 20).Select(b => (byte)b)])}, " + + $"{(i % 2 == 0 ? "TRUE" : "FALSE")}, 'scratch {i}')"); + + s.AddRange( + [ + "UPDATE Customer SET City = 'Paris' WHERE Id = 3", + "UPDATE Customer SET Credit = Credit * 2 WHERE Active = TRUE", + "UPDATE Product SET Price = Price + 1, Stock = Stock - 1", + "UPDATE Product SET Notes = 'short note' WHERE Code = 'P002'", + "UPDATE Orders SET Total = Total + 0.5 WHERE CustomerId = 2", + "DELETE FROM OrderLine WHERE Qty < 2", + "DELETE FROM Scratch WHERE Id > 4", + "DELETE FROM Customer WHERE Id = 12", + "DELETE FROM Product WHERE Code = 'P010'", + ]); + + for (int i = 17; i <= 22; i++) + s.Add($"INSERT INTO OrderLine (OrderId, LineNo, ProductCode, Qty) VALUES ({1000 + (i % 8) + 1}, {i}, 'P{(i % 9) + 1:D3}', {i})"); + + s.AddRange( + [ + "ALTER TABLE Customer ADD COLUMN Email TEXT(80)", + "UPDATE Customer SET Email = 'c' & Id & '@example.com' WHERE Id < 6", + "ALTER TABLE Product ALTER COLUMN Stock LONG", + "ALTER TABLE Scratch DROP COLUMN Flag", + "INSERT INTO Scratch (Id, Data, Label) VALUES (7, 0x0A0B0C, 'after drop')", + "DROP INDEX ixCustomerCity ON Customer", + "CREATE INDEX ixCustomerName ON Customer (Name)", + "DROP TABLE Scratch", + "CREATE TABLE Audit (Id COUNTER CONSTRAINT pkAudit PRIMARY KEY, Detail TEXT(255), LoggedOn DATETIME, Charge CURRENCY)", + ]); + + for (int i = 1; i <= 8; i++) + s.Add($"INSERT INTO Audit (Detail, LoggedOn, Charge) VALUES ('Audit entry {i} {new string('x', i * 20)}', #2022-03-{i:D2} 08:00:00#, {i * 3.25m})"); + + s.AddRange( + [ + "DELETE FROM Audit WHERE Id IN (2, 4, 6)", + "INSERT INTO Audit (Detail, LoggedOn, Charge) VALUES ('refill one', #2022-04-01#, 1)", + "INSERT INTO Audit (Detail, LoggedOn, Charge) VALUES ('refill two', #2022-04-02#, 2)", + "ALTER TABLE Audit ADD COLUMN Severity BYTE", + "UPDATE Audit SET Severity = Id MOD 4", + "UPDATE Customer SET Name = 'Renamed ' & Name WHERE Id MOD 2 = 0", + "DELETE FROM Orders WHERE OrderId = 1012", + "INSERT INTO Customer (Name, City, Joined, Credit, Active, Email) VALUES ('Late comer', 'Oslo', #2023-01-01#, 10, TRUE, 'late@example.com')", + ]); + + return [.. s]; + } + + private static string Notes(int i) => string.Concat(Enumerable.Repeat($"Note {i}. ", i * 12)); + + private static string Abbreviate(string statement) => statement.Length <= 90 ? statement : statement[..87] + "..."; + + private static string?[] RunThroughAce(string path, string[] statements) + { + var errors = new string?[statements.Length]; + using OleDbConnection connection = AceTestDatabase.Open(path); + for (int i = 0; i < statements.Length; i++) + { + try + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = statements[i]; + command.ExecuteNonQuery(); + } + catch (OleDbException e) { errors[i] = e.Message; } + } + return errors; + } + + private static string?[] RunThroughLibRed(string path, string[] statements) + { + var errors = new string?[statements.Length]; + using var database = JetDatabase.Open(path, readOnly: false); + var engine = new QueryEngine(database); + for (int i = 0; i < statements.Length; i++) + { + try { engine.ExecuteNonQuery(statements[i]); } +#pragma warning disable CA1031 // a probe records every refusal, whatever its type, and carries on + catch (Exception e) { errors[i] = $"{e.GetType().Name}: {e.Message}"; } +#pragma warning restore CA1031 + } + return errors; + } + + /// Compares the two results of the same statements applied to : page + /// counts, then every differing page — page 0 on its own, the rest grouped by page type and owner — with its + /// differing byte count and the first few differences. Returns the report and how many pages differ. + private static (string Report, int Pages) Compare(string originPath, string acePath, string noisePath, string libredPath) + { + byte[] origin = File.ReadAllBytes(originPath), ace = File.ReadAllBytes(acePath), + noise = File.ReadAllBytes(noisePath), libred = File.ReadAllBytes(libredPath); + Dictionary owners = Owners(acePath, libredPath); + HashSet[] masks = [.. Enumerable.Range(0, Math.Max(ace.Length, libred.Length) / PageSize).Select(_ => new HashSet())]; + Mask(acePath, ace, masks); + Mask(libredPath, libred, masks); + + int acePages = ace.Length / PageSize, libredPages = libred.Length / PageSize; + int identical = 0, housekeeping = 0, differingPages = 0; + var groups = new SortedDictionary>(StringComparer.Ordinal); + for (int page = 0; page < Math.Max(acePages, libredPages); page++) + { + int at = page * PageSize; + if (page >= acePages || page >= libredPages) + { + byte[] only = page >= acePages ? libred : ace; + Add(groups, "pages in one file only", $"page {page}: {(page >= acePages ? "LibRed" : "ACE")} only, " + + $"type 0x{only[at]:X2} {Owner(owners, only, at)}"); + differingPages++; + continue; + } + + var differing = new List(); + for (int i = 0; i < PageSize; i++) + { + if (ace[at + i] == libred[at + i] || masks[page].Contains(i)) continue; + // A byte ACE writes merely for holding a session, which LibRed need not reproduce. + if (at + i < origin.Length && at + i < noise.Length && origin[at + i] != noise[at + i]) { housekeeping++; continue; } + differing.Add(i); + } + if (differing.Count == 0) { identical++; continue; } + + differingPages++; + string detail = string.Join(" ", differing.Take(8).Select(i => $"+0x{i:X3}:{ace[at + i]:X2}/{libred[at + i]:X2}")); + string kind = page == 0 ? "page 0" + : $"type 0x{ace[at]:X2}{(ace[at] != libred[at] ? $"/0x{libred[at]:X2}" : "")} {Owner(owners, ace, at)}"; + Add(groups, kind, $"page {page}: {differing.Count} bytes {detail}"); + } + + var report = new StringBuilder(); + report.AppendLine(CultureInfo.InvariantCulture, + $"pages: before {origin.Length / PageSize}, ACE {acePages}, LibRed {libredPages}; identical {identical}; " + + $"{housekeeping} byte(s) differ only where ACE's session alone writes (ace/libred shown)"); + foreach (var (kind, lines) in groups) + { + report.AppendLine(CultureInfo.InvariantCulture, $"-- {kind}: {lines.Count} page(s)"); + foreach (string line in lines) report.AppendLine(CultureInfo.InvariantCulture, $" {line}"); + } + return (report.ToString(), differingPages); + } + + private static void Add(SortedDictionary> groups, string kind, string line) + { + if (!groups.TryGetValue(kind, out var lines)) groups[kind] = lines = []; + lines.Add(line); + } + + /// What owns a page: for a data or index page the table whose TDEF page its header names, for a TDEF + /// page its own table, where either file's catalog knows the name. + private static string Owner(Dictionary owners, byte[] file, int at) + { + if ((PageType)file[at] is PageType.TableDefinition or PageType.ReleasedTableDefinition) + return owners.TryGetValue(at / PageSize, out string? self) ? $"({self})" : ""; + if ((PageType)file[at] is not (PageType.DataPage or PageType.IntermediateIndexPage or PageType.LeafIndexPage)) + return ""; + if (file.AsSpan(at + 4, 4).SequenceEqual("LVAL"u8)) return "(long values)"; + int owner = BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(at + 4, 4)); + return owners.TryGetValue(owner, out string? name) ? $"({name})" : $"(tdef {owner})"; + } + + private static Dictionary Owners(params string[] paths) + { + var owners = new Dictionary(); + foreach (string path in paths) + { + using var database = JetDatabase.Open(path, readOnly: true); + foreach (TableDef table in database.Catalog.Tables) owners.TryAdd(table.DefinitionPage, table.Name); + } + return owners; + } + + /// Masks the bytes known to differ for reasons other than the format: page 0's user commit-byte table, + /// a count of each lock-file user's committed writes that LibRed, keeping no lock file, leaves alone + /// (page-00-database.md §2.2); each MSysObjects row's wall-clock DateCreate and DateUpdate; and every data page's + /// write stamp. + private static void Mask(string path, byte[] file, HashSet[] masks) + { + for (int i = 0xE00; i < PageSize; i++) masks[0].Add(i); + + using var database = JetDatabase.Open(path, readOnly: true); + TableDef objects = database.Catalog.FindTable("MSysObjects")!; + int[] dates = [.. new[] { "DateCreate", "DateUpdate" }.Select(c => objects.FindColumn(c)!.FixedOffset)]; + + for (int page = 1; page < file.Length / PageSize; page++) + { + int at = page * PageSize; + if ((PageType)file[at] != PageType.DataPage) continue; + for (int i = 0x08; i < 0x0C; i++) masks[page].Add(i); + if (BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(at + 4, 4)) != objects.DefinitionPage) continue; + + int rows = BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(at + 0x0C, 2)); + for (int row = 0; row < rows; row++) + { + int slot = BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(at + 0x0E + 2 * row, 2)); + if ((slot & 0xC000) != 0) continue; + int start = slot & 0x1FFF; + foreach (int offset in dates) + for (int i = 0; i < 8; i++) masks[page].Add(start + 2 + offset + i); + } + } + } + + /// An empty ACE 12 database made by DAO, the path Access itself takes; null where DAO is missing. + private static string? CreateEmptyThroughDao() + { + object? engine = AceTestDatabase.CreateDaoEngine(); + if (engine is null) return null; + string path = TemporaryDatabase.CreatePath("wholefile-origin-"); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + object database = Invoke(workspace, "CreateDatabase", path, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; + Invoke(database, "Close"); + return path; + } + + private static object? Invoke(object target, string member, params object?[] args) => + target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); +} From 613fe438b9b5623cd60270467f5195c770296f92 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sat, 26 Sep 2026 23:59:49 +0800 Subject: [PATCH 22/88] Split, build and renumber as ACE does, found by diffing tables across pages The whole-file probe now also runs a thousand-row INSERT ... SELECT, a 253-column table and long values chained across pages. What it found about index splits, measured against ACE and now written the same way: - A root that splits keeps its page: both halves move to new pages, left first, and the root becomes the node over them. - A full page is compressed in place before it splits, and the split is made over that image; the left half keeps its prefix unless the new entry became its first, when it is written whole. - The cut keeps every old entry that starts before the page's byte midpoint, the new entry joining its side; a new first entry halves the entries by count. - Nodes fill, compress and split as leaves do, link to their siblings, and split at the right edge when the new separator is last. - Bytes past each half's live end match ACE's, including the promoted entry a node split leaves behind. CREATE INDEX over existing rows writes the tree sequential inserts leave, so the bulk build now follows the right edge of that tree instead of building node levels bottom up, and no longer moves the index's root. ADD COLUMN writes descriptor 0x09 as ACE does: it is DAO's OrdinalPosition, which ADD COLUMN compacts to ranks, and an added fixed column's variable-table index counts dropped variable columns too. Page 0's 0x30 and 0x34 are bounded by the largest possible page, not by the file's length, as the spec had said. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/IndexDef.cs | 2 +- src/LibRed/LibRed.Core/Storage/IndexWriter.cs | 456 +++++++++++------- .../LibRed.Core/Storage/TableCreator.cs | 32 +- src/LibRed/docs/format/appendix-structures.md | 8 +- src/LibRed/docs/format/page-00-database.md | 16 +- src/LibRed/docs/format/page-02b-columns.md | 38 +- .../docs/format/page-03-04-index-btree.md | 103 ++-- test/LibRed.Benchmarks/History.tsv | 1 + .../ColumnNumberingAccessTests.cs | 153 ++++++ .../IndexRootSplitByteParityTests.cs | 252 ++++++++++ .../WholeFileParityProbeTest.cs | 85 +++- 11 files changed, 921 insertions(+), 225 deletions(-) create mode 100644 test/LibRed.Engine.AccessTests/ColumnNumberingAccessTests.cs create mode 100644 test/LibRed.Engine.AccessTests/IndexRootSplitByteParityTests.cs diff --git a/src/LibRed/LibRed.Core/Catalog/IndexDef.cs b/src/LibRed/LibRed.Core/Catalog/IndexDef.cs index 089ae9f8..ce219223 100644 --- a/src/LibRed/LibRed.Core/Catalog/IndexDef.cs +++ b/src/LibRed/LibRed.Core/Catalog/IndexDef.cs @@ -27,7 +27,7 @@ public sealed record IndexDef /// public int UniqueEntryCount { get; init; } - /// Page number of the index B-tree root. Updated in place when the root splits (grows a level). + /// Page number of the index B-tree root. Neither a split nor a bulk build moves it; a rebuild onto a fresh root does. public int RootPage { get; internal set; } /// diff --git a/src/LibRed/LibRed.Core/Storage/IndexWriter.cs b/src/LibRed/LibRed.Core/Storage/IndexWriter.cs index 4635eeed..69052c45 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexWriter.cs @@ -9,8 +9,8 @@ namespace LibRed.Storage; /// /// Maintains an index B-tree on row insert: descends from the root to the target leaf, inserts the key, /// and — when a page overflows — splits it, promoting a separator into the parent and propagating -/// splits up the tree (growing a new root when the root itself splits, and repointing the index-data -/// block's root). Leaf pages keep their doubly-linked prev/next chain; pages are written with prefix +/// splits up the tree (the tree grows a level when the root itself splits, the root keeping its page). +/// Leaf pages keep their doubly-linked prev/next chain; pages are written with prefix /// compression. A key column whose collating order LibRed cannot encode still throws — see /// for which orders it can. /// @@ -221,7 +221,8 @@ private bool UnlinkEmptyLeaf(IndexDef index, List path, CheckedIndexPage le int leafPage = path[^1]; int parentPage = path[^2]; - (List entries, int tail) = Parse(ReadMutationPage(parentPage, PageType.IntermediateIndexPage)); + CheckedIndexPage parent = ReadMutationPage(parentPage, PageType.IntermediateIndexPage); + (List entries, int tail) = Parse(parent); int slot = entries.FindIndex(e => e.Trailer == leafPage); if (slot >= 0) @@ -242,12 +243,14 @@ private bool UnlinkEmptyLeaf(IndexDef index, List path, CheckedIndexPage le $"Index '{index.Name}': node {parentPage} does not point at leaf {leafPage}."); } - if (leaf.Previous != 0) SetLeafLink(leaf.Previous, NextPageOffset, leaf.Next); - if (leaf.Next != 0) SetLeafLink(leaf.Next, PrevPageOffset, leaf.Previous); + if (leaf.Previous != 0) SetSiblingLink(leaf.Previous, NextPageOffset, leaf.Next, PageType.LeafIndexPage); + if (leaf.Next != 0) SetSiblingLink(leaf.Next, PrevPageOffset, leaf.Previous, PageType.LeafIndexPage); - // Dropping a separator only shrinks the node, so Build never overflows. A leaf's parent is one level - // above the leaves by definition. - WriteOrThrow(parentPage, Build(PageType.IntermediateIndexPage, 0, 0, tail, level: 1, entries)); + // Dropping a separator only shrinks the node, so Build never overflows, and the node keeps its prefix + // as a leaf does on a delete (see RemoveEntry). A leaf's parent is one level above the leaves by + // definition. + WriteOrThrow(parentPage, Build(PageType.IntermediateIndexPage, parent.Previous, parent.Next, tail, level: 1, + entries, parent.CompressedByteCount)); // The page leaves the index the way AllocateIndexPage brought it in: its bit out of the index's own // pages map, then released — held until this handle closes, the route ACE takes for a freed page. @@ -388,23 +391,46 @@ private void InsertIntoLeaf(IndexDef index, List path, byte[] key, int poin // AutoNumber and identity keys are ascending by construction, so this is the ordinary case. The // condition cannot fire on a random insert, which is why the general behaviour is unchanged. // - // Everywhere else ACE halves the entries the page held BEFORE this insert, and the new entry then - // joins whichever side its key falls in — so a key landing below the midpoint leaves one MORE entry - // behind than a key landing on or above it. Halving the post-insert list instead always hands the odd - // entry to the right page, which agrees with ACE only for the upper half. Measured on a 602-entry - // leaf split by one further key (§10.5): ACE keeps 302 entries for a key at position 1 or 50, and 301 - // for one at 301, 302 or 400 — so the midpoint itself goes right. - int mid = (entries.Count - 1) / 2; // midpoint of the pre-insert entries - int splitAt = pos == entries.Count - 1 - ? entries.Count - 1 - : Math.Max(1, pos < mid ? mid + 1 : mid); // never leave the left page empty + // Compressing did not make room, but ACE still compresses first: the page's old entries are rewritten + // in place at the prefix they share, and the split is made over that. It shows past each half's live + // end, where the compressed entries stand. The left half then stays at that prefix — unless the new + // entry became its first, when ACE writes it whole. (A new first entry that does not split the page + // keeps the prefix: verified both ways.) + var old = new List(entries); + old.RemoveAt(pos); + int oldShare = old.Count <= 1 ? 0 : CommonPrefixLength(old[0].Key, old[^1].Key); + int stored = Math.Max(page.Compressed, oldShare); + if (oldShare > page.Compressed) + WriteOrThrow(leafPage, Build(PageType.LeafIndexPage, page.Previous, page.Next, 0, 0, old, oldShare)); + + // Everywhere else ACE cuts that compressed page at its byte midpoint — every old entry that STARTS before + // it stays left, so the entry straddling it does too — and the new entry joins whichever half its key + // falls in. A key landing below the cut therefore leaves one MORE entry behind than a key landing on or + // above it. With equal entries that is the old entries halved, the odd one left: a 17-entry leaf keeps + // 10 for a key at any position from 1 to 8 and 9 from 9 to 16, and a 602-entry one keeps 302 for a key at + // 1 or 50 and 301 at 301, 302 or 400. It is bytes, not entries, when keys differ in length: a 91-entry + // leaf of 38- to 40-byte entries keeps 45, not 46. A new FIRST entry is the exception: then the entries + // including it are halved by count, rounding up — 9 of 18, 302 of 603 (§10.5). + int splitAt; + if (pos == entries.Count - 1) splitAt = entries.Count - 1; + else if (pos == 0) splitAt = (entries.Count + 1) / 2; + else + { + long total = -(long)stored * (old.Count - 1); + foreach (Entry e in old) total += e.Key.Length + TrailerSize; + int oldLeft = 0; + for (long start = 0; oldLeft < old.Count && start * 2 < total; oldLeft++) + start += old[oldLeft].Key.Length + TrailerSize - (oldLeft == 0 ? 0 : stored); + splitAt = pos < oldLeft ? oldLeft + 1 : oldLeft; + } + SplitAndPropagate(index, path, path.Count - 1, entries, PageType.LeafIndexPage, - page.Previous, page.Next, splitAt); + page.Previous, page.Next, splitAt, leftPrefix: pos == 0 ? 0 : stored, newFirst: pos == 0); } /// /// Splits the (leaf or node) page at into two, writes both, then promotes a - /// separator into the parent — splitting parents in turn, or growing a new root at the top. + /// separator into the parent — splitting parents in turn, or turning the root into the node over both halves. /// /// The index whose tree is being split. /// The pages from the root down to the one being split, one per level. @@ -413,12 +439,19 @@ private void InsertIntoLeaf(IndexDef index, List path, byte[] key, int poin /// Leaf or node — what the two halves are written as. /// The split page's left sibling, for the leaf chain. /// Its right sibling. - /// How many entries stay on the left page; negative for the default half. Only a - /// leaf split sets it, to keep a page full when the new entry is its maximum (see InsertIntoLeaf). + /// How many entries stay on the left page; negative for the default half. A leaf split + /// always sets it (see InsertIntoLeaf); a node split sets it for its right-edge case (see InsertSeparator), + /// where the entry at is the one promoted. + /// The prefix a leaf's left half is written at; null for the largest available, + /// which is what the right half and a node's halves are written at. + /// Whether the entry just inserted is a leaf's first. private void SplitAndPropagate(IndexDef index, List path, int level, List entries, - PageType type, int prev, int next, int splitAt = -1) + PageType type, int prev, int next, int splitAt = -1, int? leftPrefix = null, bool newFirst = false) { - int leftPage = path[level]; + // The root never moves: when it splits, both halves go to new pages, left first, and the root page is + // rewritten as the node over them — so the index-data block's root pointer stays as it is. The left half + // is the root's page carried over, so what lies past its live end is what the root held there. + int leftPage = level == 0 ? AllocateIndexPage(index) : path[level]; int rightPage = AllocateIndexPage(index); int nodeLevel = path.Count - 1 - level; // height above the leaves of the page being split @@ -432,32 +465,37 @@ private void SplitAndPropagate(IndexDef index, List path, int level, List old root] with the new page as its tail. - int newRoot = AllocateIndexPage(index); - WriteOrThrow(newRoot, Build(PageType.IntermediateIndexPage, 0, 0, tail: rightPage, nodeLevel + 1, + // The root split: the root becomes the node [promoted -> left] with the right page as its tail. Its + // bytes past the new live end stay as they were, as on any rewrite of a page in place. + WriteOrThrow(path[0], Build(PageType.IntermediateIndexPage, 0, 0, tail: rightPage, nodeLevel + 1, [new Entry(promoted, leftPage)])); - UpdateIndexRoot(index, newRoot); - index.RootPage = newRoot; // keep the in-memory def in step for the next insert return; } @@ -472,27 +510,46 @@ private void InsertSeparator(IndexDef index, List path, int level, int oldC CheckedIndexPage page = ReadMutationPage(parentPage, PageType.IntermediateIndexPage); (List entries, int tail) = Parse(page); + // The old child's pointer is repointed first, so the page ACE compresses in place when the separator + // does not fit already carries it (see below). int slot = entries.FindIndex(e => e.Trailer == oldChild); - if (slot >= 0) - { - entries[slot] = entries[slot] with { Trailer = newRight }; - entries.Insert(slot, new Entry(promoted, oldChild)); - } - else // oldChild was the tail + if (slot >= 0) entries[slot] = entries[slot] with { Trailer = newRight }; + else tail = newRight; // oldChild was the tail + var old = new List(entries); + entries.Insert(slot >= 0 ? slot : entries.Count, new Entry(promoted, oldChild)); + + // A node fills, compresses and splits exactly as a leaf does (see InsertIntoLeaf). + int parentLevel = path.Count - 1 - level; + int share = CommonPrefixLength(entries[0].Key, entries[^1].Key); + int keep = Math.Min(page.CompressedByteCount, share); + if (Build(PageType.IntermediateIndexPage, page.Previous, page.Next, tail, parentLevel, entries, keep) + is { } asIs) { - tail = newRight; - entries.Add(new Entry(promoted, oldChild)); + _channel.WritePage(parentPage, KeepTail(parentPage, asIs)); + return; } - int parentLevel = path.Count - 1 - level; - if (Build(PageType.IntermediateIndexPage, 0, 0, tail, parentLevel, entries) is { } built) + if (share > keep + && Build(PageType.IntermediateIndexPage, page.Previous, page.Next, tail, parentLevel, entries, share) + is { } compressed) { - _channel.WritePage(parentPage, KeepTail(parentPage, built)); + _channel.WritePage(parentPage, KeepTail(parentPage, compressed)); return; } + // As a leaf does, the full node is compressed in place before it splits (see InsertIntoLeaf). + int oldShare = old.Count <= 1 ? 0 : CommonPrefixLength(old[0].Key, old[^1].Key); + if (oldShare > page.CompressedByteCount) + WriteOrThrow(parentPage, + Build(PageType.IntermediateIndexPage, page.Previous, page.Next, tail, parentLevel, old, oldShare)); + + // A node has a right-edge split too: when the new separator is its last entry — the child that split was + // the tail, as on every split of an ascending load — the left node keeps every old entry but the last, + // that one is promoted, and the new separator starts the right node alone (verified vs ACE: a node of 16 + // separators taking a 17th is cut 15, promote 1, 1). Anywhere else it is cut in the middle. _splitTail = tail; - SplitAndPropagate(index, path, level, entries, PageType.IntermediateIndexPage, 0, 0); + SplitAndPropagate(index, path, level, entries, PageType.IntermediateIndexPage, page.Previous, page.Next, + splitAt: slot < 0 ? entries.Count - 2 : -1); } private int _splitTail; // carries a node's tail into SplitAndPropagate @@ -517,13 +574,13 @@ private CheckedIndexPage ReadMutationPage(int pageNumber, PageType expectedType) return page; } - /// Builds a page from entries; null if they overflow the page. Leaf pages are prefix-compressed; - /// node pages are stored uncompressed and carry their height above the leaves at , - /// both matching what Access writes. (An isolation test showed neither is strictly required — Access reads - /// a node with 0x1A=0 and compressed just fine; they are kept purely for byte-faithfulness. The one - /// hard requirement is a leaf's 0x1A=0 and the leaf-chain offsets at 0x0C/0x10.) + /// Builds a page from entries; null if they overflow the page. Leaf and node pages alike are + /// prefix-compressed at the length the caller gives, and a node carries its height above the leaves at + /// , matching what Access writes. (An isolation test showed a node's height is not + /// required — Access reads a node with 0x1A=0 just fine; it is kept purely for byte-faithfulness. The + /// one hard requirement is a leaf's 0x1A=0 and the leaf-chain offsets at 0x0C/0x10.) /// Leaf or node. - /// The page's left sibling, written into the leaf chain. + /// The page's left sibling at the same level. /// Its right sibling. /// The page's trailing pointer — a node's rightmost child. /// A node's height above the leaves; 0 on a leaf. @@ -533,7 +590,6 @@ private CheckedIndexPage ReadMutationPage(int pageNumber, PageType expectedType) private byte[]? Build(PageType type, int prev, int next, int tail, int level, List entries, int? prefix = null) { - bool isLeaf = type == PageType.LeafIndexPage; int pageSize = _channel.PageSize; var page = new byte[pageSize]; page[0] = (byte)type; @@ -544,10 +600,10 @@ private CheckedIndexPage ReadMutationPage(int pageNumber, PageType expectedType) WriteInt32Le(page, NextPageOffset, next); WriteInt32Le(page, ChildTailOffset, tail); - // A single entry (or a node) has no common-prefix compression — ACE writes 0 here (the whole key with - // itself would otherwise "compress" to its full length, which ACE does not do for one entry). - // Otherwise the caller chooses: see InsertIntoLeaf for when a page is compressed at all. - int compress = !isLeaf || entries.Count <= 1 + // A single entry has no common-prefix compression — ACE writes 0 here (the whole key with itself would + // otherwise "compress" to its full length, which ACE does not do for one entry). Otherwise the caller + // chooses, for a node as for a leaf: see InsertIntoLeaf for when a page is compressed at all. + int compress = entries.Count <= 1 ? 0 : prefix ?? CommonPrefixLength(entries[0].Key, entries[^1].Key); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(CompressedByteCountOffset, 2), (ushort)compress); @@ -581,9 +637,9 @@ private CheckedIndexPage ReadMutationPage(int pageNumber, PageType expectedType) return page; } - /// Repoints the index-data block's B-tree root (0x26) — after the root grows a level, or when a new - /// table's foreign-key index is given its root. Walks stats → column descriptors → column names → data blocks - /// to the index's block. + /// Repoints the index-data block's B-tree root (0x26) — when a new table's foreign-key index is given + /// its root (a split never moves one). Walks stats → column descriptors → column names → data blocks to the + /// index's block. /// /// A wide table's definition spans continuation pages, and the data blocks sit past the column names — /// well beyond the first page for a 255-column table. The walk therefore runs over the stitched @@ -595,8 +651,8 @@ internal void UpdateIndexRoot(IndexDef index, int newRoot) (_, IReadOnlyList continuations, int block) = LocateIndexBlock(index); WriteInt32IntoDefinition(continuations, block + IndexBlockFormat.RootPageOffset, newRoot); // The root pointer is part of the definition every other handle caches: until they reload it they - // descend from the old root — by now only the leftmost page of the tree — and an insert through one - // splits that page as if it were the root and writes it over this one. + // descend from the old root — by now only a page inside the tree — and an insert through one would + // split that page as if it were the root and write it over this one. _channel.MarkSchemaChanged(); } @@ -688,21 +744,26 @@ private void WriteInt32IntoDefinition(IReadOnlyList continuations, int offs } } - /// Patches one end of a neighbouring leaf's chain link — or - /// — without disturbing its entries. A split repairs the back-link of the leaf - /// it pushed right; unlinking an emptied leaf repairs both of its neighbours. - private void SetLeafLink(int pageNumber, int offset, int target) + /// Patches one end of a neighbouring page's sibling link — or + /// — without disturbing its entries. A split repairs the back-link of the page + /// it pushed right, leaf or node; unlinking an emptied leaf repairs both of its neighbours. + private void SetSiblingLink(int pageNumber, int offset, int target, PageType type) { CheckedIndexPage checkedPage = IndexPageReader.Read(_channel, pageNumber, _table.DefinitionPage); - if (checkedPage.Type != PageType.LeafIndexPage) - throw new InvalidDataException($"Leaf chain pointer targets non-leaf page {pageNumber}."); + if (checkedPage.Type != type) + throw new InvalidDataException( + $"Sibling pointer targets page {pageNumber}, a {checkedPage.Type} where a {type} was expected."); byte[] page = checkedPage.Buffer.Span.ToArray(); WriteInt32Le(page, offset, target); _channel.WritePage(pageNumber, page); } - private void WriteOrThrow(int pageNumber, byte[]? page) => - _channel.WritePage(pageNumber, KeepTail(pageNumber, page ?? throw new NotSupportedException( + /// The page to write. + /// The built page; null when its entries overflowed. + /// The page whose dead bytes the write carries over — this page itself, unless the + /// content is moving here from another (a split root's left half). + private void WriteOrThrow(int pageNumber, byte[]? page, int? tailFrom = null) => + _channel.WritePage(pageNumber, KeepTail(tailFrom ?? pageNumber, page ?? throw new NotSupportedException( "An index page still overflows after a split (a key wider than half a page)."))); /// @@ -722,21 +783,44 @@ private void WriteOrThrow(int pageNumber, byte[]? page) => /// The bytes kept are dead: they sit past the free-space boundary, so no reader reaches them. /// Keeping them is for byte-faithfulness with ACE, not for meaning. /// - private byte[] KeepTail(int pageNumber, byte[] built) + private byte[] KeepTail(int pageNumber, byte[] built) => Merge(built, ImageOf(pageNumber)); + + /// What a rewrite of keeps past its live end: the page's bytes when it + /// is already one of this table's index pages, zeros otherwise (see ). + private byte[] ImageOf(int pageNumber) { - if (pageNumber >= _channel.PageCount) return built; + if (pageNumber >= _channel.PageCount) return new byte[_channel.PageSize]; + // A leaf turning node — the root, when it splits — still counts as the same page rewritten. ReadOnlySpan existing = _channel.ReadPage(pageNumber).Span; - if (existing[0] != built[0] + if (existing[0] is not ((byte)PageType.LeafIndexPage or (byte)PageType.IntermediateIndexPage) || BinaryPrimitives.ReadInt32LittleEndian(existing[OwnerOffset..]) != _table.DefinitionPage) - return built; + return new byte[_channel.PageSize]; + return existing.ToArray(); + } - int liveEnd = _channel.PageSize - - BinaryPrimitives.ReadUInt16LittleEndian(built.AsSpan(FreeSpaceOffset, 2)); - existing[liveEnd..].CopyTo(built.AsSpan(liveEnd)); + /// Carries 's bytes past 's live end into it. + private byte[] Merge(byte[] built, byte[] image) + { + int liveEnd = LiveEnd(built); + image.AsSpan(liveEnd).CopyTo(built.AsSpan(liveEnd)); return built; } + /// Stores a split node's promoted middle entry just past its left half's live end, at the page's + /// prefix: ACE writes it into the page and then ends the page before it. + private void LeaveMiddleBehind(byte[] leftBytes, int prev, int right, int level, List left, Entry middle) + { + int prefix = BinaryPrimitives.ReadUInt16LittleEndian(leftBytes.AsSpan(CompressedByteCountOffset, 2)); + byte[] withMiddle = Build(PageType.IntermediateIndexPage, prev, right, middle.Trailer, level, [.. left, middle], prefix)!; + int leftEnd = LiveEnd(leftBytes); + withMiddle.AsSpan(leftEnd, LiveEnd(withMiddle) - leftEnd).CopyTo(leftBytes.AsSpan(leftEnd)); + } + + /// Where a built page's entries end: everything past it is free space. + private int LiveEnd(byte[] page) => + _channel.PageSize - BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(FreeSpaceOffset, 2)); + /// Width of the row/child pointer an entry carries after its key. private const int TrailerSize = 4; @@ -749,26 +833,28 @@ private static byte[] WithTrailer(byte[] key, int trailer) } /// - /// Fills an empty index from by writing each page once, instead of - /// inserting the entries one at a time and rewriting a whole leaf per entry. + /// Fills an empty index from , leaving exactly the pages that inserting them + /// one at a time in key order would leave, but building a page only when its layout changes rather than + /// rewriting it for every entry. /// /// The empty index to fill. /// (key, row pointer) pairs with a NullKey marker; any order. Sorted here. /// Enforce uniqueness — adjacent equal keys after the sort, null keys exempt /// (Jet's uniqueness is over the non-null keys only). /// - /// This is not a different B-tree: it is the SAME fill performs, driven - /// over pre-sorted input so each page is finished before the next begins. A sequential load already took - /// the right-edge split — a new entry that is the page's maximum leaves the full page alone and starts a - /// fresh one — so feeding sorted entries reproduces the leaf partitioning ACE produces; the difference is - /// only that a page is built once rather than rebuilt per entry. + /// This is what ACE's CREATE INDEX writes: its tree is the incremental one for sorted input, + /// byte for byte — the root keeps its page, leaves fill uncompressed, are compressed in place when full and + /// split at the right edge, and the nodes above fill, compress and split the same way, all allocated in the + /// order those splits happen (verified vs ACE). Sorted input only ever reaches the right edge of the tree, + /// so the one page per level being filled there — the spine — is all that is held; every page to its left is + /// finished, and written, when it is split off. /// Whether an entry fits is decided by arithmetic, not by calling : Build /// allocates a page-sized array, so probing with it would allocate two pages per entry and lose exactly /// what this exists to save. mirrors Build's layout — entry data starts at /// 0x1E0, the first entry stores its whole key, the rest drop the shared prefix, and each carries a - /// 4-byte trailer (see page-01/page-03-04 §10.2–10.3). - /// Node levels are then built bottom-up, splitting at the middle and promoting the middle entry as - /// does, so interior pages keep the shape the incremental path gives them. + /// 4-byte trailer (see page-01/page-03-04 §10.2–10.3). A page is built only when the prefix it is stored at + /// changes, when it splits, and at the end: the incremental path's writes between those only extend the + /// same layout, so they leave nothing past the live end that a later write keeps. /// internal void BulkBuild(IndexDef index, List<(byte[] Key, int Pointer, bool NullKey)> entries, bool rejectDuplicates) { @@ -784,102 +870,140 @@ internal void BulkBuild(IndexDef index, List<(byte[] Key, int Pointer, bool Null throw new InvalidOperationException( $"Cannot create unique index '{index.Name}' on '{_table.Name}': duplicate key values exist."); - // --- leaves --------------------------------------------------------------------------------------- - var separators = new List<(byte[] Key, int Child)>(); - int page = index.RootPage; // the empty leaf the caller created; it becomes the first leaf - int previous = 0; - var current = new List(); - long keyBytes = 0; // running sum of the current page's key lengths - int compressed = 0; // the prefix the page would be written at, as filling stands - + // The empty leaf the caller created is the root, and stays the root however tall the tree grows. + var spine = new List { new(index.RootPage, previous: 0, ImageOf(index.RootPage)) }; foreach ((byte[] key, int pointer, _) in entries) - { - current.Add(new Entry(key, pointer)); - keyBytes += key.Length; + Append(index, spine, level: 0, new Entry(key, pointer)); - int share = current.Count <= 1 ? 0 : CommonPrefixLength(current[0].Key, current[^1].Key); - if (LeafFits(current.Count, keyBytes, Math.Min(compressed, share))) - continue; - if (share > compressed && LeafFits(current.Count, keyBytes, share)) - { - compressed = share; // the compress-in-place step, without writing the page yet - continue; - } - - // Full. The entry that did not fit starts the next leaf, as the right-edge split does. - current.RemoveAt(current.Count - 1); - keyBytes -= key.Length; - int next = AllocateIndexPage(index); - WriteOrThrow(page, Build(PageType.LeafIndexPage, previous, next, tail: 0, level: 0, current, Prefix())); - separators.Add((WithTrailer(current[^1].Key, current[^1].Trailer), page)); - - previous = page; - page = next; - current = [new Entry(key, pointer)]; - keyBytes = key.Length; - compressed = 0; + for (int level = 0; level < spine.Count; level++) + { + SpinePage page = spine[level]; + _channel.WritePage(page.Number, Merge(BuildSpine(page, level, next: 0, page.Entries, page.Stored), page.Image)); } + } - WriteOrThrow(page, Build(PageType.LeafIndexPage, previous, next: 0, tail: 0, level: 0, current, Prefix())); - - // --- node levels, until one page covers the level -------------------------------------------------- - int level = 1; - while (separators.Count > 0) - (separators, page) = BuildNodeLevel(index, separators, tailChild: page, level++); + /// The page being filled at one level of a : its entries, the prefix they + /// are stored at, and what the incremental path's writes would have left past the live end. + private sealed class SpinePage(int number, int previous, byte[] image) + { + public int Number { get; } = number; + public int Previous { get; } = previous; + public byte[] Image { get; set; } = image; + public List Entries { get; } = []; + public long KeyBytes { get; set; } + public int Stored { get; set; } // the prefix the page is stored at (0x18), as the last write left it + public int Tail { get; set; } // a node's rightmost child + } - if (page != index.RootPage) + /// Appends an entry to the page at of the spine as an insert of the page's + /// new maximum would: at the prefix the page is stored at, compressed in place when that is what makes room, + /// and split at the right edge when nothing does (see , ). + private void Append(IndexDef index, List spine, int level, Entry entry) + { + SpinePage page = spine[level]; + page.Entries.Add(entry); + page.KeyBytes += entry.Key.Length; + + int share = Share(page.Entries); + int keep = Math.Min(page.Stored, share); + if (LeafFits(page.Entries.Count, page.KeyBytes, keep)) { StoreAt(page, level, keep); return; } + if (share > keep && LeafFits(page.Entries.Count, page.KeyBytes, share)) { StoreAt(page, level, share); return; } + + // Full: the page as it stood, compressed in place when its entries share more than it is stored at, and + // then split with the new entry starting the next page. + page.Entries.RemoveAt(page.Entries.Count - 1); + page.KeyBytes -= entry.Key.Length; + Materialize(page, level, page.Entries, page.Stored); + int oldShare = Share(page.Entries); + if (oldShare > page.Stored) { - UpdateIndexRoot(index, page); - index.RootPage = page; + page.Stored = oldShare; + Materialize(page, level, page.Entries, oldShare); } + SplitSpine(index, spine, level, entry); + } - // A leaf is written at the prefix its filling reached — compressed only once it had to be, as the - // incremental path writes it — and no further than its entries share (verified: ACE's CREATE INDEX over - // eleven keys sharing ten bytes writes them whole, at prefix 0). - int Prefix() => current.Count <= 1 ? 0 : Math.Min(compressed, CommonPrefixLength(current[0].Key, current[^1].Key)); + /// Moves a spine page to . When that changes its layout, the write before + /// the entry just appended is what stands past the new live end, so it is made concrete first. + private void StoreAt(SpinePage page, int level, int prefix) + { + if (prefix == page.Stored) return; + Materialize(page, level, page.Entries.GetRange(0, page.Entries.Count - 1), page.Stored); + page.Stored = prefix; } - /// Whether entries totalling of key data fit - /// one leaf at prefix — Build's layout, without building anything. - private bool LeafFits(int count, long keyBytes, int prefix) => - EntryDataOffset + keyBytes + (long)TrailerSize * count - (long)prefix * (count - 1) <= _channel.PageSize; + /// What writing at over the page leaves. + private void Materialize(SpinePage page, int level, List entries, int prefix) => + page.Image = Merge(BuildSpine(page, level, next: 0, entries, prefix), page.Image); - /// Writes one level of interior nodes over , returning the separators - /// promoted to the level above and the page that is this level's rightmost (tail) child. - private (List<(byte[] Key, int Child)> Promoted, int Tail) BuildNodeLevel( - IndexDef index, List<(byte[] Key, int Child)> children, int tailChild, int level) + private byte[] BuildSpine(SpinePage page, int level, int next, List entries, int prefix) => + Build(level == 0 ? PageType.LeafIndexPage : PageType.IntermediateIndexPage, page.Previous, next, page.Tail, + level, entries, prefix) + ?? throw new NotSupportedException("An index page overflows (a key wider than half a page)."); + + /// The right-edge split of the spine page at , as + /// makes it: the finished left half is written, the new entry starts the + /// right half, which takes the page's place on the spine, and the separator goes up a level — or, when the + /// page is the root, both halves move out and the root becomes the node over them. + private void SplitSpine(IndexDef index, List spine, int level, Entry entry) { - var promoted = new List<(byte[] Key, int Child)>(); - var current = new List(); - long keyBytes = 0; - int page = AllocateIndexPage(index); + SpinePage page = spine[level]; + bool root = level == spine.Count - 1; + int left = root ? AllocateIndexPage(index) : page.Number; + int right = AllocateIndexPage(index); + + byte[] leftBytes, promoted; + int rightTail; + if (level == 0) + { + // Every old entry stays, at the prefix the page is stored at; a copy of the last is promoted. + Entry last = page.Entries[^1]; + promoted = WithTrailer(last.Key, last.Trailer); + leftBytes = Merge(Build(PageType.LeafIndexPage, page.Previous, right, tail: 0, level: 0, page.Entries, + page.Stored)!, page.Image); + rightTail = 0; + } + else + { + // A node's right-edge split: its last entry is promoted, that entry's child becoming the left node's + // tail, and the new separator starts the right node under the old tail. + Entry middle = page.Entries[^1]; + List kept = page.Entries.GetRange(0, page.Entries.Count - 1); + promoted = middle.Key; + leftBytes = Merge(Build(PageType.IntermediateIndexPage, page.Previous, right, middle.Trailer, level, kept)!, + page.Image); + LeaveMiddleBehind(leftBytes, page.Previous, right, level, kept, middle); + rightTail = page.Tail; + } + _channel.WritePage(left, leftBytes); + + var next = new SpinePage(right, previous: left, ImageOf(right)) { Tail = rightTail }; + next.Entries.Add(entry); + next.KeyBytes = entry.Key.Length; + spine[level] = next; - foreach ((byte[] key, int child) in children) + if (root) { - current.Add(new Entry(key, child)); - keyBytes += key.Length; - if (EntryDataOffset + keyBytes + (long)TrailerSize * current.Count <= _channel.PageSize) - continue; - - // Overflow: split at the middle and promote the middle entry, whose child becomes the left node's - // tail — the division InsertSeparator makes. Nodes are stored uncompressed. - int mid = current.Count / 2; - Entry middle = current[mid]; - List left = current.GetRange(0, mid); - List right = current.GetRange(mid + 1, current.Count - mid - 1); - - WriteOrThrow(page, Build(PageType.IntermediateIndexPage, 0, 0, middle.Trailer, level, left)); - promoted.Add((middle.Key, page)); - - page = AllocateIndexPage(index); - current = right; - keyBytes = right.Sum(e => (long)e.Key.Length); + // The root keeps its page and becomes the node over both halves; its image stays past the live end. + var top = new SpinePage(page.Number, previous: 0, page.Image) { Tail = right }; + top.Entries.Add(new Entry(promoted, left)); + top.KeyBytes = promoted.Length; + spine.Add(top); + return; } - WriteOrThrow(page, Build(PageType.IntermediateIndexPage, 0, 0, tailChild, level, current)); - return (promoted, page); + spine[level + 1].Tail = right; + Append(index, spine, level + 1, new Entry(promoted, left)); } + private static int Share(List entries) => + entries.Count <= 1 ? 0 : CommonPrefixLength(entries[0].Key, entries[^1].Key); + + /// Whether entries totalling of key data fit + /// one leaf at prefix — Build's layout, without building anything. + private bool LeafFits(int count, long keyBytes, int prefix) => + EntryDataOffset + keyBytes + (long)TrailerSize * count - (long)prefix * (count - 1) <= _channel.PageSize; + /// Orders two entries as their stored key ++ trailer bytes compare, without building either. internal static int CompareEntries(byte[] aKey, int aTrailer, byte[] bKey, int bTrailer) { diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index eaa0b4e8..091f80af 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -1840,11 +1840,10 @@ public bool AddColumn(string tableName, ColumnSpec spec, string? defaultValue = // Descriptor 0x07. A VARIABLE column carries its own variable index, which is the 0x2B high-water // (measured vs ACE in VariableColumnHighWaterAccessTests — NOT the count of live variable columns, // which is lower once one has been dropped), so leave it unset and let TdefBuilder use VariableIndex. - // A FIXED column carries the count of variable columns with a smaller id, the way the create path - // computes it; unset, the legacy fallback writes 0, the one value the spec says it must not be. - VariableTableIndex = spec.IsFixedLength - ? table.Columns.Count(c => !c.IsFixedLength && c.ColumnId < maxCols) - : -1, + // A FIXED column carries the same high-water: every variable column ever given a slot has a smaller + // id than the one being added, the dropped ones included (verified vs ACE: a LONG added after a + // TEXT was dropped takes 2 where two TEXT columns had been, one of them gone). + VariableTableIndex = spec.IsFixedLength ? varCount : -1, IsFixedLength = spec.IsFixedLength, IsAutoNumber = spec.IsAutoNumber, Precision = spec.Precision, @@ -1859,7 +1858,25 @@ public bool AddColumn(string tableName, ColumnSpec spec, string? defaultValue = CalculatedResultType = spec.CalculatedResultType, }; - AppendColumnToParts(parts, colCount, TdefBuilder.BuildColumnDescriptor(newColumn, format), spec.Name, format); + // 0x09 is DAO's Field.OrdinalPosition — reported by DAO, never read by the engine, which presents columns + // in descriptor order. DAO moves a descriptor when it sets the value, so in any file it wrote the + // descriptors are in 0x09 order, ties allowed, and gaps open when a column is dropped. ADD COLUMN + // compacts it, walking the descriptors in that order: each distinct value becomes + // its rank, so tied columns stay tied, and the new column takes the next rank. DROP COLUMN, ALTER COLUMN + // and CREATE INDEX leave it alone (all verified vs ACE: ordinals 0, 0, 3, 7 become 0, 0, 1, 2 and the + // added column 3; with no ties, as after SQL DDL alone, that is simply each column's position). + int rank = -1, previous = -1; + for (int i = 0; i < colCount; i++) + { + Span ordinal = parts.Columns.AsSpan(i * format.ColumnDescriptorSize + format.ColumnSecondaryNumberOffset, 2); + int value = BinaryPrimitives.ReadUInt16LittleEndian(ordinal); + if (i == 0 || value != previous) rank++; + previous = value; + BinaryPrimitives.WriteUInt16LittleEndian(ordinal, (ushort)rank); + } + byte[] descriptor = TdefBuilder.BuildColumnDescriptor(newColumn, format); + BinaryPrimitives.WriteUInt16LittleEndian(descriptor.AsSpan(format.ColumnSecondaryNumberOffset, 2), (ushort)(rank + 1)); + AppendColumnToParts(parts, colCount, descriptor, spec.Name, format); BinaryPrimitives.WriteUInt16LittleEndian(parts.Header.AsSpan(format.TdefColumnCountOffset, 2), (ushort)(colCount + 1)); BinaryPrimitives.WriteUInt16LittleEndian(parts.Header.AsSpan(format.TdefMaxColumnsOffset, 2), (ushort)(maxCols + 1)); @@ -2633,7 +2650,8 @@ private static int EditTargetDescriptor(TdefParts parts, ColumnDef target, Colum // The target's var-index (+0x07) becomes the old variable-column count — the next var slot — for BOTH a // fixed and a variable retype (verified vs ACE); a variable retype also bumps the 0x2B var-column count. BinaryPrimitives.WriteUInt16LittleEndian(d[format.ColumnVariableIndexOffset..], (ushort)varCount); - // The duplicate id at +0x09 is deliberately left unchanged — verified ACE does not update it. + // +0x09 is deliberately left unchanged: it is the column's ordinal position, which a retype does not + // move (verified: ACE does not update it). byte flags = d[format.ColumnFlagsOffset]; flags = newSpec.IsFixedLength ? (byte)(flags | JetFormatBase.ColumnFlagFixedLength) : (byte)(flags & ~JetFormatBase.ColumnFlagFixedLength); diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index d451276c..6d538dc5 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -114,8 +114,8 @@ Variable section (`varOffsetTable`+`numVar`) omitted only when the table has nev | `0x01` | 2 | Marker `0x0659` | | `0x03` | 2 | Unknown (zero) | | `0x05` | 2 | Column id | -| `0x07` | 2 | Variable-table index — on a **fixed** column the running count of preceding variable columns, **NOT `0`**; on a variable column its own slot index, which follows the `0x2B` high-water ([page-02b §3.4](page-02b-columns.md)) | -| `0x09` | 2 | Column number — a second copy of the id `0x05` on a **user** table, but **`0`** on the tables the engine writes for itself; unchanged by an `ALTER COLUMN` that burns a new id at `0x05` ([page-02b §3.4](page-02b-columns.md)) | +| `0x07` | 2 | Variable-table index — on a **fixed** column the running count of preceding variable columns, dropped ones included (so the `0x2B` high-water for an added column), **NOT `0`**; on a variable column its own slot index, which follows the `0x2B` high-water ([page-02b §3.4](page-02b-columns.md)) | +| `0x09` | 2 | Ordinal position (DAO `Field.OrdinalPosition`), ties allowed; the engine presents columns in descriptor order, which DAO keeps in step by moving the descriptor when it sets this. A second copy of the id `0x05` at creation on a **user** table, but **`0`** on the tables the engine writes for itself; unchanged by `DROP COLUMN` and by an `ALTER COLUMN` that burns a new id at `0x05`, ranked by `ADD COLUMN` ([page-02b §3.4](page-02b-columns.md)) | | `0x0B` | 1 | Precision (Decimal) — else collation LANGID low byte (`0x09` en-US) | | `0x0C` | 1 | Scale (Decimal) — else collation LANGID high byte (`0x04` en-US) | | `0x0D` | 1 | Collation sort id — the LCID's high word (`0x01` = an alternate sort order, e.g. Hungarian Technical) | @@ -224,8 +224,8 @@ Global maps, located by page 0: free pages at `0x18` (page 1 row 0 as ACE writes | `0x02` | 2 | Free space | | `0x04` | 4 | Owning TDEF page | | `0x08` | 4 | Jet4-inserted field (zero); shifts the following fields +4 vs Jet3 | -| `0x0C` | 4 | Previous leaf page (0 = leftmost) | -| `0x10` | 4 | Next leaf page (0 = rightmost) — load-bearing for Access's scan | +| `0x0C` | 4 | Previous leaf page (0 = leftmost); a split node's left sibling | +| `0x10` | 4 | Next leaf page (0 = rightmost) — load-bearing for Access's scan; a split node's right sibling | | `0x14` | 4 | Child-tail page (node: rightmost child) | | `0x18` | 2 | Compressed-byte count (shared key-prefix length) | | `0x1A` | 1 | B-tree level/height (leaf must be `0`) | diff --git a/src/LibRed/docs/format/page-00-database.md b/src/LibRed/docs/format/page-00-database.md index 2fca9d3f..ee82b2eb 100644 --- a/src/LibRed/docs/format/page-00-database.md +++ b/src/LibRed/docs/format/page-00-database.md @@ -17,7 +17,7 @@ | `0x18` | 4 | **Global free-pages map pointer** — `[row:1][page:3]`; `0x00000100` = page 1 row 0 in every file ACE writes ([page-05 §9.1](page-05-usage-maps.md)) | | `0x1C` | 4 | **Global released-pages map pointer** — `[row:1][page:3]`; `0x00000101` = page 1 row 1 ([page-05 §9.1](page-05-usage-maps.md)) | | `0x20`–`0x2C` | 4×4 | **System-catalog bootstrap pointers**: TDEF pages of `MSysObjects` / `MSysACEs` / `MSysQueries` / `MSysRelationships` = `2, 3, 4, 5`. `0x20` is the **catalog root** (how the engine finds `MSysObjects`). | -| `0x30`–`0x3B` | 12 | Zero in every file seen, but **not merely reserved**: ACE range-checks `0x30` and `0x34` (see below) | +| `0x30`–`0x3B` | 12 | Zero in every file seen, but **not merely reserved**: ACE range-checks `0x30` and `0x34` as `[row:1][page:3]` pointers, against the largest possible page only (see below) | | `0x3C` | 2 | **ANSI code page** — LE (`0x04E4` = 1252, `0x04E2` = 1250) | | `0x3E` | 4 | **Database (encryption) key** — 0 when there is no password | | `0x42` | 40 | **Password** (Jet 4; Jet 3 = 20 bytes) — additionally masked by a creation-date-derived value, so an empty password does not read as zeroes | @@ -108,11 +108,15 @@ map is a record on a data page; the catalog pointers that follow are plain page definition is a page. What the maps hold, and how ACE validates and follows the pointers, is [page-05 §9.1](page-05-usage-maps.md). -**`0x30` and `0x34` are range-checked, `0x38` is not (verified).** A value in either of the first two naming a -page past the end of the file makes the database unopenable — the same "page must exist" check `0x18` and -`0x1C` get — while `0x38` takes any value. So `0x30` and `0x34` are **pointers**, sharing the bootstrap block's -`[row:1][page:3]` shape at least as far as that check reaches; what they point at is **not known**, since every -file examined carries zero and zero is accepted. A writer must leave all three as it found them. +**`0x30` and `0x34` are range-checked, `0x38` is not (verified).** Read as `[row:1][page:3]`, either of the first +two may name any page up to the largest a file can have: `0x080000FF` (page 524,288, row 255) opens and +`0x08000100` (page 524,289) makes the database unopenable. The file's own length is **not** consulted — a page +ten past the end of a 353-page file, or page 500,000, opens as readily — which is looser than the "page must +exist" check `0x18` and `0x1C` get. `0x38` takes any value at all. The boundary sits where a `[row][page]` +reading puts it, and read as a plain page number it would be meaningless, so `0x30` and `0x34` are most likely +**record pointers** of the shape `0x18`/`0x1C` have, rather than page pointers like the catalog block after them. +What they point at is **not known**: every file examined carries zero, zero is accepted, and a compact zeroes a +poked value. A writer must leave all three as it found them. **Catalog bootstrap.** Reading the database is a two-step hop from page 0: the pointer at `0x20` gives the `MSysObjects` TDEF page (2), and `MSysObjects` then lists every other object (each table's row `Id` is *its* diff --git a/src/LibRed/docs/format/page-02b-columns.md b/src/LibRed/docs/format/page-02b-columns.md index 0e61e663..0cac8c0f 100644 --- a/src/LibRed/docs/format/page-02b-columns.md +++ b/src/LibRed/docs/format/page-02b-columns.md @@ -10,8 +10,8 @@ | `0x01` | 2 | Record marker `0x0659` (see §3.1 note); ignored | | `0x03` | 2 | Unknown (zero observed) | | `0x05` | 2 | Column id | -| `0x07` | 2 | Variable-table index. For a **fixed** column it is the running count of variable columns with a smaller id (**not** `0`) — ACE's own `ADD COLUMN` writes `2` for a LONG added to `(K LONG, A TEXT, B TEXT)`. For a **variable** column it is that column's own slot index, which is the `0x2B` **high-water** and *not* the count of live variable columns: after a variable column is dropped the next one goes above the abandoned slot, so the two part company. Verified byte-for-byte against DAO-written system tables and against ACE performing the same DDL. | -| `0x09` | 2 | Column number — a second copy of the column id `0x05` on a **user** table, but **zero** on the tables the engine writes for itself (see the note below). It **diverges after an `ALTER COLUMN` type change**, which burns a new id into `0x05` yet leaves `0x09` at the *old* id; see §3.8 | +| `0x07` | 2 | Variable-table index. For a **fixed** column it is the running count of variable columns with a smaller id (**not** `0`), dropped ones included — ACE's own `ADD COLUMN` writes `2` for a LONG added to `(K LONG, A TEXT, B TEXT)`, and still `2` when `B` was dropped first: an added column's count is the `0x2B` high-water. For a **variable** column it is that column's own slot index, which is the `0x2B` **high-water** and *not* the count of live variable columns: after a variable column is dropped the next one goes above the abandoned slot, so the two part company. Verified byte-for-byte against DAO-written system tables and against ACE performing the same DDL. | +| `0x09` | 2 | Ordinal position — DAO's `Field.OrdinalPosition`. The engine presents columns in descriptor order, not by this; DAO keeps the two in step by moving the descriptor when it sets it. At creation a second copy of the column id `0x05` on a **user** table, but **zero** on the tables the engine writes for itself (see the note below). It **diverges** after an `ALTER COLUMN` type change, which burns a new id into `0x05` yet leaves `0x09` alone (§3.8), after a `DROP COLUMN`, whose gap the next `ADD COLUMN` closes by ranking the values, and whenever DAO sets it | | `0x0B` | 1 | Numeric **precision** (Decimal/Numeric columns); on a **Complex** column the `MSysComplexColumns.ComplexID` (see below); otherwise the low byte of the collation's LANGID (the database default, e.g. `0x09` for en-US) | | `0x0C` | 1 | Numeric **scale** (Decimal/Numeric columns); `0` on a **Complex** column; otherwise the high byte of the LANGID (`0x04` for en-US) | | `0x0D` | 1 | Collation **sort id** — the LCID's high word; `0` except for an alternate sort order (see the note below) | @@ -82,9 +82,35 @@ > > ACE's SQL DDL, DAO's object model (`CreateTableDef`/`CreateField`/`Append`, the path Access's UI uses) > and DAO-executed SQL all write the id. **Compacting a database preserves the field exactly** — before and -> after are byte-identical — so it is fixed at creation and no later rewrite normalises it. A writer that -> generalises from the system tables and writes zero everywhere is wrong; LibRed writes the id except on a -> system column. +> after are byte-identical. A writer that generalises from the system tables and writes zero everywhere is +> wrong; LibRed writes the id except on a system column. +> +> **More exactly, `0x09` is DAO's `Field.OrdinalPosition`** — a presentation value that DAO reports, not the +> order the engine presents in. At creation it counts 0, 1, 2 … and so equals the id. Verified against ACE +> and DAO: +> +> - **The engine presents columns in descriptor order and ignores `0x09`.** With the two made to disagree by +> patching `0x09` alone, ACE's `SELECT *`, the OLE DB schema's `ORDINAL_POSITION` (1-based) and the order +> of DAO's `Fields` collection all follow the descriptors; only `Field.OrdinalPosition` reports `0x09`, +> verbatim. +> - **Setting `OrdinalPosition` through DAO writes the value and moves the descriptor**, which is what keeps +> the two in step in any file DAO or Access wrote. Ties and gaps are allowed: on `(A, B, C, D, E)`, setting +> `E` to 0 and then `B` to 7 leaves descriptors in the order `A E C D B` with `0x09` = `0 0 2 3 7`. The moved +> descriptor lands before the first one with a larger `0x09` (after the last when there is none) — all +> three measured moves fit that; with a tie it went after the column already there. Only the definition page +> changes — the value is not a property in the LvProp blob. +> - **`DROP COLUMN` leaves the others alone**, so a gap opens; so do `ALTER COLUMN` (a retype keeps `0x09` +> while `0x05` takes a new id, §3.8) and `CREATE INDEX`. +> - **`ADD COLUMN` compacts it**: every distinct value is replaced by its rank, so tied columns stay tied, +> and the new column takes the next rank — `0 0 3 7` after dropping `C` becomes `0 0 1 2`, the added +> column `3`. After SQL DDL alone there are no ties, and this is simply each column's position: +> `(A, B, C, D, E)`, drop `B`, add `F` → `0x05` ids `0 2 3 4 5`, `0x09` `0 1 2 3 4`. +> +> So **descriptor order is display order**, and a reader needs nothing else — LibRed reads column order from +> descriptor order and never reads `0x09`, and matches ACE even in a file where the two were made to disagree. +> A reader that assumed descriptor order is id order would be wrong after a reorder: `E` (id 4) precedes `B` +> (id 1). Row decoding is unaffected, since it goes by id (`0x05`), variable slot (`0x07`) and fixed offset, +> never by descriptor position. > **Date/Time Extended carries only the primary language id.** For a `DATETIME2` column ACE writes the > **low byte** of the database's LANGID at `0x0B`/`0x0C` — the primary language with the sublanguage half @@ -377,7 +403,7 @@ null bitmap is keyed by **id** and not by position (§5). | `0x00` | new data type | | `0x05` | **burned id** = the old `0x29` high-water (so the id ≥ every existing id; position is unchanged) | | `0x07` | variable-table index = the **old** variable-column count (the next free var slot) — set for **both** a fixed and a variable retype | -| `0x09` | **left unchanged** — ACE does *not* update the duplicate id here (it keeps the *old* id), a deliberate quirk | +| `0x09` | **left unchanged** — it is the column's ordinal position (§3.4), which a retype does not move; it only looked like the *old* id because the two coincide until a column is dropped or moved | | `0x0F` | fixed-length bit (`0x01`) set/cleared for the new type; auto-number bit likewise | | `0x0B`/`0x0C` | precision/scale for a `DECIMAL`/`NUMERIC` (`FixedPoint`) target | | `0x15` | fixed-data offset = **end of the current fixed region** (appended) for a fixed target, or `0` for a variable target. The old slot is left where it was as dead bytes. | diff --git a/src/LibRed/docs/format/page-03-04-index-btree.md b/src/LibRed/docs/format/page-03-04-index-btree.md index 56654714..be823b62 100644 --- a/src/LibRed/docs/format/page-03-04-index-btree.md +++ b/src/LibRed/docs/format/page-03-04-index-btree.md @@ -13,8 +13,8 @@ | `0x02` | 2 | Free space | | `0x04` | 4 | Owning table TDEF page | | `0x08` | 4 | **The 4-byte field Jet4 inserted** right after the owner — purpose unknown, **`0` observed** on every ACE- and LibRed-written index page. Inserting it here is what pushes prev/next/tail/compress down by 4 vs Jet3 (see the Jet3→Jet4 note under `0x1B`). | -| `0x0C` | 4 | **Previous leaf page** (`0` on the first/leftmost leaf), little-endian. **Verified against ACE:** on an ACE-built split index the higher-key leaf's `0x0C` points back at the lower-key leaf. | -| `0x10` | 4 | **Next leaf page** (`0` on the last/rightmost leaf), little-endian. **Verified against ACE — and load-bearing:** Access's full-table `COUNT(*)`/scan descends to the leftmost leaf and walks this forward chain. If it is wrong (e.g. `next` written at `0x0C`), Access stops after the first leaf and **silently sees only those rows** — a data-loss/corruption hazard, since it then treats the rest of the table's space as free. LibRed maintains `0x0C`/`0x10` across splits (§10.5). (This is **Jet3's `0x0C` next-pointer shifted +4**; the child-tail that mdbtools lists at `0x10` is the *Jet3* tail position — in Jet4 it too shifted to `0x14`.) | +| `0x0C` | 4 | **Previous leaf page** (`0` on the first/leftmost leaf), little-endian. **Verified against ACE:** on an ACE-built split index the higher-key leaf's `0x0C` points back at the lower-key leaf. A node that has split carries the same link to its left sibling at the same level (§10.5); the root never has one. | +| `0x10` | 4 | **Next leaf page** (`0` on the last/rightmost leaf), little-endian; on a split node, its right sibling. **Verified against ACE — and load-bearing:** Access's full-table `COUNT(*)`/scan descends to the leftmost leaf and walks this forward chain. If it is wrong (e.g. `next` written at `0x0C`), Access stops after the first leaf and **silently sees only those rows** — a data-loss/corruption hazard, since it then treats the rest of the table's space as free. LibRed maintains `0x0C`/`0x10` across splits (§10.5). (This is **Jet3's `0x0C` next-pointer shifted +4**; the child-tail that mdbtools lists at `0x10` is the *Jet3* tail position — in Jet4 it too shifted to `0x14`.) | | `0x14` | 4 | **Child-tail** page (node pages: the rightmost child, referenced by no entry). **Verified** for Jet4/ACE: the tail pointer read here drives correct multi-level traversal. This is **Jet3's `0x10` tail shifted +4** by the `0x08` insertion, which is exactly why mdbtools (Jet3) documents the tail at `0x10`. | | `0x18` | 2 | Compressed-byte count (shared key prefix length, §10.3). Jet3's `0x14`, shifted +4. | | `0x1A` | 1 | The **1-byte field Jet4 inserted** just before the bitmask. ACE writes `0` on leaves and `1` on the root of a two-level split index, consistent with a **B-tree level/height** — but **only `0` and `1` have been observed** (no 3-level tree was built against ACE, so `2`+ is a guess). **Required only for leaves (verified):** writing `0x01` on a *leaf* makes ACE fail to open the whole database (`"could not find the object 'Databases'"`). **Node value is cosmetic (verified):** with correct leaf-chain offsets, nodes written with `0x1A=0` *and* prefix-compressed still give ACE the right `COUNT`/`SUM`, so Access reads a node's tail child regardless; leaf vs node is told by the page-type byte at `0x00`. LibRed still writes the height to match ACE byte-for-byte, but the only hard requirements are the leaf-chain offsets and a *leaf's* `0x1A=0`. | @@ -94,11 +94,13 @@ omits. Reconstruct: `fullEntry = prefix ++ stored`. > **Compression is optional on leaves.** A `compressedByteCount` of 0 (every entry stored in full) > is a valid *leaf* that Access reads without complaint — verified by rewriting a leaf uncompressed -> and re-seeking it. **On node (`0x03`) pages, ACE writes them uncompressed (`0x18 = 0`)**, and -> LibRed matches that. *Verified cosmetic:* compressing a node does **not** break Access — with correct -> leaf-chain offsets, nodes compressed and `0x1A=0` still give the right `COUNT`/`SUM`. A broken tail descent -> points at the leaf-chain offsets (§10.1), not at node compression. LibRed writes nodes uncompressed only to -> stay byte-faithful with ACE, not because it's required. +> and re-seeking it. **Node (`0x03`) pages follow the same cycle as leaves** (below): a node is written +> uncompressed until it fills, compressed in place when it does, and split when that is not enough, both halves +> then written at the largest prefix they share. So the node of a small two-level tree reads `0x18 = 0` — it has +> never filled — while the halves of a split node carry a prefix (verified: 16 separators at prefix 0 with 176 +> bytes free, then a 17th splits it into two nodes of 8 at prefix 1). Compression is not load-bearing on a node +> either: with correct leaf-chain offsets, nodes compressed and `0x1A=0` still give Access the right +> `COUNT`/`SUM`, and a broken tail descent points at the leaf-chain offsets (§10.1), not at node compression. > > **A leaf with ≤ 1 entry writes `0x18 = 0`.** Prefix compression describes a prefix *shared across > entries*, so with zero or one entry there is nothing to share — ACE writes `compressedByteCount = 0`, @@ -135,7 +137,8 @@ omits. Reconstruct: `fullEntry = prefix ++ stored`. > > **Building an index follows the same rule** (verified): `CREATE INDEX` over a table's rows writes each leaf > at the prefix its filling reached, so an index whose entries all fit one page uncompressed is written -> uncompressed — eleven text keys sharing ten bytes land at prefix `0` — however much they share. +> uncompressed — eleven text keys sharing ten bytes land at prefix `0` — however much they share. The whole +> tree it writes is the one sequential inserts leave (§10.5). ### 10.4 Key encoding (order-preserving) @@ -990,24 +993,50 @@ scan Access uses. The split mechanics: -- **Leaf split:** partition the sorted entries in half; the lower half stays on the original page, - the upper half goes to a newly allocated page. **The half is taken over the entries the page held - *before* the insert, and the new entry then joins whichever side its key falls in** — so the cut sits at - `mid = preInsertCount / 2`, and the original page keeps `mid + 1` entries for a key landing below `mid` - but `mid` for one landing on or above it. (Halving the post-insert list instead always hands the odd entry - to the right page, which is the same cut only for a key in the upper half.) Measured against ACE on a - 602-entry leaf split by one further key: ACE keeps **302** entries for a new key at position 1 or 50 and - **301** at position 301, 302 or 400 — so the midpoint itself goes right. The doubly-linked leaf chain is maintained — the +- **Compress, then split.** A page the new entry does not fit is first rewritten **in place** with its old + entries at the largest prefix they share (§10.3), and only when that still leaves no room is it split — over + that compressed image. Nothing reads the image once the split is written, but it is what stands past each + half's live end (see *Dead bytes* below). Verified on leaves and nodes. +- **Leaf split:** partition the sorted entries in two; the lower part stays on the original page, + the upper part goes to a newly allocated page. **The cut is taken over the entries the page held *before* + the insert — every old entry that *starts* before the byte midpoint of the compressed old page stays left, + so the entry straddling the midpoint does too — and the new entry then joins whichever side its key falls + in**: left when its position is below the number of old entries kept, right otherwise. With equal-length + entries that is the old entries halved, the odd one left. Measured against ACE: + + | leaf | new key at | ACE keeps left | + | --- | --- | --- | + | 17 equal entries (root or not) | position 1–8 | 10 | + | 17 equal entries | position 9–16 | 9 | + | 602 equal entries | position 1 or 50 | 302 | + | 602 equal entries | position 301, 302 or 400 | 301 | + | 91 entries of 38–40 bytes | position 52 | **45** — a count would keep 46 | + + **A new *first* entry is the exception**: then the entries *including* it are halved by count, rounding up + — 9 of 18 (keys of 207 and 211 bytes alike) and 302 of 603. Whether the parent node splits in the same + insert makes no difference. The doubly-linked leaf chain is maintained — the new right page's *prev* (`0x0C`) points at the left, its *next* (`0x10`) inherits the left's old next, the left's *next* becomes the right, and the old next leaf's *prev* is repointed to the right. **Getting these offsets right is essential** — Access's scan walks the `0x10` next-chain from the leftmost leaf, so a mis-placed pointer makes it lose every row past the first leaf. The promoted separator is the **left half's maximum full key**, which stays in the leaf (a copy is - promoted, B+tree-style). Leaves are prefix-compressed; LibRed also writes node pages uncompressed - with `0x1A` set to their height above the leaves to match ACE byte-for-byte, though (unlike the - leaf-chain offsets) neither is *verified* to be required — see §10.1 `0x1A` and §10.3. + promoted, B+tree-style). **The left half keeps the prefix the page was stored at** (after the + compress step) and the right half is written at the largest prefix its entries share — except that when + the new entry became the left half's first, the left half is written **whole, at prefix 0**, though its + entries still share bytes. This is specific to a split: a new first entry that fits keeps the page's prefix + (verified: a 410-entry leaf at prefix 3 takes a key below all of them and stays at 3). - **Node split:** partition on a **middle entry** whose key is *promoted* (removed from the node); - its child becomes the left node's child-tail, and the old tail stays the right node's tail. + its child becomes the left node's child-tail, and the old tail stays the right node's tail. A node + fills, compresses in place and splits exactly as a leaf does (§10.3), and both halves are written at the + largest prefix they share. Nodes carry **sibling links** too: the halves point at each other through + `0x0C`/`0x10` and the old right neighbour's `0x0C` is repointed, as on a leaf (verified on a node split + under a root). Before promoting it, the child that split is repointed to its new right half, and that + repointed node is what the compress step writes. **A node has a right-edge split too:** when the new + separator is the node's last entry — the child that split was its tail, as on every split of an ascending + load — the left node keeps every old entry but the last, that one is promoted (its child becoming the left + node's tail), and the new separator starts the right node alone. Verified: a node of 16 separators taking a + 17th is cut 15, promote 1, 1. A node carries `0x1A` set to its height above the leaves, + as ACE writes it, though (unlike the leaf-chain offsets) that is not *verified* to be required — see §10.1. - **Right-edge split.** When the incoming key is the highest on the page, both engines leave that page full and start a new one holding the new entry alone, instead of halving it: nothing sorts below a maximum key, so a middle split there strands half a page for ever. A writer that always splits down the middle spends @@ -1029,21 +1058,31 @@ The split mechanics: ascending backfill keeps meeting the right edge of a subtree. A *random* backfill into pre-packed pages is unmeasured. -> **Open — the compressed length of a page whose FIRST entry is new.** A key that sorts below everything on -> the page splits it at the same point as any other low key (verified: both engines keep 302 of 603), but ACE -> then writes the left page with `compressedByteCount = 0` where the page had been stored at `3` and the -> entries still share 3 bytes. Every other measured position keeps the `3`. Whether the trigger is "the new -> entry became the page's first" or "a split writes its left page uncompressed unless the first entry is -> unchanged" is **not** established — the two fit the measurements equally. Until it is, LibRed recomputes -> the prefix and keeps compressing, which costs a byte difference only in this one case. The right page is -> unaffected (its live bytes match exactly). - **Propagation:** the promoted separator `[key → left page]` is inserted into the parent, whose pointer to the just-split page is repointed to the new right page; if the parent overflows it splits in turn, up to the root. -- **Root growth:** when the root itself splits, a new root node is allocated holding one entry - `[promoted → old root]` with the new page as its child-tail, and the index-data block's root - pointer (§3.5 `0x26`) is repointed to it. (The single-leaf → two-leaves case hits this on the - first overflow, changing the root page's type from leaf `0x04` to node `0x03`.) +- **Root growth — the root never moves.** When the root itself splits, **both** halves go to newly allocated + pages, the left one allocated first, and the root page is rewritten in place as a node holding one entry + `[promoted → left]` with the right page as its child-tail. The index-data block's root pointer (§3.5 + `0x26`) is therefore unchanged by any split — a leaf root turns node on its first overflow, keeping its page + number. Verified for a leaf root and for a node root. +- **Dead bytes.** A split leaves bytes past each half's live end, and ACE's are reproducible: + - a page rewritten in place (the left half of an ordinary split, a root turned node) keeps what the + compressed image held there; + - the left half of a **root** split is a new page, but it too holds the root's compressed image past its + live end, as if the root had been copied there and cut; + - the right half is a new page's zeros — except when the new entry became the left half's first, when it + too holds the split page's image; + - a **node's** left half additionally holds the promoted middle entry just past its live end, stored at the + page's prefix: ACE writes it into the page and then ends the page before it. + + Each verified byte for byte on a leaf root split (new key high and new key first), a node root split, and a + split of a non-root leaf by a new first key. +- **`CREATE INDEX` over existing rows** writes the tree that inserting the keys one at a time in key order + would: every insert lands on the right edge, so the root keeps the page the index was created with, each + leaf fills uncompressed, is compressed in place when full and splits at the right edge, the nodes above do + the same, and pages are allocated in the order those splits happen, leaves and nodes interleaved. Verified + byte for byte, dead bytes included, on a tree of one node over three leaves and on one of two node levels. > Newly allocated split pages are taken from the global free-page map (§ page 1) and are registered in the > *index's own* owned-pages usage map as Access does — `IndexWriter.AllocateIndexPage` sets the bit for diff --git a/test/LibRed.Benchmarks/History.tsv b/test/LibRed.Benchmarks/History.tsv index e9dc9f00..c1fb97e5 100644 --- a/test/LibRed.Benchmarks/History.tsv +++ b/test/LibRed.Benchmarks/History.tsv @@ -17,3 +17,4 @@ utc commit dirty host suite benchmark params mean_us stddev_us alloc_kb 2026-09-08 16:54 4b8ba080 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10_indexed 44.66 0.19 22.81 2026-09-08 16:54 4b8ba080 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sub.scalar_correlated 1413.78 18.64 3.39 2026-09-08 16:54 4b8ba080 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sub.scalar_correlated 185.70 1.45 80.85 +2026-09-26 15:45 a08c9eb3 dirty DESKTOP-D1RBTPV DdlBenchmarks CreateIndexOverExistingRows - 3155.75 455.25 2792.75 diff --git a/test/LibRed.Engine.AccessTests/ColumnNumberingAccessTests.cs b/test/LibRed.Engine.AccessTests/ColumnNumberingAccessTests.cs new file mode 100644 index 00000000..3726da29 --- /dev/null +++ b/test/LibRed.Engine.AccessTests/ColumnNumberingAccessTests.cs @@ -0,0 +1,153 @@ +using System.Buffers.Binary; +using System.Data.OleDb; +using System.Globalization; +using System.Reflection; +using LibRed; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// A column descriptor's id (0x05), variable-table index (0x07) and column number (0x09) come out as ACE +/// writes them through DROP COLUMN, ADD COLUMN, a retype and CREATE INDEX. The interesting case is ADD COLUMN +/// after a drop: it renumbers every column's 0x09 to its position, and an added fixed column's 0x07 counts the +/// dropped variable columns too. +/// +[Collection(AceCollection.Name)] +public class ColumnNumberingAccessTests +{ + private const string Create = "CREATE TABLE T (A LONG, B LONG, C TEXT(10), D LONG, E TEXT(10))"; + + public static TheoryData Sequences => new() + { + new[] { Create, "ALTER TABLE T DROP COLUMN B", "ALTER TABLE T ADD COLUMN F LONG" }, + new[] { Create, "ALTER TABLE T DROP COLUMN B", "ALTER TABLE T ALTER COLUMN D TEXT(5)" }, + new[] { Create, "ALTER TABLE T DROP COLUMN B", "CREATE INDEX ix ON T (D)" }, + new[] { Create, "ALTER TABLE T DROP COLUMN B", "ALTER TABLE T ADD COLUMN F TEXT(10)", "ALTER TABLE T DROP COLUMN C", "ALTER TABLE T ADD COLUMN G LONG" }, + new[] { Create, "ALTER TABLE T ALTER COLUMN B TEXT(5)", "ALTER TABLE T ADD COLUMN F LONG" }, + new[] { Create, "ALTER TABLE T DROP COLUMN E", "ALTER TABLE T ADD COLUMN F LONG" }, + new[] { Create, "ALTER TABLE T DROP COLUMN B", "ALTER TABLE T DROP COLUMN D" }, + new[] { Create, "ALTER TABLE T DROP COLUMN A", "ALTER TABLE T ADD COLUMN F LONG" }, + new[] { Create, "ALTER TABLE T DROP COLUMN B", "ALTER TABLE T DROP COLUMN D", "ALTER TABLE T ADD COLUMN F TEXT(10)" }, + new[] { Create, "ALTER TABLE T DROP COLUMN C", "ALTER TABLE T DROP COLUMN E", "ALTER TABLE T ADD COLUMN F LONG", "ALTER TABLE T ADD COLUMN G TEXT(10)" }, + }; + + [Theory] + [MemberData(nameof(Sequences))] + public void Descriptor_numbers_follow_ace(string[] statements) + { + string ace = TemporaryDatabase.CopyPath(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "colnum-ace-"); + string libred = TemporaryDatabase.CopyPath(ace, "colnum-libred-"); + RunBoth(ace, libred, statements); + } + + // 0x09 is DAO's Field.OrdinalPosition, which DAO sets freely — ties and gaps included — and keeps the + // descriptors sorted by. Moving E first and B last gives ordinals A 0, E 0, B 1, C 2, D 3 and then B 7, in + // descriptor order A E C D B. An ADD COLUMN after that ranks them, keeping the tie. + [Fact] + public void Descriptor_numbers_follow_ace_after_dao_reorders_the_columns() + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + string ace = TemporaryDatabase.CopyPath(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "colnum-ace-"); + using (OleDbConnection conn = AceTestDatabase.Open(ace)) + using (OleDbCommand cmd = conn.CreateCommand()) + { + cmd.CommandText = Create; + cmd.ExecuteNonQuery(); + cmd.CommandText = "INSERT INTO T VALUES (1, 2, 'c1', 4, 'e1')"; + cmd.ExecuteNonQuery(); + } + object db = Invoke(engine!, "OpenDatabase", ace)!; + try + { + object fields = Get(Item(Get(db, "TableDefs")!, "T"), "Fields")!; + Set(Item(fields, "E"), "OrdinalPosition", 0); + Set(Item(fields, "B"), "OrdinalPosition", 7); + } + finally { Invoke(db, "Close"); } + AceTestDatabase.ReleaseAbandonedComObjects(); + + string libred = TemporaryDatabase.CopyPath(ace, "colnum-libred-"); + Assert.Equal("A:0/0/0 E:4/1/0 C:2/0/2 D:3/1/3 B:1/0/7", Describe(libred)); + RunBoth(ace, libred, ["ALTER TABLE T DROP COLUMN C", "ALTER TABLE T ADD COLUMN F LONG", + "INSERT INTO T (A, B, D, E, F) VALUES (2, 3, 5, 'e2', 6)", + "CREATE INDEX ixE ON T (E)", "ALTER TABLE T ALTER COLUMN D TEXT(5)", + "INSERT INTO T (A, B, D, E, F) VALUES (3, 4, 'd3', 'e3', 7)"]); + } + + private static void RunBoth(string ace, string libred, string[] statements) + { + try + { + foreach (string sql in statements) + { + using (OleDbConnection conn = AceTestDatabase.Open(ace)) + using (OleDbCommand cmd = conn.CreateCommand()) + { + cmd.CommandText = sql; + cmd.ExecuteNonQuery(); + } + using (var db = JetDatabase.Open(libred, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery(sql); + + Assert.Equal(Describe(ace), Describe(libred)); + } + + // And each engine reads the other's rows alike, whatever order the descriptors are in. + Assert.Equal(ReadThroughAce(ace), ReadThroughAce(libred)); + Assert.Equal(ReadThroughAce(ace), ReadThroughLibRed(ace)); + } + finally + { + TemporaryDatabase.Delete(ace); + TemporaryDatabase.Delete(libred); + } + } + + // Unordered, since a sequence may drop any column; each reader sorts its rows after the header instead. + private const string ReadBack = "SELECT * FROM T"; + + private static string ReadThroughAce(string path) + { + using OleDbConnection conn = AceTestDatabase.Open(path); + using OleDbCommand cmd = conn.CreateCommand(); + cmd.CommandText = ReadBack; + using OleDbDataReader r = cmd.ExecuteReader(); + var rows = new List(); + while (r.Read()) + rows.Add(string.Join(" ", Enumerable.Range(0, r.FieldCount).Select(i => r.IsDBNull(i) ? "null" : Convert.ToString(r.GetValue(i), CultureInfo.InvariantCulture)))); + return string.Join(" ", Enumerable.Range(0, r.FieldCount).Select(r.GetName)) + " | " + string.Join(" | ", rows.Order(StringComparer.Ordinal)); + } + + private static string ReadThroughLibRed(string path) + { + using var db = JetDatabase.Open(path); + var result = new QueryEngine(db).ExecuteQuery(ReadBack); + var rows = new List(); + foreach (object?[] row in result.Rows) + rows.Add(string.Join(" ", row.Select(v => v is null ? "null" : Convert.ToString(v, CultureInfo.InvariantCulture)))); + return string.Join(" ", result.Columns.Select(c => c.Name)) + " | " + string.Join(" | ", rows.Order(StringComparer.Ordinal)); + } + + private static object Item(object collection, object key) => + collection.GetType().InvokeMember("Item", BindingFlags.GetProperty, null, collection, [key])!; + private static object? Get(object target, string name) => + target.GetType().InvokeMember(name, BindingFlags.GetProperty, null, target, null); + private static void Set(object target, string name, object value) => + target.GetType().InvokeMember(name, BindingFlags.SetProperty, null, target, [value]); + private static object? Invoke(object target, string name, params object?[] args) => + target.GetType().InvokeMember(name, BindingFlags.InvokeMethod, null, target, args); + + // name:0x05/0x07/0x09 for each descriptor, in descriptor order. + private static string Describe(string path) + { + using var db = JetDatabase.Open(path); + return string.Join(" ", db.Catalog.FindTable("T")!.Columns.Select(c => + { + byte[] d = c.RawDescriptor!; + return $"{c.Name}:{BinaryPrimitives.ReadUInt16LittleEndian(d.AsSpan(5))}/" + + $"{BinaryPrimitives.ReadUInt16LittleEndian(d.AsSpan(7))}/{BinaryPrimitives.ReadUInt16LittleEndian(d.AsSpan(9))}"; + })); + } +} diff --git a/test/LibRed.Engine.AccessTests/IndexRootSplitByteParityTests.cs b/test/LibRed.Engine.AccessTests/IndexRootSplitByteParityTests.cs new file mode 100644 index 00000000..bcbe8d12 --- /dev/null +++ b/test/LibRed.Engine.AccessTests/IndexRootSplitByteParityTests.cs @@ -0,0 +1,252 @@ +using System.Buffers.Binary; +using System.Data.OleDb; +using LibRed; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// Index splits that ACE and LibRed must write byte for byte alike: a root that splits (it keeps its page and +/// both halves move out), the cut at every position a new key can land, and the cut on a leaf whose entries +/// differ in length, where it is decided by bytes rather than by count. +/// +/// +/// Each case seeds a table through ACE, then applies one further insert to one copy through ACE and to another +/// through LibRed, and compares every page the table's indexes own — the dead bytes past each page's live end +/// included, since ACE's are reproducible. +/// +[Collection(AceCollection.Name)] +public class IndexRootSplitByteParityTests +{ + private const int PageSize = 4096; + + // 204-character keys, so a leaf holds 17 and splits on the 18th. + private static string Wide(int i) => $"K{i:D4}" + new string('x', 200); + + [Theory] + [InlineData(95, "a key in the upper half")] + [InlineData(5, "a key that sorts first")] + public void A_splitting_root_leaf_keeps_its_page(int key, string _) + { + string seeded = Seed("CREATE TABLE RootSplit (k TEXT(255) CONSTRAINT pk PRIMARY KEY)", + Enumerable.Range(1, 17).Select(i => $"INSERT INTO RootSplit (k) VALUES ('{Wide(i * 10)}')")); + try { AssertInsertMatchesAce(seeded, "RootSplit", $"INSERT INTO RootSplit (k) VALUES ('{Wide(key)}')"); } + finally { TemporaryDatabase.Delete(seeded); } + } + + [Fact] + public void A_splitting_root_node_keeps_its_page() + { + // Keys spread so every leaf keeps taking new ones: the root node fills at 16 separators and the 17th + // splits it. Found by inserting until the root's own children became nodes. + static string Spread(int i) => $"{(char)('A' + i * 7 % 26)}{i:D3}" + new string((char)('a' + i % 26), 200); + string seeded = Seed("CREATE TABLE NodeSplit (k TEXT(255) CONSTRAINT pk PRIMARY KEY)", + Enumerable.Range(1, 226).Select(i => $"INSERT INTO NodeSplit (k) VALUES ('{Spread(i)}')")); + try + { + byte[] before = File.ReadAllBytes(seeded); + Assert.True(IndexPages(before, Tdef(seeded, "NodeSplit")).Count(p => before[p * PageSize] == 0x03) == 1, + "The seed was meant to leave a single root node."); + AssertInsertMatchesAce(seeded, "NodeSplit", $"INSERT INTO NodeSplit (k) VALUES ('{Spread(227)}')"); + } + finally { TemporaryDatabase.Delete(seeded); } + } + + [Fact] + public void A_leaf_is_cut_where_ace_cuts_it_at_every_position() + { + // A non-root leaf of 17 equal entries whose parent has room: 180…340 on the right of a first split. + string seeded = Seed("CREATE TABLE SplitPos (k TEXT(255) CONSTRAINT pk PRIMARY KEY)", + Enumerable.Range(1, 34).Select(i => $"INSERT INTO SplitPos (k) VALUES ('{Wide(i * 10)}')")); + try + { + for (int position = 0; position <= 17; position++) + AssertInsertMatchesAce(seeded, "SplitPos", $"INSERT INTO SplitPos (k) VALUES ('{Wide(175 + position * 10)}')"); + } + finally { TemporaryDatabase.Delete(seeded); } + } + + [Fact] + public void A_leaf_of_unequal_entries_is_cut_by_bytes() + { + // Labels 0…144 in id order land all over the text index; entries are 38 to 40 bytes stored, and the + // 146th splits a 91-entry leaf 45 and 47 where halving the count would keep 46. + static string Label(int i) => $"Bulk label number {i} " + new string('L', 30); + string seeded = Seed("CREATE TABLE Labels (Id LONG CONSTRAINT pkLabels PRIMARY KEY, Label TEXT(60))", + new[] { "CREATE INDEX ixLabel ON Labels (Label)" } + .Concat(Enumerable.Range(0, 145).Select(i => $"INSERT INTO Labels (Id, Label) VALUES ({i}, '{Label(i)}')"))); + try { AssertInsertMatchesAce(seeded, "Labels", $"INSERT INTO Labels (Id, Label) VALUES (145, '{Label(145)}')"); } + finally { TemporaryDatabase.Delete(seeded); } + } + + [Fact] + public void An_ascending_load_splits_nodes_at_the_right_edge_as_ace_does() + { + // 400 ascending keys fill 24 leaves of 17, so the root node overflows while each new separator is its + // last entry. The whole load goes through each engine from the same empty table. A load this size also + // takes ACE past the point where it places pages differently (in aligned groups), so the trees are + // compared by shape — each level left to right — rather than by page number. + string empty = Seed("CREATE TABLE Ascending (k TEXT(255) CONSTRAINT pk PRIMARY KEY)", []); + string ace = TemporaryDatabase.CopyPath(empty, "ascending-ace-"); + string libred = TemporaryDatabase.CopyPath(empty, "ascending-libred-"); + try + { + string[] inserts = [.. Enumerable.Range(1, 400).Select(i => $"INSERT INTO Ascending (k) VALUES ('{Wide(i)}')")]; + using (OleDbConnection conn = AceTestDatabase.Open(ace)) + foreach (string sql in inserts) + { + using OleDbCommand cmd = conn.CreateCommand(); + cmd.CommandText = sql; + cmd.ExecuteNonQuery(); + } + using (var db = JetDatabase.Open(libred, readOnly: false)) + { + var engine = new QueryEngine(db); + foreach (string sql in inserts) engine.ExecuteNonQuery(sql); + } + + int tdef = Tdef(empty, "Ascending"); + byte[] a = File.ReadAllBytes(ace), l = File.ReadAllBytes(libred); + Assert.True(IndexPages(a, tdef).Count(p => a[p * PageSize] == 0x03) >= 3, "The load was meant to split a node."); + Assert.Equal(Shape(a, tdef), Shape(l, tdef)); + } + finally + { + foreach (string path in new[] { empty, ace, libred }) TemporaryDatabase.Delete(path); + } + } + + // CREATE INDEX over existing rows writes the tree that inserting the keys one at a time in order would: the + // root keeps its page, full leaves are compressed before they split, nodes split at the right edge, and + // pages come from the allocator in the order those splits happen. 40 rows give one node over three leaves; + // 400 give two levels of nodes, the lower one split. + [Theory] + [InlineData(40)] + [InlineData(400)] + public void Create_index_writes_what_ace_writes(int rows) + { + string seeded = Seed("CREATE TABLE Indexed (Id LONG, K TEXT(255))", Enumerable.Range(1, rows).Select(i => + $"INSERT INTO Indexed VALUES ({i}, '{(char)('A' + i * 7 % 26)}{i:D4}{new string((char)('a' + i % 26), 200)}')")); + try + { + int tdef = Tdef(seeded, "Indexed"); + string ace = TemporaryDatabase.CopyPath(seeded, "createindex-ace-"); + string libred = TemporaryDatabase.CopyPath(seeded, "createindex-libred-"); + try + { + const string ddl = "CREATE INDEX ixK ON Indexed (K)"; + using (OleDbConnection conn = AceTestDatabase.Open(ace)) + using (OleDbCommand cmd = conn.CreateCommand()) + { + cmd.CommandText = ddl; + cmd.ExecuteNonQuery(); + } + using (var db = JetDatabase.Open(libred, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery(ddl); + + byte[] a = File.ReadAllBytes(ace), l = File.ReadAllBytes(libred); + Assert.True(a.Length == l.Length, $"ACE's file has {a.Length / PageSize} pages, LibRed's {l.Length / PageSize}."); + Assert.True(IndexPages(a, tdef).Any(p => a[p * PageSize] == 0x03), "The index was meant to have a node."); + // The index's pages, and the definition that points at its root. + foreach (int page in IndexPages(a, tdef).Union(IndexPages(l, tdef)).Append(tdef)) + { + int differ = Enumerable.Range(0, PageSize).FirstOrDefault(b => a[page * PageSize + b] != l[page * PageSize + b], -1); + Assert.True(differ < 0, $"{rows} rows: page {page} differs from ACE's at 0x{differ:X3}."); + } + } + finally + { + TemporaryDatabase.Delete(ace); + TemporaryDatabase.Delete(libred); + } + } + finally { TemporaryDatabase.Delete(seeded); } + } + + private static string Seed(string create, IEnumerable statements) + { + string seeded = TemporaryDatabase.CopyPath( + Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "rootsplit-seed-"); + using OleDbConnection conn = AceTestDatabase.Open(seeded); + foreach (string sql in statements.Prepend(create)) + { + using OleDbCommand cmd = conn.CreateCommand(); + cmd.CommandText = sql; + cmd.ExecuteNonQuery(); + } + return seeded; + } + + private static int Tdef(string path, string table) + { + using var db = JetDatabase.Open(path); + return db.Catalog.FindTable(table)!.DefinitionPage; + } + + private static void AssertInsertMatchesAce(string seeded, string table, string insert) + { + int tdef = Tdef(seeded, table); + string ace = TemporaryDatabase.CopyPath(seeded, "rootsplit-ace-"); + string libred = TemporaryDatabase.CopyPath(seeded, "rootsplit-libred-"); + try + { + using (OleDbConnection conn = AceTestDatabase.Open(ace)) + using (OleDbCommand cmd = conn.CreateCommand()) + { + cmd.CommandText = insert; + cmd.ExecuteNonQuery(); + } + using (var db = JetDatabase.Open(libred, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery(insert); + + byte[] a = File.ReadAllBytes(ace), l = File.ReadAllBytes(libred); + Assert.True(IndexPages(a, tdef).Count > IndexPages(File.ReadAllBytes(seeded), tdef).Count, + $"{insert[..Math.Min(60, insert.Length)]}…: the insert was meant to split a page."); + Assert.True(a.Length == l.Length, $"{insert}: ACE's file has {a.Length / PageSize} pages, LibRed's {l.Length / PageSize}."); + foreach (int page in IndexPages(a, tdef).Union(IndexPages(l, tdef))) + { + int differ = Enumerable.Range(0, PageSize).FirstOrDefault( + b => a[page * PageSize + b] != l[page * PageSize + b], -1); + Assert.True(differ < 0, $"{insert[..Math.Min(60, insert.Length)]}…: page {page} differs from ACE's at 0x{differ:X3}."); + } + } + finally + { + TemporaryDatabase.Delete(ace); + TemporaryDatabase.Delete(libred); + } + } + + /// Each level of the tree, top down and left to right along the sibling links, as + /// entries/prefix/free per page — everything about the tree but where its pages are. + private static string Shape(byte[] file, int tdef) + { + List pages = IndexPages(file, tdef); + int At(int page, int offset) => BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(page * PageSize + offset, 4)); + var levels = new List(); + foreach (IGrouping level in pages.GroupBy(p => file[p * PageSize + 0x1A]).OrderByDescending(g => g.Key)) + { + var byPage = level.ToHashSet(); + var shapes = new List(); + for (int p = level.Single(q => At(q, 0x0C) == 0); p != 0 && byPage.Contains(p); p = At(p, 0x10)) + { + int entries = 0; + for (int i = 0x1B; i < 0x1E0; i++) entries += System.Numerics.BitOperations.PopCount(file[p * PageSize + i]); + shapes.Add($"{entries}/{BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(p * PageSize + 0x18))}/" + + $"{BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(p * PageSize + 2))}"); + } + levels.Add($"level {level.Key}: {string.Join(" ", shapes)}"); + } + return string.Join("\n", levels); + } + + private static List IndexPages(byte[] file, int tdef) + { + var pages = new List(); + for (int p = 0; p < file.Length / PageSize; p++) + if (file[p * PageSize] is 0x03 or 0x04 + && BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(p * PageSize + 4, 4)) == tdef) + pages.Add(p); + return pages; + } +} diff --git a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs index 7463487c..430f5a78 100644 --- a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs @@ -6,12 +6,13 @@ using LibRed; using LibRed.Catalog; using LibRed.Pages; +using LibRed.Storage; using Xunit; namespace LibRed.Engine.Tests; /// -/// A hundred statements run through ACE over OLE DB and through LibRed on copies of one ACE-created empty database, +/// About 140 statements run through ACE over OLE DB and through LibRed on copies of one ACE-created empty database, /// and the files compared byte for byte. It reports how far apart they are rather than asserting they match. /// /// @@ -21,7 +22,8 @@ namespace LibRed.Engine.Tests; /// and one early divergence does not leave every later page misaligned. /// Only what differs for reasons other than the format is masked: page 0's user commit-byte table, which /// counts a lock-file user's writes and LibRed does not keep; the wall-clock DateCreate and DateUpdate of each -/// MSysObjects row; and a data page's write stamp at 0x08. A copy opened and closed through ACE with nothing +/// MSysObjects row; and a data page's write stamp at 0x08, with the copy of it a chained long value's descriptor +/// carries. A copy opened and closed through ACE with nothing /// done to it shows what ACE writes merely for holding a session; a byte that alone changes is counted apart rather /// than as a difference. Calculated and complex columns are left out — ACE's OLE DB DDL cannot create either. /// @@ -108,7 +110,10 @@ public void Each_statement_leaves_libred_and_ace_with_the_same_file() /// A hundred statements over six tables: creates with every ordinary column type and each kind of /// constraint, inserts, updates and deletes, more inserts into the space the deletes freed, a column added, - /// retyped and dropped, indexes made and dropped, and a table dropped and another made after it. + /// retyped and dropped, indexes made and dropped, and a table dropped and another made after it. Then what + /// spans pages: a thousand rows from one INSERT … SELECT over many data pages with a text index grown + /// past one leaf, a 253-column table whose definition runs onto continuation pages, and long values chained + /// across pages, grown, shrunk and deleted. private static string[] Statements() { var s = new List @@ -191,9 +196,61 @@ private static string[] Statements() "INSERT INTO Customer (Name, City, Joined, Credit, Active, Email) VALUES ('Late comer', 'Oslo', #2023-01-01#, 10, TRUE, 'late@example.com')", ]); + // A table over many data pages, filled by one INSERT … SELECT over a cross join, with an index that grows + // past one leaf; deletes across its pages; and updates that grow rows until they have to move. + s.Add("CREATE TABLE Digits (D BYTE)"); + for (int d = 0; d <= 9; d++) s.Add($"INSERT INTO Digits (D) VALUES ({d})"); + s.AddRange( + [ + "CREATE TABLE Bulk (Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), Payload TEXT(255), " + + "Code BINARY(8), Amount DECIMAL(10,2))", + "CREATE INDEX ixBulkLabel ON Bulk (Label)", + "INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT a.D * 100 + b.D * 10 + c.D, a.D, " + + "'Bulk label number ' & (a.D * 100 + b.D * 10 + c.D) & ' ' & String(30, 'L'), String(150, 'p'), " + + "a.D + b.D * 0.5 FROM Digits AS a, Digits AS b, Digits AS c ORDER BY 1", + "DELETE FROM Bulk WHERE Id MOD 7 = 0", + "UPDATE Bulk SET Payload = Payload & String(100, 'g') WHERE Id MOD 10 = 1", + "UPDATE Bulk SET Label = 'R ' & Label WHERE Id < 50", + "UPDATE Bulk SET Code = 0x0102030405060708 WHERE Id < 5", + "INSERT INTO Bulk (Id, Grp, Label, Payload) VALUES (5000, 1, 'Late bulk', 'late')", + ]); + + // A 253-column table, whose definition runs onto continuation pages, leaving room for one more column id + // after a drop — ids are never reused before a compact, and 255 is the lifetime limit. + string[] wideTypes = ["LONG", "TEXT(20)", "DOUBLE", "DATETIME", "CURRENCY", "BIT"]; + s.Add("CREATE TABLE Wide (Id LONG CONSTRAINT pkWide PRIMARY KEY, " + + string.Join(", ", Enumerable.Range(1, 252).Select(i => $"Column{i:D3} {wideTypes[i % wideTypes.Length]}")) + ")"); + for (int i = 1; i <= 3; i++) + s.Add($"INSERT INTO Wide (Id, Column001, Column002, Column252) VALUES ({i}, 'wide {i}', {i * 1.5}, {i})"); + s.AddRange( + [ + "ALTER TABLE Wide DROP COLUMN Column005", + "ALTER TABLE Wide ADD COLUMN Extra TEXT(10)", + "UPDATE Wide SET Extra = 'extra' WHERE Id = 2", + ]); + + // Long values across pages, the other binary types, and compressed text. + s.AddRange( + [ + "CREATE TABLE Doc (Id LONG CONSTRAINT pkDoc PRIMARY KEY, Body MEMO, Packed MEMO WITH COMPRESSION, " + + "Brief TEXT(100) WITH COMPRESSION, Blob LONGBINARY, Big BIGBINARY(500))", + $"INSERT INTO Doc VALUES (1, String(5000, 'b'), String(300, 'c'), 'short and compressed', 0x{Hex(6000, 1)}, 0x{Hex(400, 2)})", + $"INSERT INTO Doc VALUES (2, String(2500, 'd'), 'packed two', 'two', 0x{Hex(100, 3)}, 0x{Hex(50, 4)})", + "INSERT INTO Doc (Id, Body, Brief) VALUES (3, 'small', 'three')", + "UPDATE Doc SET Body = String(8000, 'e') WHERE Id = 2", + "UPDATE Doc SET Body = 'now small' WHERE Id = 1", + $"UPDATE Doc SET Blob = 0x{Hex(9000, 5)} WHERE Id = 3", + "DELETE FROM Doc WHERE Id = 2", + "INSERT INTO Doc (Id, Body) VALUES (4, String(4000, 'f'))", + ]); + return [.. s]; } + /// bytes as hex, a repeating pattern seeded by . + private static string Hex(int bytes, int seed) => + Convert.ToHexString([.. Enumerable.Range(0, bytes).Select(i => (byte)((i * 7 + seed) % 251))]); + private static string Notes(int i) => string.Concat(Enumerable.Repeat($"Note {i}. ", i * 12)); private static string Abbreviate(string statement) => statement.Length <= 90 ? statement : statement[..87] + "..."; @@ -345,6 +402,28 @@ private static void Mask(string path, byte[] file, HashSet[] masks) for (int i = 0; i < 8; i++) masks[page].Add(start + 2 + offset + i); } } + + // A chained long value's descriptor carries the same write stamp as the chain's first page, bytes 8–11 of + // its twelve (long-values.md). + foreach (TableDef definition in database.Catalog.Tables) + { + if (!definition.Columns.Any(c => c.Type is JetDataType.Memo or JetDataType.Ole)) continue; + Table table = database.OpenTable(definition.Name); + foreach ((RowId id, _) in table.Rows().WithIds()) + { + var page = new DataPage(); + page.Read(table.Channel.ReadPageShared(id.Page), table.Channel.Format); + if (page.Rows[id.Row] is not { IsDeleted: false, HasOverflow: false } slot) continue; + ReadOnlySpan row = page.GetRow(id.Row); + foreach (byte[] descriptor in RowDecoder.LongValueDescriptors(definition.Columns, table.Channel.Format, row).Values) + { + if (descriptor.Length < 12 || descriptor[3] != 0x00) continue; + int at = row.IndexOf(descriptor); + if (at >= 0) + for (int i = 8; i < 12; i++) masks[id.Page].Add(slot.Offset + at + i); + } + } + } } /// An empty ACE 12 database made by DAO, the path Access itself takes; null where DAO is missing. From 870346adf19433d532fb5e71389a3d277f55b80b Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 00:00:21 +0800 Subject: [PATCH 23/88] tests --- .../NorthwindBulkUpdatesLibRedTest.cs | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/test/EFCore.LibRed.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesLibRedTest.cs b/test/EFCore.LibRed.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesLibRedTest.cs index cb1d0aa4..023b7dd1 100644 --- a/test/EFCore.LibRed.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesLibRedTest.cs +++ b/test/EFCore.LibRed.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesLibRedTest.cs @@ -1609,18 +1609,14 @@ public override async Task Update_Where_Join_set_property_from_joined_table(bool AssertExecuteUpdateSql( """ -UPDATE `Customers` AS `c2` -INNER JOIN ( - SELECT `c`.`CustomerID`, `c1`.`City` AS `City0` - FROM `Customers` AS `c`, - ( - SELECT `c0`.`City` - FROM `Customers` AS `c0` - WHERE `c0`.`CustomerID` = 'ALFKI' - ) AS `c1` - WHERE `c`.`CustomerID` LIKE 'F%' -) AS `s` ON `c2`.`CustomerID` = `s`.`CustomerID` -SET `c2`.`City` = `s`.`City0` +UPDATE `Customers` AS `c`, +( + SELECT `c0`.`City` + FROM `Customers` AS `c0` + WHERE `c0`.`CustomerID` = 'ALFKI' +) AS `c1` +SET `c`.`City` = `c1`.`City` +WHERE `c`.`CustomerID` LIKE 'F%' """); } From c4d0c58f21077388911048d5cbcaefe36d97c05c Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 00:49:11 +0800 Subject: [PATCH 24/88] Decode text, currency and rows without the general-purpose detours A profile of a 10,000-row scan put most of its time in three places, none of them the format: - Plain UTF-16 text went through Encoding.Unicode, which counts and then converts with full validation - about a fifth of the scan. Without a surrogate code unit and at an even length the bytes ARE the string's chars, so they are copied; anything the decoder could treat differently (a lone surrogate, an odd trailing byte) still goes through it. Compressed text with no switch byte is one Latin-1 run, which is what the per-character loop produced for it; the mixed form decodes into a stack buffer rather than a StringBuilder. - CURRENCY was divided by 10000m per value. Division returns the smallest scale that holds the quotient, so the same decimal - value and scale, which shows in its text - is built from the integer by dropping the four places' trailing zeros. CurrencyDecodeTests holds the two equal bit for bit across 600,000 values and the edges. - RowDecoder enumerated its columns through the interface, and HasVariableSection re-scanned them, for every row. It walks an array now and answers the variable-section question from the lowest variable column id; Boolean values come from two shared boxes. The raw scan goes from 4,084 to 2,895 us and 5,385 to 4,761 KB. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Storage/RowDecoder.cs | 34 +++++++-- .../LibRed.Core/Storage/Types/JetTypeCodec.cs | 72 ++++++++++++++++--- test/LibRed.Core.Tests/CurrencyDecodeTests.cs | 41 +++++++++++ .../JetTypeCodecBoundaryTests.cs | 29 ++++++++ 4 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 test/LibRed.Core.Tests/CurrencyDecodeTests.cs diff --git a/src/LibRed/LibRed.Core/Storage/RowDecoder.cs b/src/LibRed/LibRed.Core/Storage/RowDecoder.cs index 7995a694..823dd919 100644 --- a/src/LibRed/LibRed.Core/Storage/RowDecoder.cs +++ b/src/LibRed/LibRed.Core/Storage/RowDecoder.cs @@ -1,6 +1,7 @@ using LibRed.Catalog; using LibRed.Formats; using LibRed.Storage.Types; +using System.Buffers.Binary; namespace LibRed.Storage; @@ -30,26 +31,47 @@ namespace LibRed.Storage; /// public sealed class RowDecoder(IReadOnlyList columns, JetFormatBase format, LongValueReader? longValues = null) { - private readonly IReadOnlyList _columns = columns; private readonly JetFormatBase _format = format; private readonly LongValueReader? _longValues = longValues; + // Decode runs once per row, so it walks an array rather than enumerating the interface (a boxed + // enumerator per row), and answers RowLayout.HasVariableSection from the lowest variable column id + // instead of re-scanning the columns: a row has a variable section exactly when its stored count + // reaches past that id. + private readonly ColumnDef[] _columnArray = [.. columns]; + private readonly int _lowestVariableId = LowestVariableId(columns); + + private static readonly object BoxedTrue = true; + private static readonly object BoxedFalse = false; + + private static int LowestVariableId(IReadOnlyList columns) + { + int lowest = int.MaxValue; + foreach (ColumnDef column in columns) + if (!column.IsFixedLength && column.ColumnId < lowest) + lowest = column.ColumnId; + return lowest; + } + /// Decodes the row into one value per column (aligned to ). public object?[] Decode(ReadOnlySpan row) { - var values = new object?[_columns.Count]; + var values = new object?[_columnArray.Length]; // The null-bitmap width comes from the row's own leading count (= max id + 1), NOT the live column // count — they differ once ids have a gap (a burned type-change id / DROP COLUMN gap). Reading the // stored count is exactly how ACE sizes it, and is robust to any id scheme (spec §5). A real inline // row is at least: column count + an empty var table (1 entry) + var count + null bitmap; anything // shorter is an overflow/lookup pointer slot the caller should have skipped. - RowLayout layout = ParseLayout(row); + if (row.Length < _format.RowColumnCountSize) + throw new InvalidDataException("Row is too short to be an inline record."); + bool hasVar = _lowestVariableId < BinaryPrimitives.ReadUInt16LittleEndian(row); + RowLayout layout = RowLayout.Parse(row, _format.RowColumnCountSize, hasVar); int nullBitmapSize = layout.NullBitmapSize; ReadOnlySpan nullBitmap = row[^nullBitmapSize..]; - foreach (ColumnDef column in _columns) + foreach (ColumnDef column in _columnArray) { bool present = IsPresent(nullBitmap, layout.ColumnCount, column.ColumnId); @@ -61,7 +83,7 @@ public sealed class RowDecoder(IReadOnlyList columns, JetFormatBase f // for a stored False too. Reading the bit here would report True for every row. if (column.Type == JetDataType.Boolean && !column.IsCalculated) { - values[column.Index] = present; + values[column.Index] = present ? BoxedTrue : BoxedFalse; continue; } @@ -179,8 +201,6 @@ private LongValueReader Reader(ColumnDef column) => + $"{nameof(LongValueReader)}; this {nameof(RowDecoder)} was constructed without one. " + $"Use {nameof(LongValueDescriptors)} to read the stored descriptors instead."); - private RowLayout ParseLayout(ReadOnlySpan row) => ParseLayout(_columns, _format, row); - internal static RowLayout ParseLayout(IReadOnlyList columns, JetFormatBase format, ReadOnlySpan row) { if (row.Length < format.RowColumnCountSize) diff --git a/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs b/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs index 52f025b5..c1b05b4e 100644 --- a/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs +++ b/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs @@ -66,7 +66,7 @@ public static class JetTypeCodec case JetDataType.DateTimeExtended: // ACE 17 DATETIME2 return DecodeExtendedDateTime(value); case JetDataType.Currency: - return BinaryPrimitives.ReadInt64LittleEndian(value) / 10000m; + return CurrencyFromScaled(BinaryPrimitives.ReadInt64LittleEndian(value)); case JetDataType.Guid: return new Guid(value[..16]); case JetDataType.Text: @@ -196,22 +196,74 @@ private static decimal DecodeNumeric(ReadOnlySpan value, byte scale) public static string DecodeText(ReadOnlySpan value) { if (value.Length < 2 || value[0] != 0xFF || value[1] != 0xFE) - return Encoding.Unicode.GetString(value); - - var text = new StringBuilder(value.Length - 2); + return DecodeUtf16(value); + + // With no switch byte the whole value stays in 1-byte mode, where each byte is the character of the + // same number — which is Latin-1 exactly. It is the common case by far, and one vectorised call. + ReadOnlySpan body = value[2..]; + if (!body.Contains((byte)0x00)) + return Encoding.Latin1.GetString(body); + + // Every character takes at least one byte, so the body's length bounds the decoded length. + char[]? rented = null; + Span chars = body.Length <= 256 + ? stackalloc char[256] + : (rented = System.Buffers.ArrayPool.Shared.Rent(body.Length)); + int count = 0; bool oneByte = true; - for (int i = 2; i < value.Length;) + for (int i = 0; i < body.Length;) { - if (value[i] == 0x00) { oneByte = !oneByte; i++; continue; } - if (oneByte) { text.Append((char)value[i]); i++; } + if (body[i] == 0x00) { oneByte = !oneByte; i++; continue; } + if (oneByte) { chars[count++] = (char)body[i]; i++; } else { - if (i + 1 >= value.Length) break; // a truncated trailing pair: take what is whole - text.Append((char)(value[i] | (value[i + 1] << 8))); + if (i + 1 >= body.Length) break; // a truncated trailing pair: take what is whole + chars[count++] = (char)(body[i] | (body[i + 1] << 8)); i += 2; } } - return text.ToString(); + + string text = new(chars[..count]); + if (rented is not null) + System.Buffers.ArrayPool.Shared.Return(rented); + return text; + } + + /// A CURRENCY's stored int64 (the value × 10,000) as the decimal raw / 10000m gives — the same + /// value and the same scale, which shows in its text. + /// Decimal division returns the smallest scale that holds the quotient exactly, so it amounts to + /// dropping the four places' trailing zeros. Doing that on the integer skips a full decimal divide per value, + /// which was a scan's single largest cost after text. CurrencyDecodeTests holds the two equal, bits + /// and all. + internal static decimal CurrencyFromScaled(long raw) + { + byte scale = 4; + while (scale > 0 && raw % 10 == 0) + { + raw /= 10; + scale--; + } + + // |long.MinValue| does not fit a long, so the magnitude is taken in unsigned arithmetic. + ulong magnitude = raw < 0 ? (ulong)(-(raw + 1)) + 1 : (ulong)raw; + return new decimal((int)(uint)magnitude, (int)(uint)(magnitude >> 32), 0, raw < 0, scale); + } + + /// Plain UTF-16LE text, as decodes it. + /// Without a surrogate code unit, and at an even length, UTF-16LE bytes ARE the string's chars, so + /// they are copied rather than decoded — the decoder's validation pass, counting and then converting, was + /// most of a text column's cost. Anything it could treat differently (a lone surrogate it replaces, an odd + /// trailing byte) still goes through it. + private static string DecodeUtf16(ReadOnlySpan value) + { + if (BitConverter.IsLittleEndian && (value.Length & 1) == 0) + { + ReadOnlySpan chars = System.Runtime.InteropServices.MemoryMarshal.Cast(value); + if (!chars.ContainsAnyInRange('\uD800', '\uDFFF')) + return new string(chars); + } + + return Encoding.Unicode.GetString(value); } /// diff --git a/test/LibRed.Core.Tests/CurrencyDecodeTests.cs b/test/LibRed.Core.Tests/CurrencyDecodeTests.cs new file mode 100644 index 00000000..460413c3 --- /dev/null +++ b/test/LibRed.Core.Tests/CurrencyDecodeTests.cs @@ -0,0 +1,41 @@ +using LibRed.Storage.Types; +using Xunit; + +namespace LibRed.Core.Tests; + +/// +/// A CURRENCY is decoded by building the decimal from its scaled integer, not by dividing it by 10,000 — and +/// must come out as exactly what the division gave, scale included. The scale is invisible to == but not +/// to the value's text, so these compare the decimals' bits. +/// +public class CurrencyDecodeTests +{ + public static TheoryData Edges => + [ + 0, 1, -1, 9, 10, 99, 100, 999, 1000, 9999, 10000, 10001, 15000, -15000, 123450000, -123450000, + 100000000, 1234567, -1234567, 922337203685477, long.MaxValue, long.MinValue, long.MaxValue - 1, + long.MinValue + 1, long.MaxValue / 10 * 10, long.MinValue / 10 * 10, 4_294_967_296, -4_294_967_296, + ]; + + [Theory] + [MemberData(nameof(Edges))] + public void Matches_division_at_the_edges(long raw) => AssertSameAsDivision(raw); + + [Fact] + public void Matches_division_across_a_dense_range_and_a_random_sweep() + { + for (long raw = -200_000; raw <= 200_000; raw++) + AssertSameAsDivision(raw); + + var random = new Random(20260927); + for (int i = 0; i < 200_000; i++) + AssertSameAsDivision(random.NextInt64(long.MinValue, long.MaxValue)); + } + + private static void AssertSameAsDivision(long raw) + { + decimal expected = raw / 10000m; + decimal actual = JetTypeCodec.CurrencyFromScaled(raw); + Assert.Equal(decimal.GetBits(expected), decimal.GetBits(actual)); + } +} diff --git a/test/LibRed.Core.Tests/JetTypeCodecBoundaryTests.cs b/test/LibRed.Core.Tests/JetTypeCodecBoundaryTests.cs index ffea2a6e..1bd12edb 100644 --- a/test/LibRed.Core.Tests/JetTypeCodecBoundaryTests.cs +++ b/test/LibRed.Core.Tests/JetTypeCodecBoundaryTests.cs @@ -111,6 +111,35 @@ public void Compressed_and_uncompressed_empty_text_are_distinct_encodings_of_the Assert.Equal("Å", JetTypeCodec.DecodeText(Encoding.Unicode.GetBytes("Å"))); } + // Plain UTF-16 is copied rather than decoded when that cannot differ from the decoder, and compressed text + // with no switch byte is read as one Latin-1 run. Both must give exactly what the general paths give. + [Theory] + [InlineData("")] + [InlineData("plain ascii")] + [InlineData("café ñ ü")] + [InlineData("中文 text")] + [InlineData("pair \U0001F600 kept")] + public void Utf16_text_decodes_as_the_encoder_does(string text) + { + byte[] bytes = Encoding.Unicode.GetBytes(text); + Assert.Equal(Encoding.Unicode.GetString(bytes), JetTypeCodec.DecodeText(bytes)); + } + + [Theory] + [InlineData(new byte[] { 0x41, 0x00, 0x00, 0xD8 })] // a lone high surrogate + [InlineData(new byte[] { 0x41, 0x00, 0x00, 0xDC, 0x42, 0x00 })] // a lone low surrogate + [InlineData(new byte[] { 0x41, 0x00, 0x42 })] // an odd trailing byte + public void Malformed_utf16_is_still_handled_as_the_encoder_handles_it(byte[] bytes) + => Assert.Equal(Encoding.Unicode.GetString(bytes), JetTypeCodec.DecodeText(bytes)); + + [Theory] + [InlineData(new byte[] { 0xFF, 0xFE, 0x63, 0x61, 0x66, 0xE9 }, "café")] + [InlineData(new byte[] { 0xFF, 0xFE, 0x80, 0x9F, 0xFF }, "\u0080\u009Fÿ")] + [InlineData(new byte[] { 0xFF, 0xFE, 0x63, 0x61, 0x66, 0xE9, 0x00, 0x2D, 0x4E }, "café中")] + [InlineData(new byte[] { 0xFF, 0xFE, 0x61, 0x00, 0x2D, 0x4E, 0x00, 0x62 }, "a中b")] + public void Compressed_text_decodes_in_both_modes(byte[] bytes, string expected) + => Assert.Equal(expected, JetTypeCodec.DecodeText(bytes)); + // Padding short values is right — ACE stores fixed text space-padded to the full width. Over-long is NOT // truncation: this asserted that it was, which is where the bug lived. ACE refuses an over-long value on a // fixed column exactly as it does on a variable one (measured in FixedWidthOverflowAccessTests), so the From c94c653bd54ffbc1fefc79e15218efbc3820aab6 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 00:49:20 +0800 Subject: [PATCH 25/88] Evaluate every row through one rebound evaluator, not a new one per row fbbed311 memoised where each column reference resolves inside EvalScope, on the premise that only the row moves between rows. That held for the join paths, which rebind one scope, but projection, WHERE, sort keys, group keys and aggregates built a fresh scope and evaluator per row - so each row also built a dictionary, used it once and dropped it. A scan allocated 11,577 KB where it had allocated 9,388, and scan, GROUP BY and the unindexed sort all slowed; found by bisecting on allocation, which is exact where timings are not. Those paths now make one evaluator and rebind it per row, as ExecuteJoin already did. Filter and projection make theirs inside the iterator, so each enumeration of a result has its own. The memo then pays off everywhere, and the per-row scope and evaluator go too: a scan now allocates 5,871 KB. A projection item that is just a column of its input is located once when the projection is planned and read from the row, rather than resolved by name on every row; it still takes the item's conversions. A name that is not exactly one input column (outer, niladic, ambiguous) stays with the evaluator, so its error still comes from the row that raises it. EvalScope.Locate is the one matching rule for both. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Engine/Execution/EvalScope.cs | 15 +-- .../LibRed.Engine/Execution/QueryExecutor.cs | 94 +++++++++++++----- .../LibRed.Engine.Tests/RowScopeReuseTests.cs | 95 +++++++++++++++++++ 3 files changed, 173 insertions(+), 31 deletions(-) create mode 100644 test/LibRed.Engine.Tests/RowScopeReuseTests.cs diff --git a/src/LibRed/LibRed.Engine/Execution/EvalScope.cs b/src/LibRed/LibRed.Engine/Execution/EvalScope.cs index 5ba1e2fc..ad6fb276 100644 --- a/src/LibRed/LibRed.Engine/Execution/EvalScope.cs +++ b/src/LibRed/LibRed.Engine/Execution/EvalScope.cs @@ -53,7 +53,7 @@ public bool TryResolve(ColumnReference reference, out object? value) { _resolved ??= []; if (!_resolved.TryGetValue(reference, out found)) - _resolved[reference] = found = Locate(reference); + _resolved[reference] = found = Locate(schema, reference); } if (found >= 0) @@ -69,10 +69,11 @@ public bool TryResolve(ColumnReference reference, out object? value) return false; } - /// The index of the one column of this scope's schema that names, or - /// -1 for none. Scans to the end whether or not it has matched, because finding a second match is what - /// makes the reference ambiguous. - private int Locate(ColumnReference reference) + /// The index of the one column of that names, + /// or -1 for none. Scans to the end whether or not it has matched, because finding a second match is what + /// makes the reference ambiguous — which throws, or with false is -1 too, + /// for a caller planning ahead that must leave the error to the row that would raise it. + internal static int Locate(IReadOnlyList schema, ColumnReference reference, bool throwIfAmbiguous = true) { int found = -1; for (int i = 0; i < schema.Count; i++) @@ -83,7 +84,9 @@ private int Locate(ColumnReference reference) if (!nameMatch || !qualifierMatch) continue; if (found >= 0) - throw new InvalidOperationException($"Column reference '{Describe(reference)}' is ambiguous."); + return throwIfAmbiguous + ? throw new InvalidOperationException($"Column reference '{Describe(reference)}' is ambiguous.") + : -1; found = i; } return found; diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs index d349fce5..f612bea5 100644 --- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs @@ -639,7 +639,19 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) case FilterNode filter: { var (columns, rows) = Execute(filter.Input, outer); - return (columns, rows.Where(row => Eval(columns, row, outer).IsTrue(filter.Predicate))); + return (columns, FilteredRows()); + + // One scope/evaluator per enumeration, rebound per row, as in ExecuteJoin. Created inside the + // iterator so two enumerations of the result cannot move each other's row. + IEnumerable FilteredRows() + { + ExpressionEvaluator eval = Eval(columns, [], outer); + foreach (object?[] row in rows) + { + if (eval.Rebind(row).IsTrue(filter.Predicate)) + yield return row; + } + } } // A lateral join re-runs its right side per left row, so it cannot go through ExecuteJoin (which @@ -679,16 +691,30 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) ProjectionSchema schema = ProjectionSchemaFor(project, columns); var plan = schema.Plan; - var projected = rows.Select(row => + return (schema.Columns, ProjectedRows()); + + // One scope/evaluator per enumeration, rebound per row, as in FilteredRows above. + IEnumerable ProjectedRows() { - var eval = Eval(columns, row, outer); - return plan.Select(p => p.InputIndex >= 0 - ? row[p.InputIndex] - : ExpressionEvaluator.ToResultPlaces( - ExpressionEvaluator.AsColumnType(eval.Evaluate(p.Expr!), p.ConvertTo, currency: false), p.Type)).ToArray(); - }); - - return (schema.Columns, projected); + ExpressionEvaluator eval = Eval(columns, [], outer); + foreach (object?[] row in rows) + { + eval.Rebind(row); + var values = new object?[plan.Count]; + for (int i = 0; i < plan.Count; i++) + { + var p = plan[i]; + values[i] = p.InputIndex >= 0 + ? row[p.InputIndex] + : ExpressionEvaluator.ToResultPlaces( + ExpressionEvaluator.AsColumnType( + p.Slot >= 0 ? row[p.Slot] : eval.Evaluate(p.Expr!), p.ConvertTo, currency: false), + p.Type); + } + + yield return values; + } + } } case SetOperationNode setOp: @@ -903,8 +929,11 @@ private static OutputColumn SetOperationColumn(OutputColumn left, OutputColumn r /// A ProjectNode's flattened output: the per-item plan (source input index, or an expression to /// evaluate, with the type its values are converted to, if any) and the resulting output columns. /// Structural — the same for every outer row. + /// Slot is where an item that is just a column of the input finds its value, so a row reads + /// it rather than resolving the name again; it still takes the item's conversions, which a passed-through + /// InputIndex column does not. -1 sends the item through the evaluator. private sealed record ProjectionSchema( - List<(OutputColumn Column, int InputIndex, Expression? Expr, NumberType Type, Type? ConvertTo)> Plan, + List<(OutputColumn Column, int InputIndex, Expression? Expr, NumberType Type, Type? ConvertTo, int Slot)> Plan, List Columns); /// Builds — or reuses — a ProjectNode's schema. Flattens the projection, expanding a qualified star @@ -915,14 +944,14 @@ private ProjectionSchema ProjectionSchemaFor(ProjectNode project, IReadOnlyList< if (_projectionSchemas.TryGetValue(project, out ProjectionSchema? cached)) return cached; - var plan = new List<(OutputColumn Column, int InputIndex, Expression? Expr, NumberType Type, Type? ConvertTo)>(); + var plan = new List<(OutputColumn Column, int InputIndex, Expression? Expr, NumberType Type, Type? ConvertTo, int Slot)>(); foreach (SelectItem item in project.Projection) { if (item.Value is QualifiedStarExpression star) { for (int ci = 0; ci < columns.Count; ci++) if (string.Equals(columns[ci].Qualifier, star.Table, StringComparison.OrdinalIgnoreCase)) - plan.Add((columns[ci], ci, null, default, null)); + plan.Add((columns[ci], ci, null, default, null, -1)); } else { @@ -939,7 +968,10 @@ private ProjectionSchema ProjectionSchemaFor(ProjectNode project, IReadOnlyList< OutputColumn.Computed(name, DeclaredType(item.Value, columns), type, item.Value, referenced?.Source) with { Origin = referenced?.Origin ?? ColumnOrigin.Expression, Variant = variant }, - -1, item.Value, type, convertTo)); + -1, item.Value, type, convertTo, + // The evaluator resolves this scope's columns before any outer one, so a name that is one + // column here is that column on every row. None, or more than one, stays with the evaluator. + item.Value is ColumnReference own ? EvalScope.Locate(columns, own, throwIfAmbiguous: false) : -1)); } } @@ -1842,9 +1874,10 @@ public int GetHashCode(object?[] a) var decorated = new List<(object?[] Row, object?[] Keys, int Index)>(); var index = 0; bool[] byText = ComparedAsText(keys.Select(k => k.Value), columns); + ExpressionEvaluator eval = Eval(columns, [], outer); // one evaluator, rebound per row foreach (object?[] row in rows) { - ExpressionEvaluator eval = Eval(columns, row, outer); + eval.Rebind(row); var rowKeys = new object?[keys.Count]; for (var i = 0; i < keys.Count; i++) { @@ -2282,10 +2315,17 @@ private static int CountRows(IEnumerable rows) var order = new List(); var groups = new Dictionary>(); bool[] byText = ComparedAsText(keys, columns); + ExpressionEvaluator eval = Eval(columns, [], outer); // one evaluator, rebound per row foreach (object?[] row in rows) { - var eval = Eval(columns, row, outer); - var key = new GroupKey(keys.Select((k, i) => Compared(eval.Evaluate(k), byText[i])).ToArray()); + eval.Rebind(row); + var keyValues = new object?[keys.Count]; + for (int i = 0; i < keys.Count; i++) + { + keyValues[i] = Compared(eval.Evaluate(keys[i]), byText[i]); + } + + var key = new GroupKey(keyValues); if (!groups.TryGetValue(key, out var list)) { groups[key] = list = []; @@ -2314,9 +2354,13 @@ private static int CountRows(IEnumerable rows) ExpressionEvaluator.ValidateArity(name, call.Arguments.Count); Expression? arg = call.Arguments.Count > 0 ? call.Arguments[0] : null; + // One evaluator, rebound per row. Every sequence below is consumed before this method returns, and one + // at a time, so no two of them are ever positioned on different rows at once. + ExpressionEvaluator eval = Eval(columns, [], outer); + // FILTER (WHERE …) narrows the group before anything else looks at it — COUNT(*) included. if (call.Filter is { } filter) - group = group.Where(r => Eval(columns, r, outer).IsTrue(filter)).ToList(); + group = group.Where(r => eval.Rebind(r).IsTrue(filter)).ToList(); // COUNT(*) counts rows; DISTINCT is meaningless there (and EF never emits it). if (name == "COUNT" && arg is StarExpression or null) @@ -2329,9 +2373,9 @@ private static int CountRows(IEnumerable rows) // FIRST/LAST return the argument's value from the first/last row of the group in scan order — NOT // null-filtered (verified vs ACE: First over a leading NULL row returns NULL). if (name == "FIRST") - return group.Count == 0 ? null : Compared(Eval(columns, group[0], outer).Evaluate(arg!), byText); + return group.Count == 0 ? null : Compared(eval.Rebind(group[0]).Evaluate(arg!), byText); if (name == "LAST") - return group.Count == 0 ? null : Compared(Eval(columns, group[^1], outer).Evaluate(arg!), byText); + return group.Count == 0 ? null : Compared(eval.Rebind(group[^1]).Evaluate(arg!), byText); // A list aggregate, ordered or not: STRING_AGG may go without its WITHIN GROUP, in which case the // values list in the order the rows arrive (no keys, no directions — the sort is stable). @@ -2344,7 +2388,7 @@ private static int CountRows(IEnumerable rows) return ListAgg.Of( group.Select(r => { - ExpressionEvaluator e = Eval(columns, r, outer); + ExpressionEvaluator e = eval.Rebind(r); return (e.Evaluate(call.Arguments[0]), keys.Select(k => e.Evaluate(k)).ToArray()); }), call.Arguments.Count - keys.Count == 2 ? (string)((LiteralExpression)call.Arguments[1]).Value! : "", @@ -2358,8 +2402,8 @@ private static int CountRows(IEnumerable rows) return null; // The fraction is the group's, so any row gives it; the standard makes it a constant. return Percentile.Of(name, - group.Select(r => Eval(columns, r, outer).Evaluate(call.Arguments[1])), - Eval(columns, group[0], outer).Evaluate(call.Arguments[0]), + group.Select(r => eval.Rebind(r).Evaluate(call.Arguments[1])), + eval.Rebind(group[0]).Evaluate(call.Arguments[0]), directions[0]); } @@ -2371,13 +2415,13 @@ private static int CountRows(IEnumerable rows) var pair = new RunningAggregate(name, countRows: false, currency: false); foreach (object?[] row in group) { - ExpressionEvaluator rowEval = Eval(columns, row, outer); + ExpressionEvaluator rowEval = eval.Rebind(row); pair.AddPair(rowEval.Evaluate(call.Arguments[0]), rowEval.Evaluate(call.Arguments[1])); } return pair.Result; } - IEnumerable values = group.Select(r => Compared(Eval(columns, r, outer).Evaluate(arg!), byText)); + IEnumerable values = group.Select(r => Compared(eval.Rebind(r).Evaluate(arg!), byText)); // COUNT(DISTINCT)/SUM(DISTINCT)/… aggregate the distinct set of the argument's values. MIN/MAX are // unaffected by dedup, but applying it uniformly keeps the one code path. if (call.Distinct) diff --git a/test/LibRed.Engine.Tests/RowScopeReuseTests.cs b/test/LibRed.Engine.Tests/RowScopeReuseTests.cs new file mode 100644 index 00000000..08474cf4 --- /dev/null +++ b/test/LibRed.Engine.Tests/RowScopeReuseTests.cs @@ -0,0 +1,95 @@ +using LibRed; +using LibRed.Engine; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// Filter, projection, sort, grouping and aggregation evaluate every row through one scope and evaluator, +/// rebound per row, instead of allocating a pair per row. The rebind is only sound if nothing outlives its row +/// and no two enumerations share a scope, so these pin both: a result enumerated twice, or two enumerations +/// interleaved, each see their own rows, and a correlated subquery sees the row it was asked about. +/// +public class RowScopeReuseTests : TempDatabaseTest +{ + private static QueryEngine Seeded() + { + string path = TemporaryDatabase.CopyPath( + Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "row-scope-"); + var e = new QueryEngine(TemporaryDatabase.OpenTracked(path, readOnly: false)); + + e.ExecuteNonQuery("CREATE TABLE T (Id LONG PRIMARY KEY, Grp LONG, V LONG)"); + e.ExecuteNonQuery("BEGIN TRANSACTION"); + for (int i = 1; i <= 12; i++) + e.ExecuteNonQuery($"INSERT INTO T (Id, Grp, V) VALUES ({i}, {i % 3}, {i * 10})"); + e.ExecuteNonQuery("COMMIT"); + return e; + } + + private const string FilteredProjection = "SELECT Id, V + Id FROM T WHERE V + Id > 40"; + + private static readonly (int Id, int Sum)[] Expected = + [.. Enumerable.Range(4, 9).Select(i => (i, i * 11))]; + + private static (int, int) Read(object?[] row) => (Convert.ToInt32(row[0]), Convert.ToInt32(row[1])); + + [Fact] + public void A_result_enumerated_twice_gives_the_same_rows() + { + IEnumerable rows = Seeded().ExecuteQuery(FilteredProjection).Rows; + Assert.Equal(Expected, rows.Select(Read).ToArray()); + Assert.Equal(Expected, rows.Select(Read).ToArray()); + } + + [Fact] + public void Interleaved_enumerations_do_not_move_each_others_row() + { + IEnumerable rows = Seeded().ExecuteQuery(FilteredProjection).Rows; + using IEnumerator ahead = rows.GetEnumerator(); + using IEnumerator behind = rows.GetEnumerator(); + + var fromAhead = new List<(int, int)>(); + var fromBehind = new List<(int, int)>(); + Assert.True(ahead.MoveNext()); + fromAhead.Add(Read(ahead.Current)); + while (true) + { + bool a = ahead.MoveNext(); + if (a) fromAhead.Add(Read(ahead.Current)); + bool b = behind.MoveNext(); + if (b) fromBehind.Add(Read(behind.Current)); + if (!a && !b) break; + } + + Assert.Equal(Expected, fromAhead); + Assert.Equal(Expected, fromBehind); + } + + [Fact] + public void A_correlated_subquery_sees_the_row_being_filtered_and_projected() + { + var rows = Seeded().ExecuteQuery( + "SELECT t.Id, (SELECT COUNT(*) FROM T AS i WHERE i.Grp = t.Grp AND i.Id < t.Id) FROM T AS t " + + "WHERE (SELECT MAX(i.Id) FROM T AS i WHERE i.Grp = t.Grp) > t.Id").Rows + .Select(Read).ToArray(); + + // Ids 1..9 each have a later row in their group; each is preceded by (Id - 1) / 3 earlier ones. + Assert.Equal([.. Enumerable.Range(1, 9).Select(i => (i, (i - 1) / 3))], rows); + } + + [Fact] + public void Aggregates_evaluate_each_row_of_their_own_group() + { + var rows = Seeded().ExecuteQuery( + "SELECT Grp, SUM(V), FIRST(Id), LAST(Id), COUNT(*) FILTER (WHERE V > 60) FROM T GROUP BY Grp").Rows + .Select(r => r.Select(Convert.ToInt32).ToArray()).ToArray(); + + Assert.Equal( + [ + [0, 300, 3, 12, 2], // 3, 6, 9, 12 + [1, 220, 1, 10, 2], // 1, 4, 7, 10 + [2, 260, 2, 11, 2], // 2, 5, 8, 11 + ], + rows); + } +} From 0b877f695cd06c22b1495d36567adf5584213069 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 00:49:21 +0800 Subject: [PATCH 26/88] Record the ACE head-to-head before and after the evaluator and decode fixes Co-Authored-By: Claude Opus 5.5 --- test/LibRed.Benchmarks/History.tsv | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/LibRed.Benchmarks/History.tsv b/test/LibRed.Benchmarks/History.tsv index c1fb97e5..68d5be2e 100644 --- a/test/LibRed.Benchmarks/History.tsv +++ b/test/LibRed.Benchmarks/History.tsv @@ -18,3 +18,57 @@ utc commit dirty host suite benchmark params mean_us stddev_us alloc_kb 2026-09-08 16:54 4b8ba080 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sub.scalar_correlated 1413.78 18.64 3.39 2026-09-08 16:54 4b8ba080 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sub.scalar_correlated 185.70 1.45 80.85 2026-09-26 15:45 a08c9eb3 dirty DESKTOP-D1RBTPV DdlBenchmarks CreateIndexOverExistingRows - 3155.75 455.25 2792.75 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=agg.group_low_card 10743.60 144.28 49.86 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=agg.group_low_card 15503.04 91.23 14653.90 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=join.hash_unindexed 3876.81 20.44 27.97 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=join.hash_unindexed 4364.72 27.13 5758.53 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=join.index_nested_loop 950.73 25.28 3.83 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=join.index_nested_loop 90.95 0.50 75.97 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.index_range 763.49 4.39 4.94 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.index_range 104.90 0.53 175.70 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.pk_point 706.33 9.45 3.53 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.pk_point 22.28 0.14 23.98 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=scan.three_columns 17996.95 53.12 941.88 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=scan.three_columns 6908.43 55.66 11339.35 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10 5646.32 225.60 3.44 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10 6975.61 14.56 8998.52 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10_indexed 3796.63 22.22 2.63 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10_indexed 20.92 0.22 27.50 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sub.scalar_correlated 1371.43 37.36 3.39 +2026-09-26 16:07 870346ad clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sub.scalar_correlated 123.49 0.45 118.11 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=agg.group_low_card 10611.98 106.56 49.86 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=agg.group_low_card 11636.75 742.07 9513.49 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=join.hash_unindexed 4075.93 106.43 27.97 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=join.hash_unindexed 4265.43 63.56 5540.05 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=join.index_nested_loop 918.38 35.76 3.83 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=join.index_nested_loop 83.08 0.29 62.49 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.index_range 796.82 26.42 4.94 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.index_range 81.27 0.32 80.90 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.pk_point 666.22 10.06 3.53 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.pk_point 22.48 0.10 23.70 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=scan.three_columns 18751.76 176.25 941.88 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=scan.three_columns 4391.28 24.31 5870.87 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10 5521.39 172.16 3.44 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10 6018.04 44.37 5790.54 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10_indexed 3977.17 125.73 2.67 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10_indexed 19.55 0.09 22.30 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sub.scalar_correlated 1318.04 16.95 3.39 +2026-09-26 16:24 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sub.scalar_correlated 120.90 0.86 106.64 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=agg.group_low_card 10603.61 66.40 49.86 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=agg.group_low_card 8372.37 437.67 8888.58 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=join.hash_unindexed 4009.32 80.89 27.97 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=join.hash_unindexed 2987.63 10.82 4911.08 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=join.index_nested_loop 963.97 21.47 3.84 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=join.index_nested_loop 80.44 1.76 61.57 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.index_range 826.29 11.71 4.94 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.index_range 66.92 0.18 73.87 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.pk_point 685.99 11.42 3.53 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.pk_point 21.90 0.16 23.48 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=scan.three_columns 17669.79 37.24 941.88 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=scan.three_columns 2706.86 18.90 5245.75 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10 5363.66 48.15 3.44 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10 4637.08 12.42 5165.44 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10_indexed 3798.43 32.80 2.63 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10_indexed 17.82 0.17 21.58 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sub.scalar_correlated 1350.68 44.79 3.39 +2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sub.scalar_correlated 118.20 0.99 105.83 From 06380e4cb4169dd21fe9b28805b96e9869520982 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 01:09:14 +0800 Subject: [PATCH 27/88] Decode only the columns a statement reads Decoding was most of a scan's cost, and every read decoded every column: a three-column SELECT over a ten-column table built all ten, and a GROUP BY held all ten of every row alive. A row decoder now takes a mask, and a column outside it reads as null - so the mask is only ever set where nothing can look at that column. Queries: ColumnPruning runs after index selection on every plan that is executed - top-level statements, INSERT ... SELECT and SELECT INTO sources, derived and lateral sources, subqueries - and tells each scan and seek which names to decode. The names come from every column reference anywhere in the plan, subqueries included (their correlated references read the outer tables), by name alone, so a table decodes anything a reference could mean. They are found by walking every public property of the plan and AST records, so a clause added to a node later cannot be missed; a value the walk does not know turns pruning off. A read is pruned only below a projection or aggregate without a star, because SELECT *, set operations and DISTINCT pass whole rows to the output, and DISTINCTROW dedupes on the underlying rows - which is also why nothing below a node the pass does not know is pruned. UPDATE and DELETE: the read that chooses the rows decodes what the statement names anywhere, and CompleteRows then reads each row about to be written in full, into the array its joined rows share, before any of it is used - the rewrite, its constraint and cascade checks and its index moves all see every value, as before. The parent check every child INSERT makes decodes the key alone; a cascade finds its children by key and reads the matches whole; complex-value cleanup reads the link and the index keys. ColumnPruningTests covers each place a column can be named, each shape that passes a row through whole, and each write. Breaking the write-side completion fails five of them, and pruning below DISTINCTROW two (it collapses 12 rows to 3). Against ACE at 10,000 rows: scan 2,707 -> 1,652 us, GROUP BY 8,372 -> 4,436, hash join 2,988 -> 1,681, unindexed TOP 10 4,637 -> 3,317. The walk reads properties through compiled getters: through PropertyInfo it cost a primary-key lookup 4 us of its 22. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Storage/RowDecoder.cs | 14 +- src/LibRed/LibRed.Core/Storage/Table.cs | 27 +- src/LibRed/LibRed.Core/Storage/TableCursor.cs | 8 +- .../LibRed.Engine/Execution/QueryExecutor.cs | 16 +- .../Execution/StatementExecutor.cs | 100 ++++++- src/LibRed/LibRed.Engine/Plan/PlanNodes.cs | 19 +- .../LibRed.Engine/Planning/ColumnPruning.cs | 163 +++++++++++ src/LibRed/LibRed.Engine/QueryEngine.cs | 7 +- .../LibRed.Engine.Tests/ColumnPruningTests.cs | 271 ++++++++++++++++++ 9 files changed, 588 insertions(+), 37 deletions(-) create mode 100644 src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs create mode 100644 test/LibRed.Engine.Tests/ColumnPruningTests.cs diff --git a/src/LibRed/LibRed.Core/Storage/RowDecoder.cs b/src/LibRed/LibRed.Core/Storage/RowDecoder.cs index 823dd919..1f3e7151 100644 --- a/src/LibRed/LibRed.Core/Storage/RowDecoder.cs +++ b/src/LibRed/LibRed.Core/Storage/RowDecoder.cs @@ -29,8 +29,17 @@ namespace LibRed.Storage; /// instance "mode" means. Each takes either the row alone or a the caller has already /// parsed, so a caller wanting both off one row derives the trailer arithmetic once. /// -public sealed class RowDecoder(IReadOnlyList columns, JetFormatBase format, LongValueReader? longValues = null) +/// The table's columns. +/// The file's format. +/// Page access for memo/OLE values; see the remarks. +/// Which columns to decode, by , or null for all of them. A +/// column left out is null in every row, whatever is stored — so this is only for a reader that provably never +/// looks at it (a query that does not name it), never for a row that is going to be written back. +public sealed class RowDecoder( + IReadOnlyList columns, JetFormatBase format, LongValueReader? longValues = null, bool[]? decode = null) { + private readonly bool[]? _decode = decode; + private readonly JetFormatBase _format = format; private readonly LongValueReader? _longValues = longValues; @@ -73,6 +82,9 @@ private static int LowestVariableId(IReadOnlyList columns) foreach (ColumnDef column in _columnArray) { + if (_decode is not null && !_decode[column.Index]) + continue; + bool present = IsPresent(nullBitmap, layout.ColumnCount, column.ColumnId); // Jet stores Boolean (YesNo) columns with no fixed/variable data: the value diff --git a/src/LibRed/LibRed.Core/Storage/Table.cs b/src/LibRed/LibRed.Core/Storage/Table.cs index 4158b047..06e5df13 100644 --- a/src/LibRed/LibRed.Core/Storage/Table.cs +++ b/src/LibRed/LibRed.Core/Storage/Table.cs @@ -23,11 +23,14 @@ public Table(PageChannel channel, TableDef definition) public string Name => Definition.Name; /// Returns a forward-only cursor over all rows in the table. - public TableCursor Rows() => new(this); + /// Which columns to decode, by , or null for all; a column + /// left out reads as null. For a reader that never looks at it — see . + public TableCursor Rows(bool[]? decode = null) => new(this, decode); /// A row decoder over this table's columns — reuse one across a seek/scan rather than allocating /// per row (each carries a shared ). - private RowDecoder NewDecoder() => new(Definition.Columns, Channel.Format, new LongValueReader(Channel)); + private RowDecoder NewDecoder(bool[]? decode = null) => + new(Definition.Columns, Channel.Format, new LongValueReader(Channel), decode); /// Decodes the row at (following an overflow forward-pointer to a /// relocated row), or if the slot is empty/deleted. Used by an index seek, which @@ -58,20 +61,22 @@ public Table(PageChannel channel, TableDef definition) /// Yields the rows whose key equals — an index /// seek (equality) instead of a full scan. May over-return (lossy text/binary keys); the caller re-checks - /// the predicate. - public IEnumerable SeekRows(IndexDef index, object?[] values) + /// the predicate. is as for . + public IEnumerable SeekRows(IndexDef index, object?[] values, bool[]? decode = null) { - var decoder = NewDecoder(); + var decoder = NewDecoder(decode); foreach (RowId id in new IndexWriter(Channel, Definition).Seek(index, values)) if (GetRow(id, decoder) is { } row) yield return row; } /// Like but yields each matching row together with its — - /// for an UPDATE/DELETE join that must know which physical row to rewrite/remove, not just its values. - public IEnumerable<(RowId Id, object?[] Values)> SeekRowsWithIds(IndexDef index, object?[] values) + /// for an UPDATE/DELETE join that must know which physical row to rewrite/remove, not just its values. + /// is as for : a row that is then written has to be read again + /// in full. + public IEnumerable<(RowId Id, object?[] Values)> SeekRowsWithIds(IndexDef index, object?[] values, bool[]? decode = null) { - var decoder = NewDecoder(); + var decoder = NewDecoder(decode); foreach (RowId id in new IndexWriter(Channel, Definition).Seek(index, values)) if (GetRow(id, decoder) is { } row) yield return (id, row); @@ -79,10 +84,10 @@ public Table(PageChannel channel, TableDef definition) /// Yields the rows whose key lies in [, /// ] (either bound null = open) — an index range scan. May over-return at the - /// boundaries; the caller re-checks the predicate. - public IEnumerable SeekRangeRows(IndexDef index, object?[]? low, object?[]? high) + /// boundaries; the caller re-checks the predicate. is as for . + public IEnumerable SeekRangeRows(IndexDef index, object?[]? low, object?[]? high, bool[]? decode = null) { - var decoder = NewDecoder(); + var decoder = NewDecoder(decode); foreach (RowId id in new IndexWriter(Channel, Definition).SeekRange(index, low, high)) if (GetRow(id, decoder) is { } row) yield return row; diff --git a/src/LibRed/LibRed.Core/Storage/TableCursor.cs b/src/LibRed/LibRed.Core/Storage/TableCursor.cs index eb881e78..c994c28c 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCursor.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCursor.cs @@ -7,9 +7,12 @@ namespace LibRed.Storage; /// A forward-only cursor over the rows of a table. Walks the table's data pages, /// decodes each inline row, and yields one value array per row. /// -public sealed class TableCursor(Table table) : IEnumerable +/// The table to read. +/// Which columns to decode, or null for all; see . +public sealed class TableCursor(Table table, bool[]? decode = null) : IEnumerable { private readonly Table _table = table; + private readonly bool[]? _decode = decode; public IEnumerator GetEnumerator() { @@ -24,7 +27,8 @@ public sealed class TableCursor(Table table) : IEnumerable var decoder = new RowDecoder( _table.Definition.Columns, _table.Channel.Format, - new LongValueReader(_table.Channel)); + new LongValueReader(_table.Channel), + _decode); foreach (int pageNumber in _table.UsageMap.DataPages()) { diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs index f612bea5..dd55b2a4 100644 --- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs @@ -530,7 +530,8 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) { var outerAliases = outerScope.VisibleAliases().ToHashSet(StringComparer.OrdinalIgnoreCase); _subqueryPlans[query] = plan = - Planning.IndexSelection.Apply(QueryPlanner.PlanStatement(query), _database.Catalog, outerAliases); + ColumnPruning.Apply( + Planning.IndexSelection.Apply(QueryPlanner.PlanStatement(query), _database.Catalog, outerAliases)); } return plan; } @@ -581,7 +582,7 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) var table = _database.OpenTable(scan.Table); string alias = scan.Alias ?? scan.Table; var columns = table.Definition.Columns.Select(c => OutputColumn.Of(alias, c)).ToList(); - return (columns, _describing ? [] : table.Rows()); + return (columns, _describing ? [] : table.Rows(ColumnPruning.Mask(table.Definition, scan.Decode))); } case IndexSeekNode seek: @@ -607,7 +608,8 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) // by scanning instead. The FilterNode this seek was planned under is kept whatever happens // (see IndexSelection), so it re-checks every row either way and the answer is the same one // the table would give with no index on it at all. - return (columns, seekable ? table.SeekRows(seek.Index, keyValues) : table.Rows()); + bool[]? decode = ColumnPruning.Mask(table.Definition, seek.Decode); + return (columns, seekable ? table.SeekRows(seek.Index, keyValues, decode) : table.Rows(decode)); } case IndexRangeSeekNode range: @@ -626,7 +628,8 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) v[col] = evaluator.Evaluate(e); return v; } - return (columns, table.SeekRangeRows(range.Index, Bound(range.Low), Bound(range.High))); + return (columns, table.SeekRangeRows(range.Index, Bound(range.Low), Bound(range.High), + ColumnPruning.Mask(table.Definition, range.Decode))); } case DerivedTableNode derived: @@ -1564,6 +1567,7 @@ private static HashSet ContributingQualifiers( var seekColumns = innerTable.Definition.Columns.Select(c => OutputColumn.Of(innerAlias, c)).ToList(); var joinColumns = leftColumns.Concat(seekColumns).ToList(); int[] keyCols = seek.Index.Columns.Select(c => c.Column.Index).ToArray(); + bool[]? decode = ColumnPruning.Mask(innerTable.Definition, seek.Decode); IEnumerable SeekRows() { @@ -1589,7 +1593,9 @@ private static HashSet ContributingQualifiers( out keyValues[keyCols[i]]); bool matched = false; - foreach (object?[] right in seekable ? innerTable.SeekRows(seek.Index, keyValues) : innerTable.Rows()) + foreach (object?[] right in seekable + ? innerTable.SeekRows(seek.Index, keyValues, decode) + : innerTable.Rows(decode)) { object?[] combined = [.. left, .. right]; if (on is null || onEval.Rebind(combined).IsTrue(on)) diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 8cd9067f..eb391fe9 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -222,7 +222,16 @@ private bool ParentRowExists(ForeignKey fk, object?[] target) { Table parent = _database.OpenTable(fk.ReferencedTable); int[] parentCols = ReferencedColumnsOf(fk, parent); - return parent.Rows().Any(row => KeyEquals(row, parentCols, target)); + return parent.Rows(KeyMask(parent, parentCols)).Any(row => KeyEquals(row, parentCols, target)); + } + + /// A decode mask for just of : a key comparison + /// reads nothing else, and every other column of every row it passes over would be decoded for nothing. + private static bool[] KeyMask(Table table, int[] columns) + { + var mask = new bool[table.Definition.Columns.Count]; + foreach (int column in columns) mask[column] = true; + return mask; } /// The positions the relationship's referenced columns occupy in a row of the parent table. @@ -251,7 +260,11 @@ private IEnumerable ChildRelationshipsOf(string parentTable) => { Table child = _database.OpenTable(fk.Table); int[] childCols = [.. fk.Columns.Select(c => child.Definition.FindColumn(c.Column)!.Index)]; - return [.. child.Rows().WithIds().Where(r => KeyEquals(r.Values, childCols, key))]; + // The search reads the key alone; a match is then read whole, because the cascade rewrites or deletes it. + return [.. child.Rows(KeyMask(child, childCols)).WithIds() + .Where(r => KeyEquals(r.Values, childCols, key)) + .Select(r => (r.Id, child.GetRow(r.Id) + ?? throw new InvalidOperationException($"Row {r.Id} of '{child.Name}' vanished while it was being read.")))]; } /// @@ -316,7 +329,9 @@ private void DeleteComplexValues(Table table, object?[] values) int recordId = Convert.ToInt32(raw, System.Globalization.CultureInfo.InvariantCulture); Table flat = _database.OpenTable(complex.FlatTable.Name); - foreach ((RowId flatId, object?[] flatValues) in flat.Rows().WithIds().ToList()) + // Read for the owner link and the index keys to remove, and nothing else: Delete reads the row itself. + int[] read = [complex.OwnerLink.Index, .. flat.Definition.RealIndexes.SelectMany(i => i.Columns).Select(c => c.Column.Index)]; + foreach ((RowId flatId, object?[] flatValues) in flat.Rows(KeyMask(flat, read)).WithIds().ToList()) { if (flatValues[complex.OwnerLink.Index] is not { } link || Convert.ToInt32(link, System.Globalization.CultureInfo.InvariantCulture) != recordId) @@ -803,8 +818,8 @@ private int ExecuteSelectInto(SelectStatement statement) // 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)); + ResultSet source = _scalarRunner.ExecuteQuery(Planning.ColumnPruning.Apply(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 @@ -993,8 +1008,8 @@ void InsertRow(ReadOnlySpan supplied) // 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)); + ResultSet source = _scalarRunner.ExecuteQuery(Planning.ColumnPruning.Apply( + 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, @@ -1154,9 +1169,9 @@ void EmitLateral(SubqueryTable sq, JoinKind kind, Expression? on) // correlated predicate becomes a seek rather than a rescan, which matters here more than anywhere // because the body runs once per outer row. var outerColumns = tables.SelectMany(t => t.Columns).ToList(); - Plan.PlanNode plan = Planning.IndexSelection.Apply( + Plan.PlanNode plan = Planning.ColumnPruning.Apply(Planning.IndexSelection.Apply( Planning.QueryPlanner.PlanStatement(sq.Query), _database.Catalog, - tables.Select(t => t.Alias).ToHashSet(StringComparer.OrdinalIgnoreCase)); + tables.Select(t => t.Alias).ToHashSet(StringComparer.OrdinalIgnoreCase))); // The rows vary per outer row but the schema does not, and JoinRows needs it before reading any row. // Probe once against an all-null outer row and drop the rows unread, as QueryExecutor.ExecuteApply does. @@ -1399,7 +1414,8 @@ ExpressionEvaluator Over(object?[] values) => // PlanStatement, not PlanSelect: EF puts a set operation here for Union/Except/Intersect/Concat before // an ExecuteUpdate, and a table value constructor for an inline collection. Same widening as the // subquery predicates needed, for the same reason - a derived table is a query expression. - var plan = Planning.IndexSelection.Apply(Planning.QueryPlanner.PlanStatement(query), _database.Catalog); + var plan = Planning.ColumnPruning.Apply( + Planning.IndexSelection.Apply(Planning.QueryPlanner.PlanStatement(query), _database.Catalog)); ResultSet result = _scalarRunner.ExecuteQuery(plan); var columns = result.ColumnNames.Select((name, i) => new OutputColumn(alias, name, result.ColumnTypes[i])).ToList(); return (columns, result.Rows.ToList()); @@ -1439,6 +1455,10 @@ ExpressionEvaluator Over(object?[] values) => if (tables.Count == 1) seekPlan[0] = SeekPlanFor(0, tables, where); + // Only the columns the statement names are decoded; the rows it then writes are read again in full by + // CompleteRows before anything is written. + bool[]?[] decode = [.. tables.Select(t => t.Table is { } table ? Planning.ColumnPruning.Mask(table.Definition, _readNames) : null)]; + // Precompute the accumulated columns visible when seeking/evaluating an ON at each depth. var colsUpTo = new List[tables.Count + 1]; colsUpTo[0] = []; @@ -1527,11 +1547,11 @@ void Recurse(int i) var keyEval = new ExpressionEvaluator(new EvalScope(colsUpTo[i], Flatten(acc, i), null), _scalarRunner, _parameters, _session); var keyValues = new object?[tables[i].Table!.Definition.Columns.Count]; keyValues[p.Index.Columns[0].Column.Index] = keyEval.Evaluate(p.Key); - rows = tables[i].Table!.SeekRowsWithIds(p.Index, keyValues); + rows = tables[i].Table!.SeekRowsWithIds(p.Index, keyValues, decode[i]); } else { - rows = tables[i].Table!.Rows().WithIds(); + rows = tables[i].Table!.Rows(decode[i]).WithIds(); } // The ON (also the seek's residual re-check — index keys can over-return) gates each candidate. @@ -1558,6 +1578,33 @@ void Recurse(int i) return result; } + // The column names the UPDATE or DELETE being executed reads anywhere (ON, WHERE, SET values, subqueries, a + // derived source), which is all JoinRows decodes; null decodes every column. + private HashSet? _readNames; + + /// + /// Reads each row the statement is about to write in full. JoinRows decoded only the columns the statement + /// names, which is all that choosing the rows and evaluating the SETs look at — but a row is rewritten, its + /// constraints checked and its index entries moved from all of its values. The rest are filled into the same + /// array, which every joined row of that physical row shares, before any of it is read. + /// + private void CompleteRows(List<(RowId Id, object?[] Values)[]> joinRows, List tables, IEnumerable targets) + { + int[] pruned = [.. targets.Distinct().Where(t => + tables[t].Table is { } table && Planning.ColumnPruning.Mask(table.Definition, _readNames) is not null)]; + if (pruned.Length == 0) return; + + var done = new HashSet(ReferenceEqualityComparer.Instance); + foreach (var combo in joinRows) + foreach (int ti in pruned) + { + if (IsNullExtended(combo[ti]) || !done.Add(combo[ti].Values)) continue; + object?[] full = tables[ti].Table!.GetRow(combo[ti].Id) + ?? throw new InvalidOperationException($"Row {combo[ti].Id} of '{tables[ti].Table!.Name}' vanished while it was being read."); + Array.Copy(full, combo[ti].Values, full.Length); + } + } + /// Concatenates the value arrays of the first accumulated join rows. private static object?[] Flatten((RowId, object?[])[] acc, int count) { @@ -1652,6 +1699,19 @@ private static Table TargetTable(List tables, int ti) => /// Null). /// private int ExecuteUpdate(UpdateStatement statement) + { + _readNames = Planning.ColumnPruning.ReferencedNames(statement); + try + { + return Update(statement); + } + finally + { + _readNames = null; + } + } + + private int Update(UpdateStatement statement) { var (tables, kinds, ons, groupBases) = ResolveSource(statement.From); var columns = tables.SelectMany(t => t.Columns).ToList(); @@ -1684,6 +1744,8 @@ private int ExecuteUpdate(UpdateStatement statement) throw new InvalidOperationException( $"Duplicate output destination '{tables[duplicate.Key.TableIndex].Alias}.{duplicate.First().Column.Name}'."); + CompleteRows(joinRows, tables, targets.Select(t => t.TableIndex)); + // Apply SETs to the shared value arrays; snapshot each touched row's original bytes on first touch. var dirty = new Dictionary<(string, RowId), (Table Table, RowId Id, object?[] Original, object?[] Values)>(); // A null-extended side's value array is the joined row's own, so it keys the new row it becomes. @@ -1802,6 +1864,19 @@ private int ExecuteUpdate(UpdateStatement statement) /// target without a row, which delete nothing but which ACE counts (verified). /// private int ExecuteDelete(DeleteStatement statement) + { + _readNames = Planning.ColumnPruning.ReferencedNames(statement); + try + { + return Delete(statement); + } + finally + { + _readNames = null; + } + } + + private int Delete(DeleteStatement statement) { var (tables, kinds, ons, groupBases) = ResolveSource(statement.From); var columns = tables.SelectMany(t => t.Columns).ToList(); @@ -1809,6 +1884,7 @@ private int ExecuteDelete(DeleteStatement statement) int ti = DeleteTarget(tables, statement.TargetTable); Table target = TargetTable(tables, ti); + CompleteRows(joinRows, tables, [ti]); var deleted = new Dictionary(); int withoutRow = 0; diff --git a/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs b/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs index af9ca58b..57792f7b 100644 --- a/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs +++ b/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs @@ -4,7 +4,12 @@ namespace LibRed.Engine.Plan; /// Full-table scan of a base table, exposing its columns under . -public sealed record ScanNode(string Table, string? Alias) : PlanNode; +public sealed record ScanNode(string Table, string? Alias) : PlanNode +{ + /// The column names the query reads, when ColumnPruning has shown nothing else can reach the + /// output; every other column is left undecoded and reads as null. Null decodes them all. + public IReadOnlySet? Decode { get; init; } +} /// A FROM-less SELECT source: yields exactly one row with no columns, so a constant projection like /// SELECT 2 evaluates once. ACE accepts a bare SELECT 2 (verified) — this matches that. @@ -17,7 +22,11 @@ public sealed record SingleRowNode : PlanNode; /// is lossy for text/binary), so the residual predicate is re-checked by the above. /// A key may reference an outer row (index-nested-loop join). /// -public sealed record IndexSeekNode(string Table, string? Alias, IndexDef Index, IReadOnlyList Keys) : PlanNode; +public sealed record IndexSeekNode(string Table, string? Alias, IndexDef Index, IReadOnlyList Keys) : PlanNode +{ + /// As . + public IReadOnlySet? Decode { get; init; } +} /// /// An index range scan: reads the rows of whose single-column @@ -30,7 +39,11 @@ public sealed record IndexSeekNode(string Table, string? Alias, IndexDef Index, /// That is the one case where the index may have several columns: the bounds are what the executor reads a /// single column for, and there are none. /// -public sealed record IndexRangeSeekNode(string Table, string? Alias, IndexDef Index, Expression? Low, Expression? High) : PlanNode; +public sealed record IndexRangeSeekNode(string Table, string? Alias, IndexDef Index, Expression? Low, Expression? High) : PlanNode +{ + /// As . + public IReadOnlySet? Decode { get; init; } +} /// A derived table: the output of re-exposed under an alias. The alias /// is optional (Access permits an aliasless derived table); its columns are then unqualified. diff --git a/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs b/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs new file mode 100644 index 00000000..27139d99 --- /dev/null +++ b/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs @@ -0,0 +1,163 @@ +using System.Collections; +using System.Collections.Concurrent; +using System.Reflection; +using LibRed.Engine.Plan; +using LibRed.Sql.Ast; +using Linq = System.Linq.Expressions; + +namespace LibRed.Engine.Planning; + +/// +/// A post-planning pass that tells each table read which columns the query can ever look at, so the rest are +/// never decoded. Decoding is most of a scan's cost, and a query rarely reads every column: a three-column +/// SELECT over a ten-column table decoded all ten, and a GROUP BY held all ten of every row alive. +/// +/// +/// An undecoded column reads as null, so the pass has to be right about two things. +/// Which names are read. Every column reference anywhere in the plan counts — including inside a +/// subquery, whose correlated references read the outer tables — by name alone, whatever its qualifier. A table +/// decodes each column any reference could mean, which may be more than the query needs but is never less. The +/// references are found by walking every public property of the plan and AST records rather than a list of +/// the places expressions sit, so a clause added to a node later cannot be missed; a value of a type the walk +/// does not know stops the pass for the whole plan. +/// Where a table's rows can surface. Names only cover what an expression reads. A row can also +/// reach the output whole: SELECT * plans no projection, a set operation or DISTINCT compares whole rows, +/// and DISTINCTROW dedupes on the underlying rows. So a read is pruned only below a node that builds its output +/// from expressions — a projection or an aggregate without a star in it — and nothing below a node this pass +/// does not know is pruned at all. +/// +internal static class ColumnPruning +{ + public static PlanNode Apply(PlanNode plan) + { + if (!HasHiddenRead(plan, visible: true)) + return plan; + HashSet? names = ReferencedNames(plan); + return names is null ? plan : Rewrite(plan, visible: true, names); + } + + /// Whether any table read sits where pruning could apply — so a plan with none is not walked. Its + /// arms are 's: a node it descends through, Rewrite must too, and on the same terms. + private static bool HasHiddenRead(PlanNode node, bool visible) => node switch + { + ScanNode or IndexSeekNode or IndexRangeSeekNode => !visible, + ProjectNode p => HasHiddenRead(p.Input, HasStar(p.Projection)), + AggregateNode a => HasHiddenRead(a.Input, HasStar(a.Projection)), + FilterNode or SortNode or LimitNode or DistinctNode or DerivedTableNode or WindowNode + or JoinNode or HashJoinNode or SetOperationNode => node.Children.Any(c => HasHiddenRead(c, visible)), + _ => false, + }; + + /// The subtree to rewrite. + /// Whether this node's rows can reach the output whole, not only through expressions. + /// The column names the whole plan reads. + private static PlanNode Rewrite(PlanNode node, bool visible, HashSet names) => node switch + { + ScanNode s => visible ? s : s with { Decode = names }, + IndexSeekNode s => visible ? s : s with { Decode = names }, + IndexRangeSeekNode s => visible ? s : s with { Decode = names }, + + // Built from expressions, so what they read is all that is read — unless a star passes a row through. + ProjectNode p => p with { Input = Rewrite(p.Input, HasStar(p.Projection), names) }, + AggregateNode a => a with { Input = Rewrite(a.Input, HasStar(a.Projection), names) }, + + // Rows pass through as they are, so they are as visible as this node's own. + FilterNode f => f with { Input = Rewrite(f.Input, visible, names) }, + SortNode s => s with { Input = Rewrite(s.Input, visible, names) }, + LimitNode l => l with { Input = Rewrite(l.Input, visible, names) }, + DistinctNode d => d with { Input = Rewrite(d.Input, visible, names) }, + DerivedTableNode dt => dt with { Input = Rewrite(dt.Input, visible, names) }, + WindowNode w => w with { Input = Rewrite(w.Input, visible, names) }, + JoinNode j => j with { Left = Rewrite(j.Left, visible, names), Right = Rewrite(j.Right, visible, names) }, + HashJoinNode h => h with { Left = Rewrite(h.Left, visible, names), Right = Rewrite(h.Right, visible, names) }, + SetOperationNode so => so with { Left = Rewrite(so.Left, visible, names), Right = Rewrite(so.Right, visible, names) }, + + // DistinctRowNode compares whole underlying rows; anything else is unknown. Neither is pruned below. + _ => node, + }; + + /// The decode mask for from the names a read may use — by + /// , as RowDecoder takes it — or null to decode every + /// column, when there are no names or every column is among them. + internal static bool[]? Mask(LibRed.Catalog.TableDef table, IReadOnlySet? names) + { + if (names is null) return null; + var mask = new bool[table.Columns.Count]; + bool all = true; + foreach (LibRed.Catalog.ColumnDef column in table.Columns) + all &= mask[column.Index] = names.Contains(column.Name); + return all ? null : mask; + } + + /// Whether a projection passes some input row through whole (* or table.*). COUNT(*)'s + /// star is an argument, not a projection item, so it does not count. + private static bool HasStar(IReadOnlyList projection) => + projection.Any(item => item.Value is StarExpression or QualifiedStarExpression); + + /// Every column name any reference in uses, or null when the walk met a value + /// it cannot see into — then nothing is pruned. + internal static HashSet? ReferencedNames(object root) + { + var names = new HashSet(StringComparer.OrdinalIgnoreCase); + return Collect(root, names) ? names : null; + } + + private static bool Collect(object? value, HashSet names) + { + switch (value) + { + case null or string or byte[] or decimal or DateTime or DateTimeOffset or TimeSpan or Guid or Type: + return true; + case ColumnReference reference: + names.Add(reference.Column); + return true; + case IEnumerable items: + foreach (object? item in items) + if (!Collect(item, names)) return false; + return true; + } + + Type type = value.GetType(); + if (type.IsPrimitive || type.IsEnum || type.Namespace == "LibRed.Catalog") + return true; + if (type.Namespace is not ("LibRed.Sql.Ast" or "LibRed.Engine.Plan")) + return false; + + foreach (Func property in Getters.GetOrAdd(type, ReadableProperties)) + { + object? child; +#pragma warning disable CA1031 // Whatever a computed property throws, it means the walk cannot see into it. + try + { + child = property(value); + } + catch (Exception) + { + return false; // a computed property this shape cannot answer: be safe, prune nothing + } +#pragma warning restore CA1031 + + if (!Collect(child, names)) return false; + } + + return true; + } + + // Compiled rather than read through PropertyInfo.GetValue: the walk runs for every statement planned, and + // reflective reads made it several microseconds — a fifth of a primary-key lookup. + private static readonly ConcurrentDictionary[]> Getters = new(); + + // PlanNode.Children repeats the inputs its own properties already hold. + private static Func[] ReadableProperties(Type type) => + [.. type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead && p.GetIndexParameters().Length == 0 + && !(p.Name == nameof(PlanNode.Children) && typeof(PlanNode).IsAssignableFrom(type))) + .Select(Getter)]; + + private static Func Getter(PropertyInfo property) + { + Linq.ParameterExpression instance = Linq.Expression.Parameter(typeof(object)); + Linq.Expression read = Linq.Expression.Property(Linq.Expression.Convert(instance, property.DeclaringType!), property); + return Linq.Expression.Lambda>(Linq.Expression.Convert(read, typeof(object)), instance).Compile(); + } +} diff --git a/src/LibRed/LibRed.Engine/QueryEngine.cs b/src/LibRed/LibRed.Engine/QueryEngine.cs index e5f2d999..4daeb6c0 100644 --- a/src/LibRed/LibRed.Engine/QueryEngine.cs +++ b/src/LibRed/LibRed.Engine/QueryEngine.cs @@ -261,7 +261,8 @@ private CommandResult ExecuteWritingStatement(SqlStatement statement, IReadOnlyD /// (or, for NOT EXISTS, when it doesn't). @@ROWCOUNT reflects the THEN, or 0 when the guard skips it. private CommandResult ExecuteIfThen(IfThenStatement ifThen, IReadOnlyDictionary? parameters) { - var condPlan = IndexSelection.Apply(QueryPlanner.PlanStatement(ifThen.Condition), _database.Catalog); + var condPlan = ColumnPruning.Apply( + IndexSelection.Apply(QueryPlanner.PlanStatement(ifThen.Condition), _database.Catalog)); bool hasRows = new QueryExecutor(_database, parameters, _session).ExecuteQuery(condPlan).Rows.Any(); if (hasRows == ifThen.Negated) return new CommandResult(ResultSet.Empty, 0); // guard not satisfied — skip the body @@ -269,9 +270,9 @@ private CommandResult ExecuteIfThen(IfThenStatement ifThen, IReadOnlyDictionary< } /// Plans a bound statement, then applies index selection (turning scans into index seeks where a - /// predicate allows). + /// predicate allows) and column pruning (decoding only the columns the query reads). private PlanNode PlanWithIndexes(BoundStatement bound) => - IndexSelection.Apply(QueryPlanner.Plan(bound), _database.Catalog); + ColumnPruning.Apply(IndexSelection.Apply(QueryPlanner.Plan(bound), _database.Catalog)); /// The optimised plan for a query — exposed for tests to assert the chosen access path/strategy /// (e.g. that an unindexed equi-join becomes a hash join). diff --git a/test/LibRed.Engine.Tests/ColumnPruningTests.cs b/test/LibRed.Engine.Tests/ColumnPruningTests.cs new file mode 100644 index 00000000..2b681506 --- /dev/null +++ b/test/LibRed.Engine.Tests/ColumnPruningTests.cs @@ -0,0 +1,271 @@ +using LibRed; +using LibRed.Engine; +using LibRed.Engine.Plan; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// A table read decodes only the columns its statement names (ColumnPruning, and JoinRows for +/// UPDATE and DELETE), and an undecoded column reads as null. So what needs pinning is that nothing which reads +/// a column goes without it: every place a column can be named, every shape that passes a whole row to the +/// output, and every write — which has to rewrite, check and re-index a row from all of its values, including +/// the ones the statement never mentioned. +/// +public class ColumnPruningTests : TempDatabaseTest +{ + private static QueryEngine Seeded() + { + string path = TemporaryDatabase.CopyPath( + Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "column-pruning-"); + var e = new QueryEngine(TemporaryDatabase.OpenTracked(path, readOnly: false)); + + e.ExecuteNonQuery("CREATE TABLE T (Id LONG PRIMARY KEY, Grp LONG, V LONG, Label TEXT(20), Note TEXT(50))"); + e.ExecuteNonQuery("CREATE INDEX IX_Label ON T (Label)"); + e.ExecuteNonQuery("CREATE INDEX IX_V ON T (V)"); + e.ExecuteNonQuery("CREATE TABLE U (K LONG PRIMARY KEY, X LONG, Descr TEXT(20))"); + e.ExecuteNonQuery("BEGIN TRANSACTION"); + for (int i = 1; i <= 12; i++) + e.ExecuteNonQuery($"INSERT INTO T (Id, Grp, V, Label, Note) VALUES ({i}, {i % 3}, {i * 10}, 'L{i}', 'N{i}')"); + for (int k = 0; k <= 2; k++) + e.ExecuteNonQuery($"INSERT INTO U (K, X, Descr) VALUES ({k}, {k * 100}, 'D{k}')"); + e.ExecuteNonQuery("COMMIT"); + return e; + } + + private static List Rows(QueryEngine e, string sql) => [.. e.ExecuteQuery(sql).Rows]; + + private static int[] Ints(QueryEngine e, string sql) => [.. Rows(e, sql).Select(r => Convert.ToInt32(r[0]))]; + + private static int[] Sorted(QueryEngine e, string sql) => [.. Ints(e, sql).Order()]; + + private static object?[] RowOfT(QueryEngine e, int id) => Rows(e, $"SELECT * FROM T WHERE Id = {id}").Single(); + + // --- every place a column can be read from, each the only place that names it ----------------------- + + [Fact] + public void A_column_named_only_in_the_where_is_read() + => Assert.Equal([10, 11, 12], Sorted(Seeded(), "SELECT Id FROM T WHERE V > 90")); + + [Fact] + public void A_column_named_only_in_the_order_by_is_read() + => Assert.Equal([12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], Ints(Seeded(), "SELECT Id FROM T ORDER BY V DESC")); + + [Fact] + public void A_column_named_only_in_the_group_by_is_read() + => Assert.Equal([4, 4, 4], Ints(Seeded(), "SELECT COUNT(*) FROM T GROUP BY Grp")); + + [Fact] + public void A_column_named_only_in_an_aggregate_is_read() + => Assert.Equal([780], Ints(Seeded(), "SELECT SUM(V) FROM T")); + + [Fact] + public void A_column_named_only_in_the_having_is_read() + // Group sums: 0 → 300, 1 → 220, 2 → 260. + => Assert.Equal([0], Ints(Seeded(), "SELECT Grp FROM T GROUP BY Grp HAVING SUM(V) > 260")); + + [Fact] + public void Columns_named_only_in_a_join_are_read() + => Assert.Equal([1, 4, 7, 10], Sorted(Seeded(), "SELECT T.Id FROM T INNER JOIN U ON T.Grp = U.K WHERE U.X = 100")); + + [Fact] + public void Columns_named_only_in_a_hash_join_and_its_projection_are_read() + { + var rows = Rows(Seeded(), "SELECT T.Id, U.Descr FROM T INNER JOIN U ON T.Grp = U.X") + .Select(r => (Convert.ToInt32(r[0]), (string)r[1]!)).Order().ToArray(); + Assert.Equal([(3, "D0"), (6, "D0"), (9, "D0"), (12, "D0")], rows); + } + + [Fact] + public void An_outer_column_named_only_inside_a_correlated_subquery_is_read() + { + QueryEngine e = Seeded(); + Assert.Equal([10], Ints(e, "SELECT t.Id FROM T AS t WHERE EXISTS (SELECT 1 FROM U AS u WHERE u.X = t.V)")); + + var rows = Rows(e, "SELECT t.Id, (SELECT u.Descr FROM U AS u WHERE u.K = t.Grp) FROM T AS t WHERE t.Id <= 3") + .Select(r => (Convert.ToInt32(r[0]), (string)r[1]!)).ToArray(); + Assert.Equal([(1, "D1"), (2, "D2"), (3, "D0")], rows); + } + + [Fact] + public void A_column_named_only_in_an_in_subquery_is_read() + => Assert.Equal([10], Ints(Seeded(), "SELECT Id FROM T WHERE V IN (SELECT X FROM U)")); + + [Fact] + public void Columns_named_only_in_a_window_are_read() + { + var numbers = Rows(Seeded(), "SELECT Id, ROW_NUMBER() OVER (PARTITION BY Grp ORDER BY V DESC) FROM T") + .ToDictionary(r => Convert.ToInt32(r[0]), r => Convert.ToInt32(r[1])); + Assert.Equal(1, numbers[12]); + Assert.Equal(2, numbers[9]); + Assert.Equal(4, numbers[3]); + Assert.Equal(1, numbers[10]); + Assert.Equal(4, numbers[1]); + } + + [Fact] + public void A_derived_star_is_read_for_the_columns_its_outer_query_names() + => Assert.Equal([11, 12], Sorted(Seeded(), "SELECT d.Id FROM (SELECT * FROM T) AS d WHERE d.V > 100")); + + [Fact] + public void Seeks_decode_what_their_residual_and_projection_name() + { + QueryEngine e = Seeded(); + Assert.Equal([5], Ints(e, "SELECT Id FROM T WHERE Label = 'L5' AND V = 50")); + Assert.Equal(["L3", "L4", "L5"], + Rows(e, "SELECT Label FROM T WHERE V BETWEEN 30 AND 50").Select(r => (string)r[0]!).Order().ToArray()); + } + + [Fact] + public void A_bare_count_reads_no_column_and_still_counts_every_row() + => Assert.Equal([12], Ints(Seeded(), "SELECT COUNT(*) FROM T")); + + // --- shapes that pass whole rows to the output, which must not be pruned --------------------------- + + [Fact] + public void Select_star_returns_every_column() + => Assert.Equal([5, 2, 50, "L5", "N5"], RowOfT(Seeded(), 5).Select(Normalise)); + + [Fact] + public void A_qualified_star_returns_every_column_of_its_table() + { + object?[] row = Rows(Seeded(), "SELECT T.*, U.Descr FROM T INNER JOIN U ON T.Grp = U.K WHERE T.Id = 5").Single(); + Assert.Equal([5, 2, 50, "L5", "N5", "D2"], row.Select(Normalise)); + } + + [Fact] + public void A_union_of_stars_returns_every_column() + { + var rows = Rows(Seeded(), "SELECT * FROM T WHERE Id <= 2 UNION SELECT * FROM T WHERE Id <= 2"); + Assert.Equal(["N1", "N2"], rows.Select(r => (string)r[4]!).Order()); + } + + [Fact] + public void Distinctrow_still_tells_underlying_rows_apart_by_columns_it_does_not_output() + { + // Grp 0 joins three U rows, 1 two, 2 one: 24 joined rows over the 12 of T. DISTINCTROW keeps one per T + // row. Deciding that from Grp alone — all a pruned read would have — collapses them to three. + Assert.Equal(24, Rows(Seeded(), "SELECT T.Grp FROM T INNER JOIN U ON U.K >= T.Grp").Count); + Assert.Equal(12, Rows(Seeded(), "SELECT DISTINCTROW T.Grp FROM T INNER JOIN U ON U.K >= T.Grp").Count); + } + + // --- the plan: which reads are pruned, and to what --------------------------------------------------- + + private static IEnumerable Nodes(PlanNode node) => [node, .. node.Children.SelectMany(Nodes)]; + + private static IReadOnlySet? DecodeOf(PlanNode node) => node switch + { + ScanNode s => s.Decode, + IndexSeekNode s => s.Decode, + IndexRangeSeekNode s => s.Decode, + _ => throw new InvalidOperationException($"{node.GetType().Name} is not a table read."), + }; + + private static IEnumerable Reads(PlanNode plan) => + Nodes(plan).Where(n => n is ScanNode or IndexSeekNode or IndexRangeSeekNode); + + [Fact] + public void A_projection_prunes_its_scan_to_the_names_it_reads() + { + IReadOnlySet? decode = DecodeOf(Reads(Seeded().PlanFor("SELECT Id, Label FROM T WHERE Grp = 1")).Single()); + Assert.NotNull(decode); + Assert.Contains("Id", decode); + Assert.Contains("Label", decode); + Assert.Contains("Grp", decode); + Assert.DoesNotContain("Note", decode); + } + + [Theory] + [InlineData("SELECT * FROM T")] + [InlineData("SELECT DISTINCT * FROM T")] + [InlineData("SELECT * FROM T WHERE Id = 3")] + [InlineData("SELECT DISTINCTROW T.Grp FROM T INNER JOIN U ON U.K >= T.Grp")] + public void A_read_whose_rows_can_reach_the_output_whole_is_not_pruned(string sql) + => Assert.All(Reads(Seeded().PlanFor(sql)), read => Assert.Null(DecodeOf(read))); + + // --- writes: a row is rewritten, checked and re-indexed from every value it has ----------------------- + + [Fact] + public void An_update_keeps_the_columns_it_never_names() + { + QueryEngine e = Seeded(); + Assert.Equal(4, e.ExecuteNonQuery("UPDATE T SET V = V + 1 WHERE Grp = 1")); + Assert.Equal([1, 1, 11, "L1", "N1"], RowOfT(e, 1).Select(Normalise)); + Assert.Equal([2, 2, 20, "L2", "N2"], RowOfT(e, 2).Select(Normalise)); + } + + [Fact] + public void A_joined_update_keeps_both_tables_columns() + { + QueryEngine e = Seeded(); + e.ExecuteNonQuery("UPDATE T INNER JOIN U ON T.Grp = U.K SET T.V = U.X WHERE T.Id = 4"); + Assert.Equal([4, 1, 100, "L4", "N4"], RowOfT(e, 4).Select(Normalise)); + Assert.Equal([1, 100, "D1"], Rows(e, "SELECT * FROM U WHERE K = 1").Single().Select(Normalise)); + } + + [Fact] + public void An_update_moves_the_index_entry_of_a_column_it_only_assigns() + { + // Label appears only as a SET target, so the read that finds the row does not decode it; the old index + // entry can only be removed if the row is read in full before it is rewritten. + QueryEngine e = Seeded(); + e.ExecuteNonQuery("UPDATE T SET Label = 'Z' WHERE Id = 2"); + Assert.Equal([2], Ints(e, "SELECT Id FROM T WHERE Label = 'Z'")); + Assert.Empty(Ints(e, "SELECT Id FROM T WHERE Label = 'L2'")); + Assert.Equal([2, 2, 20, "Z", "N2"], RowOfT(e, 2).Select(Normalise)); + } + + [Fact] + public void A_delete_removes_the_index_entries_of_columns_it_never_names() + { + QueryEngine e = Seeded(); + Assert.Equal(1, e.ExecuteNonQuery("DELETE FROM T WHERE V = 30")); + Assert.Empty(Ints(e, "SELECT Id FROM T WHERE Label = 'L3'")); + Assert.Equal([11], Ints(e, "SELECT COUNT(*) FROM T")); + } + + [Fact] + public void An_update_through_a_derived_table_keeps_the_columns_it_does_not_select() + { + QueryEngine e = Seeded(); + Assert.Equal(4, e.ExecuteNonQuery("UPDATE (SELECT Id, V FROM T WHERE Grp = 2) SET V = 0")); + Assert.Equal([5, 2, 0, "L5", "N5"], RowOfT(e, 5).Select(Normalise)); + } + + [Fact] + public void Cascades_and_key_checks_read_what_they_need() + { + QueryEngine e = Seeded(); + e.ExecuteNonQuery("CREATE TABLE Par (Id LONG PRIMARY KEY, Nm TEXT(20))"); + e.ExecuteNonQuery("CREATE TABLE Ch (Id LONG PRIMARY KEY, Pid LONG, Info TEXT(20), " + + "CONSTRAINT fk FOREIGN KEY (Pid) REFERENCES Par (Id) ON UPDATE CASCADE ON DELETE CASCADE)"); + e.ExecuteNonQuery("INSERT INTO Par (Id, Nm) VALUES (1, 'p1')"); + e.ExecuteNonQuery("INSERT INTO Ch (Id, Pid, Info) VALUES (1, 1, 'c1')"); + e.ExecuteNonQuery("INSERT INTO Ch (Id, Pid, Info) VALUES (2, 1, 'c2')"); + + // The parent check reads the key alone, and still finds a missing parent. + Assert.ThrowsAny(() => e.ExecuteNonQuery("INSERT INTO Ch (Id, Pid, Info) VALUES (3, 99, 'x')")); + + // An ON UPDATE CASCADE rewrites each child from all of its values, not just its key. + e.ExecuteNonQuery("UPDATE Par SET Id = 10 WHERE Id = 1"); + Assert.Equal([[1, 10, "c1"], [2, 10, "c2"]], + Rows(e, "SELECT * FROM Ch ORDER BY Id").Select(r => r.Select(Normalise).ToArray()).ToArray()); + + e.ExecuteNonQuery("DELETE FROM Par WHERE Id = 10"); + Assert.Empty(Rows(e, "SELECT * FROM Ch")); + } + + [Fact] + public void Insert_select_and_select_into_carry_the_columns_they_name() + { + QueryEngine e = Seeded(); + e.ExecuteNonQuery("CREATE TABLE T2 (Id LONG, Note TEXT(50))"); + e.ExecuteNonQuery("INSERT INTO T2 (Id, Note) SELECT Id, Note FROM T WHERE Grp = 0"); + Assert.Equal(["N12", "N3", "N6", "N9"], Rows(e, "SELECT Note FROM T2").Select(r => (string)r[0]!).Order()); + + e.ExecuteNonQuery("SELECT Id, Label INTO T3 FROM T WHERE Grp = 1"); + Assert.Equal(["L1", "L10", "L4", "L7"], Rows(e, "SELECT Label FROM T3").Select(r => (string)r[0]!).Order()); + } + + private static object? Normalise(object? value) => value is int or short or long or byte ? Convert.ToInt32(value) : value; +} From 35a85556161b925a16df72b6a68d64ada901d7b4 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 01:09:14 +0800 Subject: [PATCH 28/88] Record the ACE head-to-head with column pruning Co-Authored-By: Claude Opus 5.5 --- test/LibRed.Benchmarks/History.tsv | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/LibRed.Benchmarks/History.tsv b/test/LibRed.Benchmarks/History.tsv index 68d5be2e..d343b8e1 100644 --- a/test/LibRed.Benchmarks/History.tsv +++ b/test/LibRed.Benchmarks/History.tsv @@ -72,3 +72,25 @@ utc commit dirty host suite benchmark params mean_us stddev_us alloc_kb 2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10_indexed 17.82 0.17 21.58 2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sub.scalar_correlated 1350.68 44.79 3.39 2026-09-26 16:41 870346ad dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sub.scalar_correlated 118.20 0.99 105.83 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=agg.group_low_card 10681.59 195.04 49.86 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=agg.group_low_card 4436.07 152.82 5780.63 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=join.hash_unindexed 3985.84 107.62 27.97 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=join.hash_unindexed 1680.76 12.45 2269.99 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=join.index_nested_loop 903.75 5.99 3.83 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=join.index_nested_loop 87.19 2.27 58.16 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.index_range 785.89 13.73 4.94 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.index_range 54.37 0.50 42.46 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.pk_point 701.82 10.54 3.53 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.pk_point 25.88 0.34 23.97 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=scan.three_columns 17838.69 205.94 941.88 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=scan.three_columns 1651.51 7.95 2840.70 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10 5414.83 74.92 3.44 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10 3316.50 82.74 2292.07 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10_indexed 3827.73 36.49 2.63 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10_indexed 19.17 0.10 19.30 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sub.scalar_correlated 1392.71 33.44 3.39 +2026-09-26 17:06 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sub.scalar_correlated 126.70 2.16 103.23 +2026-09-26 17:08 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.pk_point 636.17 8.88 3.52 +2026-09-26 17:08 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.pk_point 22.36 0.13 23.97 +2026-09-26 17:08 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10_indexed 3850.75 45.49 2.63 +2026-09-26 17:08 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10_indexed 17.93 0.49 19.30 From 160d7ea47477b00e0345a3ebce989e4f24394565 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 01:24:48 +0800 Subject: [PATCH 29/88] Look catalog and key rows up by their key, and decode the rest only for a match The reads that find a row by one or two columns decoded every column of every row they passed: an index back-fill decoded whole rows to build keys from a few, and each MSysObjects lookup resolved every object's LvProp blob - a long value - on the way to the one it wanted. Table gains DecodeOnly, a mask for a read that looks at nothing else, and RowsWhere, which searches on the key alone and reads each match whole, because a row looked up by key is usually about to be rewritten or deleted. The index back-fill and its duplicate check, the narrowing ALTER COLUMN check, and the object-name, permission and next-id scans decode only what they compare. The LvProp edits, relationship renames and deletes, catalog-row updates and deletes, permission reads and MarkAsSystemTable find their rows through RowsWhere (most via the existing RowsKeyed), as do a cascade's child search and ReadComplexValues, which no longer decodes other records' attachments to find one record's. A back-fill over 2,000 rows allocates 2,045 KB where it allocated 2,662; the catalog scans are too small to show on the benchmark corpus's three tables. ALTER COLUMN retypes, rebuilds and an AutoNumber ADD COLUMN rewrite every row and still read them whole. LibRed.Core.AccessTests pass, so the DDL still writes what ACE writes. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/JetDatabase.cs | 15 ++--- .../LibRed.Core/Storage/CatalogWriter.cs | 3 +- .../LibRed.Core/Storage/DatabaseCreator.cs | 5 +- src/LibRed/LibRed.Core/Storage/Table.cs | 21 +++++++ .../LibRed.Core/Storage/TableCreator.cs | 60 +++++++++---------- src/LibRed/LibRed.Core/Storage/ViewCreator.cs | 3 +- .../Execution/StatementExecutor.cs | 18 +----- 7 files changed, 64 insertions(+), 61 deletions(-) diff --git a/src/LibRed/LibRed.Core/JetDatabase.cs b/src/LibRed/LibRed.Core/JetDatabase.cs index 54a52513..916f6e3f 100644 --- a/src/LibRed/LibRed.Core/JetDatabase.cs +++ b/src/LibRed/LibRed.Core/JetDatabase.cs @@ -265,15 +265,12 @@ public void RollbackNested() public IReadOnlyList ReadComplexValues(ComplexColumn column, int complexId) { ArgumentNullException.ThrowIfNull(column); - var values = new List(); - foreach (object?[] row in OpenTable(column.FlatTable.Name).Rows()) - { - if (row[column.OwnerLink.Index] is not { } link - || Convert.ToInt32(link, System.Globalization.CultureInfo.InvariantCulture) != complexId) - continue; - values.Add([.. column.ValueColumns.Select(c => row[c.Index])]); - } - return values; + // Found by the owner link alone, so another record's attachments are not read to find this one's. + int link = column.OwnerLink.Index; + return [.. OpenTable(column.FlatTable.Name) + .RowsWhere([link], row => row[link] is { } owner + && Convert.ToInt32(owner, System.Globalization.CultureInfo.InvariantCulture) == complexId) + .Select(r => (object?[])[.. column.ValueColumns.Select(c => r.Values[c.Index])])]; } /// diff --git a/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs b/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs index 387f45a8..aa7f84e7 100644 --- a/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs @@ -69,7 +69,8 @@ public void AddPermissionRows(int objectId, int containerId) (byte[] users, _, byte[] creator) = catalog.SecuritySids; var grants = new List<(byte[] Sid, int Acm)>(); - foreach (object?[] row in new Table(channel, msysAces).Rows()) + var aces = new Table(channel, msysAces); + foreach (object?[] row in aces.Rows(aces.DecodeOnly([idIndex, sidIndex, acmIndex, inheritIndex]))) { if (row[idIndex] is not int id || id != containerId || row[inheritIndex] is not true) continue; byte[] sid = (byte[])row[sidIndex]!; diff --git a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs index 4450b270..0a7195dc 100644 --- a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs @@ -429,10 +429,9 @@ private static void MarkAsSystemTable(JetDatabase db, string name, int flags) int flagsIndex = objectsDef.FindColumn("Flags")!.Index; int ownerIndex = objectsDef.FindColumn("Owner")!.Index; - foreach ((RowId rowId, object?[] values) in msysObjects.Rows().WithIds()) + foreach ((RowId rowId, object?[] values) in msysObjects.RowsWhere([idIndex], row => row[idIndex] is { } id + && Convert.ToInt32(id, CultureInfo.InvariantCulture) == definition.DefinitionPage)) { - if (values[idIndex] is not { } id - || Convert.ToInt32(id, CultureInfo.InvariantCulture) != definition.DefinitionPage) continue; values[flagsIndex] = flags; values[ownerIndex] = SidEngine; msysObjects.Update(rowId, values, new HashSet { flagsIndex, ownerIndex }); diff --git a/src/LibRed/LibRed.Core/Storage/Table.cs b/src/LibRed/LibRed.Core/Storage/Table.cs index 06e5df13..da783881 100644 --- a/src/LibRed/LibRed.Core/Storage/Table.cs +++ b/src/LibRed/LibRed.Core/Storage/Table.cs @@ -27,6 +27,27 @@ public Table(PageChannel channel, TableDef definition) /// left out reads as null. For a reader that never looks at it — see . public TableCursor Rows(bool[]? decode = null) => new(this, decode); + /// A decode mask for and the seeks that reads only + /// (by ): for a reader that looks at nothing else, such as a key comparison, + /// which would otherwise decode every other column of every row it passes over for nothing. + public bool[] DecodeOnly(IEnumerable columns) + { + var mask = new bool[Definition.Columns.Count]; + foreach (int column in columns) mask[column] = true; + return mask; + } + + /// The rows whose satisfy , each read in full. + /// The search decodes only the key; a match is read again whole, because a caller looking a row up by key + /// is usually about to rewrite or delete it, which takes every value. Lazy, as is. + public IEnumerable<(RowId Id, object?[] Values)> RowsWhere(IEnumerable keyColumns, Func match) + { + foreach ((RowId id, object?[] key) in Rows(DecodeOnly(keyColumns)).WithIds()) + if (match(key)) + yield return (id, GetRow(id) + ?? throw new InvalidDataException($"Row {id.Page}:{id.Row} of '{Name}' was read a moment ago and is gone.")); + } + /// A row decoder over this table's columns — reuse one across a seek/scan rather than allocating /// per row (each carries a shared ). private RowDecoder NewDecoder(bool[]? decode = null) => diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 091f80af..294c93f3 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -807,7 +807,8 @@ private void EnsureExistingRowsFitIndex( .ToArray(); var seen = new HashSet(); - foreach (object?[] values in new Table(_channel, table).Rows()) + var rows = new Table(_channel, table); + foreach (object?[] values in rows.Rows(rows.DecodeOnly(keyColumns.Select(k => k.Column.Index)))) { if (keyColumns.Any(k => values[k.Column.Index] is null)) { @@ -853,7 +854,10 @@ private void BackfillIndex(string tableName, string indexName, bool ignoreNulls, { var keyColumnIds = index.Columns.Select(c => c.Column.Index).ToArray(); var entries = new List<(byte[] Key, int Pointer, bool NullKey)>(); - foreach ((RowId id, object?[] values) in new Table(_channel, table).Rows().WithIds()) + // The key is all an entry is made from, so it is all that is decoded: a wide row, or one whose memo the + // index cannot even hold, would otherwise be read whole for every entry. + var rows = new Table(_channel, table); + foreach ((RowId id, object?[] values) in rows.Rows(rows.DecodeOnly(keyColumnIds)).WithIds()) { bool hasNullKey = keyColumnIds.Any(i => values[i] is null); if (ignoreNulls && hasNullKey) continue; @@ -1303,7 +1307,8 @@ public bool DropQueryObject(string name) int nameIdx = mo.RequireColumn("Name").Index; int typeIdx = mo.RequireColumn("Type").Index; - foreach (object?[] values in new Table(_channel, mo).Rows()) + var objects = new Table(_channel, mo); + foreach (object?[] values in objects.Rows(objects.DecodeOnly([idIdx, nameIdx, typeIdx]))) if (string.Equals(values[nameIdx] as string, name, StringComparison.OrdinalIgnoreCase) && Convert.ToInt16(values[typeIdx] ?? (short)0, CultureInfo.InvariantCulture) == type) return Convert.ToInt32(values[idIdx], CultureInfo.InvariantCulture); @@ -1361,7 +1366,8 @@ private void RepointRelationshipTables(string oldName, string newName) int parentIndex = def.RequireColumn("szReferencedObject").Index; var table = new Table(_channel, def); - foreach ((RowId id, object?[] values) in table.Rows().WithIds().ToList()) + foreach ((RowId id, object?[] values) in table.RowsWhere([childIndex, parentIndex], + v => NameMatches(v[childIndex], oldName) || NameMatches(v[parentIndex], oldName)).ToList()) { var updates = new List<(int Column, object? Value)>(2); if (NameMatches(values[childIndex], oldName)) updates.Add((childIndex, newName)); @@ -1544,7 +1550,9 @@ private void RepointRelationshipColumns(string tableName, string oldName, string int parentColumn = def.RequireColumn("szReferencedColumn").Index; var table = new Table(_channel, def); - foreach ((RowId id, object?[] values) in table.Rows().WithIds().ToList()) + foreach ((RowId id, object?[] values) in table.RowsWhere([childTable, childColumn, parentTable, parentColumn], + v => (NameMatches(v[childTable], tableName) && NameMatches(v[childColumn], oldName)) + || (NameMatches(v[parentTable], tableName) && NameMatches(v[parentColumn], oldName))).ToList()) { var updates = new List<(int Column, object? Value)>(2); if (NameMatches(values[childTable], tableName) && NameMatches(values[childColumn], oldName)) @@ -1573,7 +1581,8 @@ private bool ObjectNameExists(string name, int exceptObjectId) int nameIndex = mo.RequireColumn("Name").Index; int typeIndex = mo.RequireColumn("Type").Index; - foreach (object?[] values in new Table(_channel, mo).Rows()) + var objects = new Table(_channel, mo); + foreach (object?[] values in objects.Rows(objects.DecodeOnly([idIndex, nameIndex, typeIndex]))) { if (!NameMatches(values[nameIndex], name)) continue; // Skip the object being renamed — it can't collide with itself (same-name and case-only renames). @@ -1646,9 +1655,8 @@ private void UpdateCatalogRows(string catalogTable, string keyColumn, int keyVal /// The rows of a catalog table whose key column holds , materialised /// before the caller writes any of them back. private static List<(RowId Id, object?[] Values)> RowsKeyed(Table table, int keyColumn, int keyValue) => - [.. table.Rows().WithIds() - .Where(r => r.Values[keyColumn] is not null - && Convert.ToInt32(r.Values[keyColumn], CultureInfo.InvariantCulture) == keyValue)]; + [.. table.RowsWhere([keyColumn], values => values[keyColumn] is not null + && Convert.ToInt32(values[keyColumn], CultureInfo.InvariantCulture) == keyValue)]; private void DeleteCatalogRows(string catalogTable, string keyColumn, int keyValue) { @@ -1656,11 +1664,7 @@ private void DeleteCatalogRows(string catalogTable, string keyColumn, int keyVal int idx = t.RequireColumn(keyColumn).Index; var table = new Table(_channel, t); - var rows = table.Rows().WithIds() - .Where(r => r.Values[idx] is not null - && Convert.ToInt32(r.Values[idx], CultureInfo.InvariantCulture) == keyValue) - .ToList(); - foreach ((RowId id, object?[] values) in rows) + foreach ((RowId id, object?[] values) in RowsKeyed(table, idx, keyValue)) { foreach (IndexDef index in t.RealIndexes) table.RemoveIndexEntry(index, values, id); @@ -2049,9 +2053,8 @@ private void MutateLvPropForColumn(string tableName, string columnName, Action 0 } blob) return; byte[] cleaned = PropertyBlob.RemoveOwner(blob, columnName); @@ -3200,9 +3200,8 @@ private void DeleteRelationshipRows(string name) // measured by whole-file diff: the row's space back in the page's free count and the table's row count // down). Flagging the slot alone also left the index entries standing, pointing at a dead row. var table = new Table(_channel, msys); - var rows = table.Rows().WithIds() - .Where(r => string.Equals(r.Values[nameIdx] as string, name, StringComparison.OrdinalIgnoreCase)) - .ToList(); + var rows = table.RowsWhere([nameIdx], + values => string.Equals(values[nameIdx] as string, name, StringComparison.OrdinalIgnoreCase)).ToList(); foreach ((RowId id, object?[] values) in rows) { foreach (IndexDef index in msys.RealIndexes) @@ -3693,10 +3692,7 @@ private void AddPermissionRows(int objectId) => { TableDef msysAces = _catalog.RequireTable("MSysACEs"); int idIndex = msysAces.RequireColumn("ObjectId").Index; - return [.. new Table(_channel, msysAces).Rows() - .Where(r => r[idIndex] is not null - && Convert.ToInt32(r[idIndex], CultureInfo.InvariantCulture) == objectId) - .Select(r => (object?[])r.Clone())]; + return [.. RowsKeyed(new Table(_channel, msysAces), idIndex, objectId).Select(r => r.Values)]; } /// Makes the object's only MSysACEs rows, re-pointed at it — for a diff --git a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs index a4f861c4..dde01c26 100644 --- a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs @@ -79,7 +79,8 @@ private int AllocateObject(string name, short type, int parentId, int flags) // increment from 0x80000000) in the same scan. bool relationship = parentId == CatalogFormat.RelationshipContainerParentId; int nextId = unchecked((int)0x80000000); - foreach (object?[] row in new Table(_channel, msysObjects).Rows()) + var objects = new Table(_channel, msysObjects); + foreach (object?[] row in objects.Rows(objects.DecodeOnly([idIndex, nameIndex, parentIndex]))) { if ((!relationship || row[parentIndex] is int parent && parent == parentId) && string.Equals(row[nameIndex] as string, name, StringComparison.OrdinalIgnoreCase)) diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index eb391fe9..0cb06a05 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -222,16 +222,7 @@ private bool ParentRowExists(ForeignKey fk, object?[] target) { Table parent = _database.OpenTable(fk.ReferencedTable); int[] parentCols = ReferencedColumnsOf(fk, parent); - return parent.Rows(KeyMask(parent, parentCols)).Any(row => KeyEquals(row, parentCols, target)); - } - - /// A decode mask for just of : a key comparison - /// reads nothing else, and every other column of every row it passes over would be decoded for nothing. - private static bool[] KeyMask(Table table, int[] columns) - { - var mask = new bool[table.Definition.Columns.Count]; - foreach (int column in columns) mask[column] = true; - return mask; + return parent.Rows(parent.DecodeOnly(parentCols)).Any(row => KeyEquals(row, parentCols, target)); } /// The positions the relationship's referenced columns occupy in a row of the parent table. @@ -261,10 +252,7 @@ private IEnumerable ChildRelationshipsOf(string parentTable) => Table child = _database.OpenTable(fk.Table); int[] childCols = [.. fk.Columns.Select(c => child.Definition.FindColumn(c.Column)!.Index)]; // The search reads the key alone; a match is then read whole, because the cascade rewrites or deletes it. - return [.. child.Rows(KeyMask(child, childCols)).WithIds() - .Where(r => KeyEquals(r.Values, childCols, key)) - .Select(r => (r.Id, child.GetRow(r.Id) - ?? throw new InvalidOperationException($"Row {r.Id} of '{child.Name}' vanished while it was being read.")))]; + return [.. child.RowsWhere(childCols, values => KeyEquals(values, childCols, key))]; } /// @@ -331,7 +319,7 @@ private void DeleteComplexValues(Table table, object?[] values) Table flat = _database.OpenTable(complex.FlatTable.Name); // Read for the owner link and the index keys to remove, and nothing else: Delete reads the row itself. int[] read = [complex.OwnerLink.Index, .. flat.Definition.RealIndexes.SelectMany(i => i.Columns).Select(c => c.Column.Index)]; - foreach ((RowId flatId, object?[] flatValues) in flat.Rows(KeyMask(flat, read)).WithIds().ToList()) + foreach ((RowId flatId, object?[] flatValues) in flat.Rows(flat.DecodeOnly(read)).WithIds().ToList()) { if (flatValues[complex.OwnerLink.Index] is not { } link || Convert.ToInt32(link, System.Globalization.CultureInfo.InvariantCulture) != recordId) From 94954a5280e6ceb60e4289e8d8afe06e2a77fd04 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 01:24:49 +0800 Subject: [PATCH 30/88] Record the DDL benchmarks with key-only catalog reads Co-Authored-By: Claude Opus 5.5 --- test/LibRed.Benchmarks/History.tsv | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/LibRed.Benchmarks/History.tsv b/test/LibRed.Benchmarks/History.tsv index d343b8e1..cb3d702e 100644 --- a/test/LibRed.Benchmarks/History.tsv +++ b/test/LibRed.Benchmarks/History.tsv @@ -94,3 +94,7 @@ utc commit dirty host suite benchmark params mean_us stddev_us alloc_kb 2026-09-26 17:08 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.pk_point 22.36 0.13 23.97 2026-09-26 17:08 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10_indexed 3850.75 45.49 2.63 2026-09-26 17:08 0b877f69 dirty DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10_indexed 17.93 0.49 19.30 +2026-09-26 17:20 35a85556 dirty DESKTOP-D1RBTPV DdlBenchmarks CreateEmptyDatabase - 10893.04 1326.03 10206.94 +2026-09-26 17:20 35a85556 dirty DESKTOP-D1RBTPV DdlBenchmarks CreateTable - 699.60 135.58 1504.63 +2026-09-26 17:20 35a85556 dirty DESKTOP-D1RBTPV DdlBenchmarks CreateIndexOverExistingRows - 1346.20 220.81 2045.45 +2026-09-26 17:20 35a85556 dirty DESKTOP-D1RBTPV DdlBenchmarks AddColumn - 156.54 10.89 403.62 From 108e62c2fa03f9669b5500d9b3e211bee962d6eb Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 01:42:07 +0800 Subject: [PATCH 31/88] Seek referential-integrity and complex-value lookups by their key's index Every child INSERT scanned its parent table for the referenced key - and again at commit, where the check is repeated - a cascade scanned the child table for its children, and reading or deleting a record's multi-value or attachment values scanned the column's flat table. Table.RowsWithKey seeks through an index on exactly the key's columns when the table has one and falls back to RowsWhere's scan when it does not; the key comparison still decides every row, since an index key folds case and trailing spaces and the seek only narrows. The engine's RowsHoldingKey puts each value through TryGetSeekKey first, as a query's column = value seek does, and scans when a value is not of its column's kind. A parent always has the index - its referenced columns are a primary or unique key - and a flat table's owner link is identified by its index in the first place, so both always seek. Finding one row of a 10,000-row table: 2,922 us by the key-only scan, 17 us by seek. ForeignKeySeekTests hold the seek to the rows the scan found: a parent and cascade children whose text keys differ in case and trailing spaces; three of them fail when the seek returns nothing. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/JetDatabase.cs | 5 +- src/LibRed/LibRed.Core/Storage/Table.cs | 22 ++++++ .../Execution/StatementExecutor.cs | 39 +++++++--- .../ForeignKeySeekTests.cs | 71 +++++++++++++++++++ 4 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 test/LibRed.Engine.Tests/ForeignKeySeekTests.cs diff --git a/src/LibRed/LibRed.Core/JetDatabase.cs b/src/LibRed/LibRed.Core/JetDatabase.cs index 916f6e3f..6322d2ab 100644 --- a/src/LibRed/LibRed.Core/JetDatabase.cs +++ b/src/LibRed/LibRed.Core/JetDatabase.cs @@ -265,10 +265,11 @@ public void RollbackNested() public IReadOnlyList ReadComplexValues(ComplexColumn column, int complexId) { ArgumentNullException.ThrowIfNull(column); - // Found by the owner link alone, so another record's attachments are not read to find this one's. + // Seeked by the owner link, whose index is what identifies the link column in the first place (see + // JetCatalog), so no other record's values are read to find this one's. int link = column.OwnerLink.Index; return [.. OpenTable(column.FlatTable.Name) - .RowsWhere([link], row => row[link] is { } owner + .RowsWithKey([link], [complexId], row => row[link] is { } owner && Convert.ToInt32(owner, System.Globalization.CultureInfo.InvariantCulture) == complexId) .Select(r => (object?[])[.. column.ValueColumns.Select(c => r.Values[c.Index])])]; } diff --git a/src/LibRed/LibRed.Core/Storage/Table.cs b/src/LibRed/LibRed.Core/Storage/Table.cs index da783881..29179f06 100644 --- a/src/LibRed/LibRed.Core/Storage/Table.cs +++ b/src/LibRed/LibRed.Core/Storage/Table.cs @@ -48,6 +48,28 @@ public bool[] DecodeOnly(IEnumerable columns) ?? throw new InvalidDataException($"Row {id.Page}:{id.Row} of '{Name}' was read a moment ago and is gone.")); } + /// The rows whose hold (position for position), + /// each read in full — seeked through an index on exactly those columns when the table has one, and found + /// by 's scan when it has none. decides every row either way: + /// an index key is lossy (text folds case and trailing spaces), so the seek only narrows. + /// The key has to be in each column's own kind already, because an index answers only in its + /// column's kind; a caller that cannot promise that uses . A null in the key scans, + /// since how an index keys a null is not the question a key comparison asks. + public IEnumerable<(RowId Id, object?[] Values)> RowsWithKey( + int[] columns, object?[] key, Func match) + { + IndexDef? index = key.Any(k => k is null) ? null : Definition.Indexes.FirstOrDefault(i => + i.RootPage > 0 && i.Columns.Count == columns.Length && i.Columns.All(c => columns.Contains(c.Column.Index))); + if (index is null) + return RowsWhere(columns, match); + + // A seek key is addressed by column ordinal, not by position in the index. + var seekKey = new object?[Definition.Columns.Count]; + for (int i = 0; i < columns.Length; i++) + seekKey[columns[i]] = key[i]; + return SeekRowsWithIds(index, seekKey).Where(r => match(r.Values)); + } + /// A row decoder over this table's columns — reuse one across a seek/scan rather than allocating /// per row (each carries a shared ). private RowDecoder NewDecoder(bool[]? decode = null) => diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 0cb06a05..465e9945 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -222,7 +222,27 @@ private bool ParentRowExists(ForeignKey fk, object?[] target) { Table parent = _database.OpenTable(fk.ReferencedTable); int[] parentCols = ReferencedColumnsOf(fk, parent); - return parent.Rows(parent.DecodeOnly(parentCols)).Any(row => KeyEquals(row, parentCols, target)); + return RowsHoldingKey(parent, parentCols, target).Any(); + } + + /// The rows of whose hold + /// on 's terms, read in full: seeked through an index on those columns when the key + /// can be sought (every value in its column's kind, as a query's column = value seek requires), else + /// scanned. A relationship's parent side always has one — its referenced columns are a primary or unique + /// key — so the check every child INSERT makes, which scanned the whole parent table, is a seek. + private static IEnumerable<(RowId Id, object?[] Values)> RowsHoldingKey(Table table, int[] columns, object?[] key) + { + bool Holds(object?[] values) => KeyEquals(values, columns, key); + + var seekKey = new object?[key.Length]; + for (int i = 0; i < key.Length; i++) + { + ColumnDef column = table.Definition.Columns.First(c => c.Index == columns[i]); + if (!ExpressionEvaluator.TryGetSeekKey(column, new LiteralExpression(key[i]), key[i], out seekKey[i])) + return table.RowsWhere(columns, Holds); + } + + return table.RowsWithKey(columns, seekKey, Holds); } /// The positions the relationship's referenced columns occupy in a row of the parent table. @@ -251,8 +271,9 @@ private IEnumerable ChildRelationshipsOf(string parentTable) => { Table child = _database.OpenTable(fk.Table); int[] childCols = [.. fk.Columns.Select(c => child.Definition.FindColumn(c.Column)!.Index)]; - // The search reads the key alone; a match is then read whole, because the cascade rewrites or deletes it. - return [.. child.RowsWhere(childCols, values => KeyEquals(values, childCols, key))]; + // Seeked where the child has an index on its key (ACE gives an enforced relationship one), else found by + // a scan of the key; a match is read whole either way, because the cascade rewrites or deletes it. + return [.. RowsHoldingKey(child, childCols, key)]; } /// @@ -317,14 +338,12 @@ private void DeleteComplexValues(Table table, object?[] values) int recordId = Convert.ToInt32(raw, System.Globalization.CultureInfo.InvariantCulture); Table flat = _database.OpenTable(complex.FlatTable.Name); - // Read for the owner link and the index keys to remove, and nothing else: Delete reads the row itself. - int[] read = [complex.OwnerLink.Index, .. flat.Definition.RealIndexes.SelectMany(i => i.Columns).Select(c => c.Column.Index)]; - foreach ((RowId flatId, object?[] flatValues) in flat.Rows(flat.DecodeOnly(read)).WithIds().ToList()) + // Seeked by the owner link, whose index is what identifies it (see JetCatalog). + int linkColumn = complex.OwnerLink.Index; + foreach ((RowId flatId, object?[] flatValues) in flat.RowsWithKey([linkColumn], [recordId], values => + values[linkColumn] is { } link + && Convert.ToInt32(link, System.Globalization.CultureInfo.InvariantCulture) == recordId).ToList()) { - if (flatValues[complex.OwnerLink.Index] is not { } link - || Convert.ToInt32(link, System.Globalization.CultureInfo.InvariantCulture) != recordId) - continue; - foreach (IndexDef index in flat.Definition.RealIndexes) flat.RemoveIndexEntry(index, flatValues, flatId); flat.Delete(flatId); diff --git a/test/LibRed.Engine.Tests/ForeignKeySeekTests.cs b/test/LibRed.Engine.Tests/ForeignKeySeekTests.cs new file mode 100644 index 00000000..6fa41ca0 --- /dev/null +++ b/test/LibRed.Engine.Tests/ForeignKeySeekTests.cs @@ -0,0 +1,71 @@ +using LibRed; +using LibRed.Engine; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// Referential integrity finds a parent row, and a cascade its children, through an index on the key where +/// there is one, rather than scanning the table. A seek is only a faster way to the same rows: the key +/// comparison folds case and trailing spaces, so the index — whose keys fold them too — has to reach every row +/// the scan's comparison would have accepted. These hold it to that. +/// +public class ForeignKeySeekTests : TempDatabaseTest +{ + private static QueryEngine Seeded() + { + string path = TemporaryDatabase.CopyPath( + Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "fk-seek-"); + var e = new QueryEngine(TemporaryDatabase.OpenTracked(path, readOnly: false)); + + e.ExecuteNonQuery("CREATE TABLE Par (Code TEXT(10) PRIMARY KEY, Nm TEXT(20))"); + e.ExecuteNonQuery("CREATE TABLE Ch (Id LONG PRIMARY KEY, Code TEXT(10), Info TEXT(20), " + + "CONSTRAINT fk FOREIGN KEY (Code) REFERENCES Par (Code) ON UPDATE CASCADE ON DELETE CASCADE)"); + e.ExecuteNonQuery("BEGIN TRANSACTION"); + for (int i = 0; i < 50; i++) + e.ExecuteNonQuery($"INSERT INTO Par (Code, Nm) VALUES ('P{i}', 'parent {i}')"); + e.ExecuteNonQuery("INSERT INTO Par (Code, Nm) VALUES ('ABC', 'mixed')"); + e.ExecuteNonQuery("COMMIT"); + return e; + } + + private static int Count(QueryEngine e, string sql) => Convert.ToInt32(e.ExecuteQuery(sql).Rows.Single()[0]); + + [Fact] + public void A_child_finds_its_parent_whatever_the_case_and_trailing_spaces_of_its_key() + { + QueryEngine e = Seeded(); + e.ExecuteNonQuery("INSERT INTO Ch (Id, Code, Info) VALUES (1, 'abc', 'lower')"); + e.ExecuteNonQuery("INSERT INTO Ch (Id, Code, Info) VALUES (2, 'Abc ', 'padded')"); + e.ExecuteNonQuery("INSERT INTO Ch (Id, Code, Info) VALUES (3, 'P7', 'plain')"); + Assert.Equal(3, Count(e, "SELECT COUNT(*) FROM Ch")); + } + + [Fact] + public void A_child_with_no_parent_is_still_refused() + => Assert.ThrowsAny(() => Seeded().ExecuteNonQuery("INSERT INTO Ch (Id, Code, Info) VALUES (1, 'P99', 'x')")); + + [Fact] + public void A_cascade_reaches_every_child_its_key_comparison_matches() + { + QueryEngine e = Seeded(); + e.ExecuteNonQuery("INSERT INTO Ch (Id, Code, Info) VALUES (1, 'abc', 'lower')"); + e.ExecuteNonQuery("INSERT INTO Ch (Id, Code, Info) VALUES (2, 'ABC ', 'padded')"); + e.ExecuteNonQuery("INSERT INTO Ch (Id, Code, Info) VALUES (3, 'P1', 'other')"); + + e.ExecuteNonQuery("DELETE FROM Par WHERE Code = 'ABC'"); + Assert.Equal(["P1"], e.ExecuteQuery("SELECT Code FROM Ch").Rows.Select(r => (string)r[0]!)); + } + + [Fact] + public void An_update_cascade_rewrites_the_children_it_finds_by_seek_from_all_their_values() + { + QueryEngine e = Seeded(); + e.ExecuteNonQuery("INSERT INTO Ch (Id, Code, Info) VALUES (1, 'P3', 'first')"); + e.ExecuteNonQuery("INSERT INTO Ch (Id, Code, Info) VALUES (2, 'p3', 'second')"); + + e.ExecuteNonQuery("UPDATE Par SET Code = 'Q3' WHERE Code = 'P3'"); + Assert.Equal([("Q3", "first"), ("Q3", "second")], + e.ExecuteQuery("SELECT Code, Info FROM Ch ORDER BY Id").Rows.Select(r => ((string)r[0]!, (string)r[1]!))); + } +} From 95f87f5f28dcdbad9bdb76d5538019317f337752 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 03:22:05 +0800 Subject: [PATCH 32/88] Compare text in the database's collation, by LibRed's own keys alone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since dc1c909b the evaluator compared text by General v0 collation keys, falling back to the runtime's invariant culture for anything the table did not cover, while grouping and hashing still used the invariant culture outright. So a query's answer could depend on ICU against Windows NLS, a database in any other order was compared as General v0, and a GROUP BY could disagree with '=' about which texts are one value. A query compares text in the DATABASE's collation, never a column's: measured against ACE with Croatian v1 against General v1 stamped on one side at a time, ORDER BY, GROUP BY, a comparison with a literal either way round, two literals, an expression and MIN/MAX all follow page 0 (page-02b §3.4). JetTextComparer is now one comparer per collation, built on the encoders an index key uses - v0 or v1 with the order's tailoring - and refuses an order the encoder refuses rather than answering in another. Every comparison, equality, hash and sort in the engine takes the database's comparer: the evaluator, GROUP BY and DISTINCT keys, set operations, hash joins and semi-joins, hoisted IN sets, MIN/MAX, window partitions and ordering, list and percentile aggregates, referential integrity keys. Nothing consults the runtime's culture. GROUP BY folds what the collation folds - 'ß' with 'ss', case, trailing spaces, but not an accent - as ACE does, in both General orders. The keys cover the whole BMP. General v0's measured table lacked the 66 controls, DEL and U+FEFF; ACE keys every one - NUL and U+FEFF wholly ignorable, tab to carriage return two-byte primaries 08 03-08 07, the other 60 word-sort ignorables with consecutive codes 03-3D - and the v0 encoder now does too, verified against ACE in both orders. The cost was a full encode of both sides of every comparison. Encoding reuses the thread's working lists instead of building seven per string; a sort or grouping key is encoded once per value; texts identical but for trailing spaces compare without a key; and a literal or parameter side's key is made once per evaluator. InStr's plain-text search stays only under an untailored General order, where PlainTextCollationTests proves it for both versions. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Storage/JetTextCollation.cs | 72 +++++++- .../LibRed.Core/Storage/JetTextComparer.cs | 118 +++++++++++-- .../LibRed.Engine/Execution/EvalScope.cs | 4 + .../Execution/ExpressionEvaluator.cs | 146 +++++++++++------ .../LibRed.Engine/Execution/HoistedInSet.cs | 19 +-- src/LibRed/LibRed.Engine/Execution/ListAgg.cs | 12 +- .../LibRed.Engine/Execution/Percentile.cs | 6 +- .../LibRed.Engine/Execution/QueryExecutor.cs | 142 +++++++++------- .../Execution/RunningAggregate.cs | 10 +- .../Execution/StatementExecutor.cs | 14 +- .../Execution/WindowFunctions.cs | 8 +- .../Execution/WindowPartition.cs | 5 + .../LibRed.Engine/Planning/IndexSelection.cs | 6 +- src/LibRed/docs/format/page-02b-columns.md | 8 + .../docs/format/page-03-04-index-btree.md | 12 ++ .../CollationQueryProbeTests.cs | 155 ++++++++++++++++++ .../CollationV1PatchProbeTests.cs | 30 ++-- .../ControlCharacterKeyAccessTests.cs | 84 ++++++++++ .../CollationBmpCoverageTests.cs | 38 +++++ .../PlainTextCollationTests.cs | 53 ++++++ .../CollationComparisonTests.cs | 134 +++++++++++++++ 21 files changed, 901 insertions(+), 175 deletions(-) create mode 100644 test/LibRed.Core.AccessTests/CollationQueryProbeTests.cs create mode 100644 test/LibRed.Core.AccessTests/ControlCharacterKeyAccessTests.cs create mode 100644 test/LibRed.Core.Tests/CollationBmpCoverageTests.cs create mode 100644 test/LibRed.Core.Tests/PlainTextCollationTests.cs create mode 100644 test/LibRed.Engine.Tests/CollationComparisonTests.cs diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs index abbebc07..66dcae76 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs @@ -263,6 +263,45 @@ void Add(int ligature, params int[] components) public static bool TryEncode(string value, List output, LocaleTailoring? tailoring = null) => TryEncode(value, output, tailoring, out _); + /// The word-sort inline code ACE gives a control character other than NUL and tab through carriage + /// return: the 60 of them — U+0001–0008, U+000E–001F, DEL and U+0080–009F — take consecutive codes + /// 0x03–0x3D in code-point order (measured against ACE, every one). + private static bool TryGetControlInlineCode(char c, out byte code) + { + int? value = c switch + { + >= '\u0001' and <= '\u0008' => c + 0x02, // 0x03–0x0A + >= '\u000E' and <= '\u001F' => c - 0x03, // 0x0B–0x1C + '\u007F' => 0x1D, + >= '\u0080' and <= '\u009F' => c - 0x62, // 0x1E–0x3D + _ => null, + }; + code = (byte)(value ?? 0); + return value is not null; + } + + [ThreadStatic] private static EncodeScratch? t_scratch; + + /// The lists builds a key in, + /// kept per thread and reused. + private sealed class EncodeScratch + { + public List Primaries { get; } = []; + public List Secondaries { get; } = []; + public List<(int Position, byte Code)> Inline { get; } = []; + public List Kana { get; } = []; + public List Prolonged { get; } = []; + + public void Clear() + { + Primaries.Clear(); + Secondaries.Clear(); + Inline.Clear(); + Kana.Clear(); + Prolonged.Clear(); + } + } + /// The text to encode. /// The key body is appended to this. /// Per-character overrides for a locale order other than General; null for General. @@ -278,21 +317,27 @@ public static bool TryEncode( hasWordSortRecord = false; ReadOnlySpan s = value.AsSpan().TrimEnd(' '); + // The working lists are this thread's, cleared rather than allocated: a text comparison encodes both + // sides every time, and building five lists per string made a GROUP BY over a text column spend most of + // its time collecting them. Safe because nothing here re-enters TryEncode. + EncodeScratch scratch = t_scratch ??= new EncodeScratch(); + scratch.Clear(); + // Build the primary weight bytes and a parallel secondary weight per byte (0x02 = no accent). - var primaries = new List(); - var secondaries = new List(); + List primaries = scratch.Primaries; + List secondaries = scratch.Secondaries; // 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)>(); + List<(int Position, byte Code)> inline = scratch.Inline; // One entry per kana in the string: true for a small form. Emitted as a section of its own. - var kana = new List(); + List kana = scratch.Kana; // 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(); + List prolonged = scratch.Prolonged; // 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; @@ -318,10 +363,23 @@ public static bool TryEncode( // Neither half contributes, so both are skipped and an astral character vanishes from the key. if (char.IsSurrogate(c)) continue; + // The control characters, DEL and U+FEFF are in neither table; ACE keys all 66 (page-03-04 §10.4). + // NUL and U+FEFF vanish from the key altogether, as an astral character does. + if (c is '\0' or '') continue; + + // Tab, line feed, vertical tab, form feed and carriage return each weigh a two-byte primary. + if (c is >= '\t' and <= '\r') + { + AddWeight([0x08, (byte)(c - '\t' + 0x03)], DefaultSecondary); + continue; + } + // The 20 hand-verified ignorables first, then the 40 measured ones — 60 across the BMP, every dash - // and quotation form, the Arabic harakat, and the CJK and fullwidth punctuation. + // and quotation form, the Arabic harakat, and the CJK and fullwidth punctuation — then the 60 + // remaining controls, which are word-sort ignorables too. if (Ignorables.TryGetValue(c, out byte code) || - JetTextCollationTableV0.TryGetInlineCode(c, out code)) + JetTextCollationTableV0.TryGetInlineCode(c, out code) || + TryGetControlInlineCode(c, out code)) { inline.Add((secondaries.Count, code)); lastWeight = [InlineMid, code]; diff --git a/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs b/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs index 37b729f2..d26fa9f0 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs @@ -1,22 +1,116 @@ +using System.Collections.Concurrent; using System.Runtime.InteropServices; +using LibRed.Catalog; namespace LibRed.Storage; /// -/// Text compared in Jet's General sort order — the order ACE's text index keys hold, and the one it compares text in -/// (case folded, accents significant, ß as ss, hyphens and apostrophes weighed only after the letters, -/// trailing spaces ignored). See . +/// Text compared as ACE compares it: in one collation, by the keys its index encoder writes for that collation +/// (case folded, accents significant, hyphens and apostrophes weighed only after the letters, trailing spaces +/// ignored, and each locale's own letters where it has them — see ). /// -public static class JetTextComparer +/// +/// A query compares text in the database's collation — page 0's, never a column's (page-02b §3.4, +/// verified against ACE) — so an engine uses the one comparer gives for +/// . +/// Built on LibRed's own weight tables alone, which cover the whole Basic Multilingual Plane, so the answer +/// is the same on every platform: nothing here consults the runtime's culture data, whose collation differs +/// between ICU and Windows NLS and between their versions. An order the index encoder refuses is refused here +/// too, at the first comparison, rather than answered in some other order. +/// +public sealed class JetTextComparer : IEqualityComparer { - /// The sign of against , or null when either holds a character - /// the order does not cover. - public static int? Compare(string a, string b) + private static readonly ConcurrentDictionary Comparers = new(); + + /// The comparer for . + public static JetTextComparer For(Collation collation) => Comparers.GetOrAdd(collation, c => new JetTextComparer(c)); + + private readonly LocaleTailoring? _tailoring; + private readonly bool _version1; + + private JetTextComparer(Collation collation) { - var left = new List(a.Length + 4); - var right = new List(b.Length + 4); - if (!JetTextCollation.TryEncode(a, left) || !JetTextCollation.TryEncode(b, right)) - return null; + Collation = collation; + _tailoring = collation.IsIndexKeyEncodable ? JetLocaleTailoring.For(collation) : null; + _version1 = collation.Version == Collation.GeneralVersion; + } + + /// The collation this compares in. + public Collation Collation { get; } + + /// Whether this is a General order with nothing tailored — no letter of its own and no reversed + /// accents — so plain ASCII compares as it does in General (see PlainTextCollationTests). A locale + /// that tailors nothing counts: its keys are General's. + public bool IsUntailoredGeneral => + Collation.IsIndexKeyEncodable && _tailoring is null or { Entries.Count: 0, ReverseDiacritics: false }; + + // Both sides are encoded into this thread's buffers: a comparison is made per row, or per pair in a sort, + // and fresh lists each time were most of what a text GROUP BY allocated. + [ThreadStatic] private static List? t_left; + [ThreadStatic] private static List? t_right; + + /// The key sorts by. Two keys compare, byte for byte, as + /// compares their texts, so a sort can build each value's key once. + public byte[] Key(string text) + { + List key = t_left ??= []; + Encode(text, key); + return [.. key]; + } + + /// The sign of against . + public int Compare(string a, string b) + { + List left = t_left ??= [], right = t_right ??= []; + Encode(a, left); + Encode(b, right); return Math.Sign(CollectionsMarshal.AsSpan(left).SequenceCompareTo(CollectionsMarshal.AsSpan(right))); } -} \ No newline at end of file + + /// The sign of against the text whose is + /// — for a side compared with many values, whose key is made once. + public int Compare(string a, byte[] bKey) + { + List left = t_left ??= []; + Encode(a, left); + return Math.Sign(CollectionsMarshal.AsSpan(left).SequenceCompareTo(bKey)); + } + + /// Whether the two texts are one value in this collation. + /// Texts identical once trailing spaces are dropped are equal in any order, so they are answered + /// without encoding either. + public bool Equals(string? x, string? y) + { + if (x is null || y is null) return x is null && y is null; + return x.AsSpan().TrimEnd(' ').SequenceEqual(y.AsSpan().TrimEnd(' ')) || Compare(x, y) == 0; + } + + /// A hash that agrees with : taken over the key. + public int GetHashCode(string text) + { + List key = t_left ??= []; + Encode(text, key); + var hash = new HashCode(); + hash.AddBytes(CollectionsMarshal.AsSpan(key)); + return hash.ToHashCode(); + } + + private void Encode(string text, List output) + { + if (!Collation.IsIndexKeyEncodable) + throw new NotSupportedException( + $"Text is compared in the database's collation, {Collation.Order} version {Collation.Version}" + + (Collation.SortId == 0 ? "" : $" sort id {Collation.SortId}") + ", which is not implemented yet."); + + output.Clear(); + bool encoded = _version1 + ? JetTextCollationV1.TryEncode(text, output, _tailoring, out _) + : JetTextCollation.TryEncode(text, output, _tailoring, out _); + + // Both tables cover the whole BMP, and each order handles surrogates (CollationBmpCoverageTests), so + // this cannot be reached by any string; refusing is still the answer if it ever is. + if (!encoded) + throw new NotSupportedException( + $"'{text}' holds a character the {Collation.Order} v{Collation.Version} order has no weight for."); + } +} diff --git a/src/LibRed/LibRed.Engine/Execution/EvalScope.cs b/src/LibRed/LibRed.Engine/Execution/EvalScope.cs index ad6fb276..dee00c8f 100644 --- a/src/LibRed/LibRed.Engine/Execution/EvalScope.cs +++ b/src/LibRed/LibRed.Engine/Execution/EvalScope.cs @@ -128,6 +128,10 @@ public IReadOnlyList AllColumns() /// Executes a subquery, correlating it to an enclosing query's scope. internal interface IScalarSubqueryRunner { + /// How text compares in this database: its page-0 collation, which every query-time text + /// comparison uses whatever its operands (see ). + LibRed.Storage.JetTextComparer TextComparer { get; } + /// The subquery's single value, correlated to . object? ExecuteScalar(SqlStatement query, EvalScope outerScope); diff --git a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs index 97b82be4..eb26da20 100644 --- a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs +++ b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs @@ -45,6 +45,9 @@ public ExpressionEvaluator Rebind(object?[] row) return this; } + /// How text compares here: the database's collation, for every comparison whatever its operands. + private JetTextComparer Text => subqueries.TextComparer; + public object? Evaluate(Expression expression) => expression switch { LiteralExpression l => l.Value, @@ -130,7 +133,7 @@ private static bool TryNiladicFunction(ColumnReference c, out object? value) foreach (object? item in items) { if (item is null) hasNull = true; - else if (Compare(val, item) == 0) { found = true; break; } + else if (Compare(val, item, Text) == 0) { found = true; break; } } } } @@ -154,7 +157,7 @@ private static bool TryNiladicFunction(ColumnReference c, out object? value) foreach (Expression itemExpr in inl.Items) { if (Evaluate(itemExpr) is not { } item) hasNull = true; - else if (CompareAsKinds(inl.Value, val, itemExpr, item) == 0) { found = true; break; } + else if (CompareAsKinds(inl.Value, val, itemExpr, item, Text) == 0) { found = true; break; } } return !found && hasNull ? null : found != inl.Negated; } @@ -167,7 +170,7 @@ private static bool TryNiladicFunction(ColumnReference c, out object? value) object? val = Evaluate(be.Value), low = Evaluate(be.Low), high = Evaluate(be.High); if (val is null || low is null || high is null) return null; - int toLow = CompareAsKinds(be.Value, val, be.Low, low), toHigh = CompareAsKinds(be.Value, val, be.High, high); + int toLow = CompareAsKinds(be.Value, val, be.Low, low, Text), toHigh = CompareAsKinds(be.Value, val, be.High, high, Text); bool inside = (toLow >= 0 && toHigh <= 0) || (toLow <= 0 && toHigh >= 0); return inside != be.Negated; } @@ -517,7 +520,7 @@ _ when RunningAggregate.Supports(name) => RunningAggregate.IsPair(name) ? (2, 2) if (left is null) return null; object? right = Evaluate(f.Arguments[1]); - return right is not null && Compare(left, right) == 0 ? null : left; + return right is not null && Compare(left, right, Text) == 0 ? null : left; } /// @@ -564,7 +567,7 @@ _ when RunningAggregate.Supports(name) => RunningAggregate.IsPair(name) ? (2, 2) object? value = Evaluate(argument); if (value is null) continue; - if (result is null || (greatest ? Compare(value, result) > 0 : Compare(value, result) < 0)) + if (result is null || (greatest ? Compare(value, result, Text) > 0 : Compare(value, result, Text) < 0)) result = value; } @@ -674,9 +677,9 @@ private static bool IsLocaleId(int lcid) /// a position and length, or (-1, 0). A textual match compares in the database sort order, so 'SS' finds 'ß' and /// the matched length can differ from 's. /// - private static (int Index, int Length) FindText(string text, string find, int start, bool binary) + private static (int Index, int Length) FindText(string text, string find, int start, bool binary, JetTextComparer order) { - if (binary || IsPlainText(text) && IsPlainText(find)) + if (binary || order.IsUntailoredGeneral && IsPlainText(text) && IsPlainText(find)) { int index = text.IndexOf(find, start, binary ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); return (index, find.Length); @@ -686,17 +689,19 @@ private static (int Index, int Length) FindText(string text, string find, int st { for (int length = shortest; length <= Math.Min(text.Length - i, find.Length * 2); length++) { - if (CompareText(text.Substring(i, length), find) == 0) + if (order.Compare(text.Substring(i, length), find) == 0) return (i, length); } } return (-1, 0); } - /// Text whose database order is plain case-insensitive order: ASCII with no hyphen or apostrophe, - /// which the order weighs apart, and no trailing space, which it ignores. + /// Text an untailored General order compares as plain case-insensitive text: printable ASCII with no + /// hyphen or apostrophe, which the order weighs apart, and no trailing space, which it ignores (held for both + /// versions by PlainTextCollationTests). A tailored order gives such letters weights of its own, so the + /// caller asks first. private static bool IsPlainText(string text) => - text.All(c => c < 0x80 && c is not ('-' or '\'')) && !text.EndsWith(' '); + text.All(c => c is >= ' ' and <= '~' and not ('-' or '\'')) && !text.EndsWith(' '); /// /// Access String(count, character): the character repeated. A text gives its first character (an empty @@ -726,7 +731,7 @@ private static bool IsPlainText(string text) => string left = ConcatText(a), right = ConcatText(b); if (binary) return Math.Sign(string.CompareOrdinal(left, right)); - int order = CompareText(left, right); + int order = Text.Compare(left, right); return order != 0 ? order : Math.Sign(TrailingSpaces(left) - TrailingSpaces(right)); static int TrailingSpaces(string s) => s.Length - s.TrimEnd(' ').Length; @@ -761,7 +766,7 @@ private static bool IsPlainText(string text) => string window = s1[..start]; // search within Left(string1, start) if (s2.Length == 0) return start; // empty needle → the effective start position int last = -1; - for ((int index, int _) = FindText(window, s2, 0, binary); index >= 0; (index, _) = FindText(window, s2, index + 1, binary)) + for ((int index, int _) = FindText(window, s2, 0, binary, Text); index >= 0; (index, _) = FindText(window, s2, index + 1, binary, Text)) last = index; return last + 1; } @@ -1545,7 +1550,7 @@ private static DateTime DateValueArgument(object v) => if (s1.Length == 0) return 0; if (s2.Length == 0) return start; if (start > s1.Length) return 0; - return FindText(s1, s2, start - 1, binary).Index + 1; + return FindText(s1, s2, start - 1, binary, Text).Index + 1; } /// Access REPLACE(string, find, replace[, start[, count[, compare]]]) — the text from start on, with @@ -1574,7 +1579,7 @@ private static DateTime DateValueArgument(object v) => int pos = 0, replaced = 0; while (true) { - (int j, int length) = count >= 0 && replaced >= count ? (-1, 0) : FindText(s, find, pos, binary); + (int j, int length) = count >= 0 && replaced >= count ? (-1, 0) : FindText(s, find, pos, binary, Text); if (j < 0) { sb.Append(s.AsSpan(pos)); break; } sb.Append(s, pos, j - pos).Append(repl); pos = j + length; @@ -2034,7 +2039,7 @@ private static DateTime InDateRange(Func compute) { bool distinct = left is null || right is null ? (left is null) != (right is null) - : CompareAsKinds(b.Left, left, b.Right, right) != 0; + : CompareAsKinds(b.Left, left, b.Right, right, Text) != 0; return distinct == (b.Operator == BinaryOperator.IsDistinctFrom); } @@ -2084,12 +2089,12 @@ private static DateTime InDateRange(Func compute) return b.Operator switch { - BinaryOperator.Equal => CompareAsKinds(b.Left, left, b.Right, right) == 0, - BinaryOperator.NotEqual => CompareAsKinds(b.Left, left, b.Right, right) != 0, - BinaryOperator.LessThan => CompareAsKinds(b.Left, left, b.Right, right) < 0, - BinaryOperator.LessThanOrEqual => CompareAsKinds(b.Left, left, b.Right, right) <= 0, - BinaryOperator.GreaterThan => CompareAsKinds(b.Left, left, b.Right, right) > 0, - BinaryOperator.GreaterThanOrEqual => CompareAsKinds(b.Left, left, b.Right, right) >= 0, + BinaryOperator.Equal => CompareOperands(b, left, right) == 0, + BinaryOperator.NotEqual => CompareOperands(b, left, right) != 0, + BinaryOperator.LessThan => CompareOperands(b, left, right) < 0, + BinaryOperator.LessThanOrEqual => CompareOperands(b, left, right) <= 0, + BinaryOperator.GreaterThan => CompareOperands(b, left, right) > 0, + BinaryOperator.GreaterThanOrEqual => CompareOperands(b, left, right) >= 0, // LIKE reads any other value as the text CStr gives it (verified vs ACE: TRUE LIKE '-1' is True). A binary // value becomes text too, so LIKE is case-insensitive over a binary column even though '=' on the same // column is byte-wise: `B LIKE 'A%'` matches both 0x4100 ('A') and 0x6100 ('a'). @@ -2289,15 +2294,50 @@ private static (object Left, object Right) Comparable(object left, object right) return (leftText ? TextAsNumber((string)left) : Serial(left), rightText ? TextAsNumber((string)right) : Serial(right)); } + /// The order of a comparison operator's two operands. Two texts compare by collation key, as + /// would compare them, with two shortcuts that change no answer: texts identical + /// once trailing spaces go are equal in any order, and the key of a literal or parameter side is made once for + /// this evaluator — which is reused across rows — rather than for every row it is compared with. + private int CompareOperands(BinaryExpression b, object left, object right) + { + if (left is string l && right is string r) + { + if (l.AsSpan().TrimEnd(' ').SequenceEqual(r.AsSpan().TrimEnd(' '))) + return 0; + if (b.Right is LiteralExpression or ParameterExpression) + return Text.Compare(l, ConstantKey(b.Right, r)); + if (b.Left is LiteralExpression or ParameterExpression) + return -Text.Compare(r, ConstantKey(b.Left, l)); + } + + return CompareAsKinds(b.Left, left, b.Right, right, Text); + } + + // Collation keys of the literal and parameter operands met so far, by node: a statement's constants do not + // change between the rows its evaluator is rebound to. + private Dictionary? _constantKeys; + + private byte[] ConstantKey(Expression constant, string text) + { + // IDE0028's only fix here is `[]`, which would drop the comparer and key the nodes structurally. +#pragma warning disable IDE0028 + _constantKeys ??= new Dictionary(ReferenceEqualityComparer.Instance); +#pragma warning restore IDE0028 + if (!_constantKeys.TryGetValue(constant, out byte[]? key)) + _constantKeys[constant] = key = Text.Key(text); + return key; + } + /// The order of two values once has brought them to a common kind. A parameter /// compared with text takes the text's type (verified vs ACE: a numeric parameter against a text column compares /// as text, so [S] > ? with 100 counts 'abc' and '11'). - private static int CompareAsKinds(Expression leftOperand, object left, Expression rightOperand, object right) + private static int CompareAsKinds( + Expression leftOperand, object left, Expression rightOperand, object right, JetTextComparer text) { if (leftOperand is ParameterExpression && right is string && left is not string) left = ConcatText(left); if (rightOperand is ParameterExpression && left is string && right is not string) right = ConcatText(right); (object l, object r) = Comparable(left, right); - return Compare(l, r); + return Compare(l, r, text); } /// The key an index seek must use for column = value to select exactly the rows the @@ -2872,7 +2912,7 @@ v is bool b ? b // For date arithmetic: a DateTime becomes its OLE Automation serial; a number is taken verbatim (as days). private static double Oa(object v) => v is DateTime d ? d.ToOADate() : Dbl(v); - private static int Compare(object left, object right) + private static int Compare(object left, object right, JetTextComparer text) { if (IsNumeric(left) && IsNumeric(right)) { @@ -2901,7 +2941,7 @@ private static int Compare(object left, object right) return CompareBytes(lb, rb); if (left is string || right is string) - return CompareText(left.ToString()!, right.ToString()!); + return text.Compare(left.ToString()!, right.ToString()!); // Dates compare by their OLE Automation serial rather than chronologically. Below the epoch // (1899-12-30) the day count is negative while the time fraction stays positive, so 1899-12-29 06:00 is @@ -2928,22 +2968,22 @@ private static int Compare(object left, object right) if (left is IComparable c && left.GetType() == right.GetType()) return c.CompareTo(right); - return CompareText(left.ToString()!, right.ToString()!); + return text.Compare(left.ToString()!, right.ToString()!); } /// Whether two non-null values are equal under the same coercions as = (used by the hash /// join to re-check a bucket candidate). Only meaningful within one type kind — see . - public static bool KeyEqual(object a, object b) => Compare(a, b) == 0; + public static bool KeyEqual(object a, object b, JetTextComparer text) => + a is string sa && b is string sb ? text.Equals(sa, sb) : Compare(a, b, text) == 0; /// A hash for a non-null join key that agrees with within a type kind: values - /// the evaluator treats as equal hash the same (numeric via double, text via Access's case-insensitive/ - /// trailing-space-trimmed collation, binary structurally). The planner only builds a hash join over - /// same-kind key columns, so this is total over the keys it actually sees. - public static int KeyHash(object v) => v switch + /// the evaluator treats as equal hash the same (numeric via double, text by its collation key, binary + /// structurally). The planner only builds a hash join over same-kind key columns, so this is total over the + /// keys it actually sees. + public static int KeyHash(object v, JetTextComparer text) => v switch { byte[] b => BinaryHash(b), - string s => System.Globalization.CultureInfo.InvariantCulture.CompareInfo - .GetHashCode(s.TrimEnd(' '), System.Globalization.CompareOptions.IgnoreCase), + string s => text.GetHashCode(s), _ when IsNumeric(v) => Dbl(v).GetHashCode(), _ => v.GetHashCode(), }; @@ -2964,27 +3004,35 @@ private static int CompareBytes(byte[] a, byte[] b) return a.Length.CompareTo(b.Length); } - /// Access text comparison, in the database sort order (): case-insensitive, - /// trailing spaces ignored, an accented letter beside its base letter but not equal to it (verified vs ACE: - /// 'é' < 'f', 'café' ≠ 'cafe'), 'ß' = 'ss', and a hyphen weighed after the letters. A - /// character that order does not cover compares case-insensitively. - // The linguistic comparison is the point: ordinal (CA1309) would put 'é' after 'z' and make 'ß' ≠ 'ss', - // neither of which is what ACE does. -#pragma warning disable CA1309 - private static int CompareText(string a, string b) => - JetTextComparer.Compare(a, b) - ?? Math.Sign(string.Compare(a.TrimEnd(' '), b.TrimEnd(' '), StringComparison.InvariantCultureIgnoreCase)); -#pragma warning restore CA1309 - - /// Orders two values for SORT (nulls first), using the same coercion as comparisons. - public static int CompareForSort(object? a, object? b) => (a, b) switch + /// Orders two values for SORT (nulls first), using the same coercion as comparisons, and text in + /// 's collation. + public static int CompareForSort(object? a, object? b, JetTextComparer text) => (a, b) switch { (null, null) => 0, (null, _) => -1, (_, null) => 1, - _ => Compare(a, b), + // Two sort keys compare their collation keys, which is what comparing the texts would encode again; + // one against anything else unwraps to its text and compares as text always does. + (CollatedText x, CollatedText y) => Math.Sign(x.Key.AsSpan().SequenceCompareTo(y.Key)), + (CollatedText x, _) => CompareForSort(x.Text, b, text), + (_, CollatedText y) => CompareForSort(a, y.Text, text), + _ => Compare(a, b, text), }; + /// A value as a sort key: text carries its collation key, made once, so sorting n rows encodes n + /// strings rather than two per comparison; anything else is itself. Only for values that are compared with + /// in the same collation and never returned. + internal static object? SortKey(object? value, JetTextComparer text) => + value is string s ? new CollatedText(s, text.Key(s)) : value; + + /// A text sort key and its collation key (). + internal sealed class CollatedText(string text, byte[] key) + { + public string Text { get; } = text; + public byte[] Key { get; } = key; + public override string ToString() => Text; + } + // Booleans count as numeric for comparison: EF maps CLR bool to a numeric (smallint) column, and // a boolean predicate (e.g. IS NOT NULL) must compare equal to that stored value. The comparison // coercions (Dec/Dbl) use Jet's convention (false = 0, true = -1) so a bool matches the numeric value diff --git a/src/LibRed/LibRed.Engine/Execution/HoistedInSet.cs b/src/LibRed/LibRed.Engine/Execution/HoistedInSet.cs index 30defc6b..ae672fb1 100644 --- a/src/LibRed/LibRed.Engine/Execution/HoistedInSet.cs +++ b/src/LibRed/LibRed.Engine/Execution/HoistedInSet.cs @@ -13,8 +13,8 @@ namespace LibRed.Engine.Execution; /// within one type kind, which is the same constraint the hash join lives under: 5 = '5' and /// 5 = 5.0, but '5' ≠ '5.0', so no single hash can agree with = across kinds. Numeric and /// text are taken because is defined to agree with -/// for exactly those (numeric via double, text via Access's -/// case-insensitive, trailing-space-trimmed collation). Everything else — mixed kinds in the body, or a probe +/// for exactly those (numeric via double, text via the database's +/// collation key). Everything else — mixed kinds in the body, or a probe /// of a different kind from the body — declines, and the caller scans the list as it always did. Declining /// costs nothing but the old behaviour; a wrong hash would silently drop matching rows. /// Dates deliberately do not qualify. The evaluator compares two DateTimes by their OLE Automation @@ -32,8 +32,6 @@ private enum Kind Text, } - private static readonly IEqualityComparer Comparer = new EvaluatorEquality(); - private readonly HashSet _values; private readonly Kind _kind; @@ -49,12 +47,13 @@ private HoistedInSet(HashSet values, Kind kind, bool hasNull) /// Builds a set over , or null when they cannot be hashed consistently /// (a kind outside , or more than one kind among them). An empty or all-null body also - /// returns null: there is nothing to accelerate, and the caller's scan of it is already trivial. - public static HoistedInSet? TryBuild(IReadOnlyList values) + /// returns null: there is nothing to accelerate, and the caller's scan of it is already trivial. Text is + /// compared in 's collation, as = compares it. + public static HoistedInSet? TryBuild(IReadOnlyList values, LibRed.Storage.JetTextComparer text) { Kind? kind = null; bool hasNull = false; - var set = new HashSet(Comparer); + var set = new HashSet(new EvaluatorEquality(text)); foreach (object? value in values) { @@ -87,11 +86,11 @@ _ when ExpressionEvaluator.IsNumeric(value) => Kind.Numeric, }; /// Equality and hashing delegated to the evaluator, so the set agrees with = exactly. - private sealed class EvaluatorEquality : IEqualityComparer + private sealed class EvaluatorEquality(LibRed.Storage.JetTextComparer text) : IEqualityComparer { public new bool Equals(object? a, object? b) => - a is not null && b is not null && ExpressionEvaluator.KeyEqual(a, b); + a is not null && b is not null && ExpressionEvaluator.KeyEqual(a, b, text); - public int GetHashCode(object value) => ExpressionEvaluator.KeyHash(value); + public int GetHashCode(object value) => ExpressionEvaluator.KeyHash(value, text); } } \ No newline at end of file diff --git a/src/LibRed/LibRed.Engine/Execution/ListAgg.cs b/src/LibRed/LibRed.Engine/Execution/ListAgg.cs index 6323ee10..ad3bd5fe 100644 --- a/src/LibRed/LibRed.Engine/Execution/ListAgg.cs +++ b/src/LibRed/LibRed.Engine/Execution/ListAgg.cs @@ -13,17 +13,17 @@ internal static class ListAgg /// The list over — each a value and its WITHIN GROUP key values — or Null when no value /// is present. Each value is written as & writes it. Rows whose keys tie keep their order. Under /// a value repeated — equal as GROUP BY takes values to be equal — is listed once, - /// where it first comes. + /// where it first comes. Text is ordered and compared in 's collation. /// public static string? Of( IEnumerable<(object? Value, object?[] Keys)> rows, string separator, IReadOnlyList directions, - bool distinct) + bool distinct, LibRed.Storage.JetTextComparer text) { var comparer = Comparer.Create((a, b) => { for (int k = 0; k < directions.Count; k++) { - int c = ExpressionEvaluator.CompareForSort(a[k], b[k]); + int c = ExpressionEvaluator.CompareForSort(a[k], b[k], text); if (c != 0) return directions[k] == SortDirection.Descending ? -c : c; } @@ -33,10 +33,10 @@ internal static class ListAgg if (distinct) { var seen = new HashSet(); - values = values.Where(v => seen.Add(new QueryExecutor.GroupKey([v]))); + values = values.Where(v => seen.Add(new QueryExecutor.GroupKey([v], text))); } - var text = values.Select(ExpressionEvaluator.ConcatText).ToList(); - return text.Count == 0 ? null : string.Join(separator, text); + var listed = values.Select(ExpressionEvaluator.ConcatText).ToList(); + return listed.Count == 0 ? null : string.Join(separator, listed); } } \ No newline at end of file diff --git a/src/LibRed/LibRed.Engine/Execution/Percentile.cs b/src/LibRed/LibRed.Engine/Execution/Percentile.cs index f4215dde..9226e72a 100644 --- a/src/LibRed/LibRed.Engine/Execution/Percentile.cs +++ b/src/LibRed/LibRed.Engine/Execution/Percentile.cs @@ -29,7 +29,9 @@ internal static class Percentile /// either side of it; text cannot be interpolated. The fraction is read as the Decimal it was written as, so /// that 0.7 of 10 values is the 7th rather than the 8th, as a Double's 7.000000000000001 would make it. /// - public static object? Of(string name, IEnumerable values, object? fraction, SortDirection direction) + public static object? Of( + string name, IEnumerable values, object? fraction, SortDirection direction, + LibRed.Storage.JetTextComparer text) { if (fraction is null) return null; @@ -38,7 +40,7 @@ internal static class Percentile throw new ArgumentException($"Invalid procedure call: a percentile fraction must be from 0 to 1, not {p}."); // A stable sort, so values that compare equal but differ — 'a' and 'A' — keep their order. - var comparer = Comparer.Create(ExpressionEvaluator.CompareForSort); + var comparer = Comparer.Create((a, b) => ExpressionEvaluator.CompareForSort(a, b, text)); var present = values.Where(v => v is not null); List sorted = direction == SortDirection.Descending ? [.. present.OrderByDescending(v => v, comparer)] diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs index dd55b2a4..b22af31b 100644 --- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs @@ -75,6 +75,14 @@ public sealed class QueryExecutor : IScalarSubqueryRunner private readonly JetDatabase _database; private readonly ParameterBag _parameters; private readonly SessionState? _session; + private LibRed.Storage.JetTextComparer? _textComparer; + + /// How text compares in this database: in its page-0 collation, for every comparison a query makes + /// (page-02b §3.4) — never a column's own, and never the runtime's culture. + internal LibRed.Storage.JetTextComparer TextComparer => + _textComparer ??= LibRed.Storage.JetTextComparer.For(_database.Collation); + + LibRed.Storage.JetTextComparer IScalarSubqueryRunner.TextComparer => TextComparer; // Every cache below is keyed by reference identity, so IDE0028 is suppressed across the block: its only // fix is a collection expression, which would drop ReferenceEqualityComparer and key these on the AST @@ -375,8 +383,8 @@ private bool TryHoist(SqlStatement query, EvalScope outerScope, Func Keys, HashSet NullTailKeys) BuildSemiJoinKeys( SelectStatement keyQuery, int keyWidth, IReadOnlyList nullSafe, bool trackNullTail = false) { - var keys = new HashSet(HashKeyComparer.Instance); - var nullTail = new HashSet(HashKeyComparer.Instance); + var keys = new HashSet(new HashKeyComparer(TextComparer)); + var nullTail = new HashSet(new HashKeyComparer(TextComparer)); var (_, rows) = Execute(SubqueryPlan(keyQuery, new EvalScope([], [], null)), null); foreach (object?[] row in rows) { @@ -424,7 +432,7 @@ private bool TryHoist(SqlStatement query, EvalScope outerScope, Func BuildGroupedAggregate( SelectStatement keyQuery, int keyWidth, IReadOnlyList nullSafe) { - var values = new Dictionary(HashKeyComparer.Instance); + var values = new Dictionary(new HashKeyComparer(TextComparer)); var (_, rows) = Execute(SubqueryPlan(keyQuery, new EvalScope([], [], null)), null); foreach (object?[] row in rows) { @@ -500,7 +508,7 @@ private bool TryHoist(SqlStatement query, EvalScope outerScope, Func ExecuteSetOp(SetOperator op, IEnumerable left, IEnumerable right) + private IEnumerable ExecuteSetOp(SetOperator op, IEnumerable left, IEnumerable right) { switch (op) { @@ -890,13 +898,13 @@ private static OutputColumn SetOperationColumn(OutputColumn left, OutputColumn r return Distinct(left.Concat(right)); case SetOperator.Intersect: { - var keep = new HashSet(right.Select(r => new GroupKey(r))); - return Distinct(left).Where(r => keep.Contains(new GroupKey(r))); + var keep = new HashSet(right.Select(r => new GroupKey(r, TextComparer))); + return Distinct(left).Where(r => keep.Contains(new GroupKey(r, TextComparer))); } case SetOperator.Except: { - var remove = new HashSet(right.Select(r => new GroupKey(r))); - return Distinct(left).Where(r => !remove.Contains(new GroupKey(r))); + var remove = new HashSet(right.Select(r => new GroupKey(r, TextComparer))); + return Distinct(left).Where(r => !remove.Contains(new GroupKey(r, TextComparer))); } default: throw new NotSupportedException($"Set operator {op} is not supported."); @@ -904,24 +912,24 @@ private static OutputColumn SetOperationColumn(OutputColumn left, OutputColumn r } /// Yields rows with duplicates removed by structural (value-wise) equality. - private static IEnumerable Distinct(IEnumerable rows) + private IEnumerable Distinct(IEnumerable rows) { var seen = new HashSet(); foreach (object?[] row in rows) - if (seen.Add(new GroupKey(row))) + if (seen.Add(new GroupKey(row, TextComparer))) yield return row; } /// Yields the first row for each distinct combination of the values at /// (the columns of the DISTINCTROW contributing tables), preserving order. - private static IEnumerable DistinctByIndexes(IEnumerable rows, int[] indexes) + private IEnumerable DistinctByIndexes(IEnumerable rows, int[] indexes) { var seen = new HashSet(); foreach (object?[] row in rows) { var key = new object?[indexes.Length]; for (int i = 0; i < indexes.Length; i++) key[i] = row[indexes[i]]; - if (seen.Add(new GroupKey(key))) + if (seen.Add(new GroupKey(key, TextComparer))) yield return row; } } @@ -1704,7 +1712,7 @@ private static HashSet ContributingQualifiers( { // Build phase: hash the build side by its keys. A row with any null key can never satisfy an // equi-join (SQL null = null is not true), so it is dropped from the table. - var table = new Dictionary>(HashKeyComparer.Instance); + var table = new Dictionary>(new HashKeyComparer(TextComparer)); 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 @@ -1804,10 +1812,8 @@ private sealed class RowIdentityComparer : IEqualityComparer /// 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 + private sealed class HashKeyComparer(LibRed.Storage.JetTextComparer text) : IEqualityComparer { - public static readonly HashKeyComparer Instance = new(); - // A null element equals only a null element. KeyEqual/KeyHash are documented for non-null keys, and for a // plain `=` correlation no null ever reaches here (such rows are dropped from the build and short-circuit // on probe). A null-safe correlation — EF's `a = b OR (a IS NULL AND b IS NULL)` — does hash nulls, so the @@ -1824,7 +1830,7 @@ public bool Equals(object?[]? a, object?[]? b) continue; } - if (!ExpressionEvaluator.KeyEqual(a[i]!, b[i]!)) return false; + if (!ExpressionEvaluator.KeyEqual(a[i]!, b[i]!, text)) return false; } return true; } @@ -1832,7 +1838,7 @@ public bool Equals(object?[]? a, object?[]? b) public int GetHashCode(object?[] a) { var h = new HashCode(); - foreach (object? v in a) h.Add(v is null ? 0 : ExpressionEvaluator.KeyHash(v)); + foreach (object? v in a) h.Add(v is null ? 0 : ExpressionEvaluator.KeyHash(v, text)); return h.ToHashCode(); } } @@ -1887,7 +1893,7 @@ public int GetHashCode(object?[] a) var rowKeys = new object?[keys.Count]; for (var i = 0; i < keys.Count; i++) { - rowKeys[i] = Compared(eval.Evaluate(keys[i].Value), byText[i]); + rowKeys[i] = ExpressionEvaluator.SortKey(Compared(eval.Evaluate(keys[i].Value), byText[i]), TextComparer); } decorated.Add((row, rowKeys, index++)); @@ -1895,7 +1901,7 @@ public int GetHashCode(object?[] a) { // Keep only the best `max` so far. Doing this in batches (rather than per row) amortises the sort // over the rows it discards, so the list never grows past 2·max however large the input is. - Trim(decorated, keys, max); + Trim(decorated, keys, max, TextComparer); } } @@ -1914,28 +1920,32 @@ public int GetHashCode(object?[] a) int Compare((object?[] Row, object?[] Keys, int Index) a, (object?[] Row, object?[] Keys, int Index) b) { - int c = CompareEvaluatedKeys(keys, a.Keys, b.Keys); + int c = CompareEvaluatedKeys(keys, a.Keys, b.Keys, TextComparer); // Ties fall back to input order, reproducing a stable sort's result exactly. return c != 0 ? c : a.Index.CompareTo(b.Index); } - static void Trim(List<(object?[] Row, object?[] Keys, int Index)> list, IReadOnlyList keys, int max) + static void Trim( + List<(object?[] Row, object?[] Keys, int Index)> list, IReadOnlyList keys, int max, + LibRed.Storage.JetTextComparer text) { list.Sort((a, b) => { - int c = CompareEvaluatedKeys(keys, a.Keys, b.Keys); + int c = CompareEvaluatedKeys(keys, a.Keys, b.Keys, text); return c != 0 ? c : a.Index.CompareTo(b.Index); }); list.RemoveRange(max, list.Count - max); } } - /// Compares two rows' already-evaluated ORDER BY key values, honouring each key's direction. - private static int CompareEvaluatedKeys(IReadOnlyList keys, object?[] a, object?[] b) + /// Compares two rows' already-evaluated ORDER BY key values, honouring each key's direction, with + /// text in 's collation. + private static int CompareEvaluatedKeys( + IReadOnlyList keys, object?[] a, object?[] b, LibRed.Storage.JetTextComparer text) { for (var i = 0; i < keys.Count; i++) { - int c = ExpressionEvaluator.CompareForSort(a[i], b[i]); + int c = ExpressionEvaluator.CompareForSort(a[i], b[i], text); if (keys[i].Direction == SortDirection.Descending) { c = -c; @@ -2063,7 +2073,7 @@ private void ComputeWindow( for (int k = 0; k < key.Length; k++) key[k] = eval.Evaluate(fn.Over.PartitionBy[k]); - var groupKey = new GroupKey(key); + var groupKey = new GroupKey(key, TextComparer); if (!partitions.TryGetValue(groupKey, out List? members)) partitions[groupKey] = members = []; members.Add(i); @@ -2099,7 +2109,7 @@ private void ComputeWindow( if (fn.Over.OrderBy.Count > 0) members.Sort((a, b) => { - int c = CompareEvaluatedKeys(fn.Over.OrderBy, sortKeys[a], sortKeys[b]); + int c = CompareEvaluatedKeys(fn.Over.OrderBy, sortKeys[a], sortKeys[b], TextComparer); return c != 0 ? c : a.CompareTo(b); }); @@ -2109,7 +2119,7 @@ private void ComputeWindow( { // With no ORDER BY every row of the partition is a peer of every other. bool samePeer = fn.Over.OrderBy.Count == 0 - || CompareEvaluatedKeys(fn.Over.OrderBy, sortKeys[members[i - 1]], sortKeys[members[i]]) == 0; + || CompareEvaluatedKeys(fn.Over.OrderBy, sortKeys[members[i - 1]], sortKeys[members[i]], TextComparer) == 0; peerStart[i] = samePeer ? peerStart[i - 1] : i; peerOrdinal[i] = samePeer ? peerOrdinal[i - 1] : peerOrdinal[i - 1] + 1; } @@ -2123,7 +2133,7 @@ private void ComputeWindow( var output = new object?[members.Count]; def.Evaluate( - new WindowPartition(peerStart, peerOrdinal, members.Select(m => arguments[m]).ToList(), call, frameInput, + new WindowPartition(peerStart, peerOrdinal, members.Select(m => arguments[m]).ToList(), TextComparer, call, frameInput, included is null ? null : members.Select(m => included[m]).ToList()), output); @@ -2262,22 +2272,25 @@ private static void CheckFrame(WindowFunction fn, WindowFrame frame) .Select((item, i) => ExpressionEvaluator.ToResultPlaces( ExpressionEvaluator.AsColumnType(eval.Evaluate(item.Value), conversions[i].ConvertTo, currency: false), outTypes[i])) .ToArray(); - object?[] sortKeys = node.OrderBy.Select((k, i) => Compared(eval.Evaluate(k.Value), sortByText[i])).ToArray(); - object?[] groupKeys = node.GroupBy.Select((k, i) => Compared(eval.Evaluate(k), groupByText[i])).ToArray(); + // Only compared, never returned — so text keys carry their collation keys (see SortKey). + object?[] sortKeys = node.OrderBy.Select((k, i) => + ExpressionEvaluator.SortKey(Compared(eval.Evaluate(k.Value), sortByText[i]), TextComparer)).ToArray(); + object?[] groupKeys = node.GroupBy.Select((k, i) => + ExpressionEvaluator.SortKey(Compared(eval.Evaluate(k), groupByText[i]), TextComparer)).ToArray(); outRows.Add((row, sortKeys, groupKeys)); } if (node.OrderBy.Count > 0) // Stable (see SortNode): groups with equal ORDER BY keys keep their input (first-appearance) order. outRows = outRows.OrderBy(x => x, Comparer<(object?[] Row, object?[] SortKeys, object?[] GroupKeys)>.Create( - (a, b) => CompareEvaluatedKeys(node.OrderBy, a.SortKeys, b.SortKeys))).ToList(); + (a, b) => CompareEvaluatedKeys(node.OrderBy, a.SortKeys, b.SortKeys, TextComparer))).ToList(); else if (node.GroupBy.Count > 0) // No explicit ORDER BY: Access orders GROUP BY output ascending by the grouping columns. outRows.Sort((a, b) => { for (int i = 0; i < node.GroupBy.Count; i++) { - int c = ExpressionEvaluator.CompareForSort(a.GroupKeys[i], b.GroupKeys[i]); + int c = ExpressionEvaluator.CompareForSort(a.GroupKeys[i], b.GroupKeys[i], TextComparer); if (c != 0) return c; } return 0; @@ -2331,7 +2344,7 @@ private static int CountRows(IEnumerable rows) keyValues[i] = Compared(eval.Evaluate(keys[i]), byText[i]); } - var key = new GroupKey(keyValues); + var key = new GroupKey(keyValues, TextComparer); if (!groups.TryGetValue(key, out var list)) { groups[key] = list = []; @@ -2344,12 +2357,12 @@ private static int CountRows(IEnumerable rows) /// The distinct set of scalar values, using the same value-equality () as /// SELECT DISTINCT / GROUP BY, so COUNT(DISTINCT col) dedupes exactly as ACE groups. Order preserved. - private static List DistinctValues(IEnumerable values) + private List DistinctValues(IEnumerable values) { var seen = new HashSet(); var result = new List(); foreach (object? v in values) - if (seen.Add(new GroupKey([v]))) + if (seen.Add(new GroupKey([v], TextComparer))) result.Add(v); return result; } @@ -2399,7 +2412,8 @@ private static int CountRows(IEnumerable rows) }), call.Arguments.Count - keys.Count == 2 ? (string)((LiteralExpression)call.Arguments[1]).Value! : "", order, - call.Distinct); + call.Distinct, + TextComparer); } if (call.WithinGroup is { } directions) @@ -2410,7 +2424,8 @@ private static int CountRows(IEnumerable rows) return Percentile.Of(name, group.Select(r => eval.Rebind(r).Evaluate(call.Arguments[1])), eval.Rebind(group[0]).Evaluate(call.Arguments[0]), - directions[0]); + directions[0], + TextComparer); } // A binary set function reads a pair from each row; the standard gives it no DISTINCT. @@ -2418,7 +2433,7 @@ private static int CountRows(IEnumerable rows) { if (call.Distinct) throw new NotSupportedException($"{call.Name} takes no DISTINCT."); - var pair = new RunningAggregate(name, countRows: false, currency: false); + var pair = new RunningAggregate(name, countRows: false, currency: false, TextComparer); foreach (object?[] row in group) { ExpressionEvaluator rowEval = eval.Rebind(row); @@ -2433,7 +2448,7 @@ private static int CountRows(IEnumerable rows) if (call.Distinct) values = DistinctValues(values.Where(v => v is not null)); - var aggregate = new RunningAggregate(name, countRows: false, currency: IsCurrency(arg!, columns)); + var aggregate = new RunningAggregate(name, countRows: false, currency: IsCurrency(arg!, columns), TextComparer); foreach (object? value in values) aggregate.Add(value); return aggregate.Result; @@ -2495,45 +2510,48 @@ private static IEnumerable Aggregates(Expression e) /// the evaluator compares values on, and hashed on its matching . /// /// - /// Two keys are the same key when = would call them equal: text folds case and trailing spaces, - /// and a number folds across its CLR types, so a LONG 1 and a DOUBLE 1.0 arriving under one key are one - /// group. Both are measured — ACE returns a single group for either — and a - /// column alone never mixes numeric types, so the second only shows up through an expression, e.g. an - /// IIF whose arms are typed differently. + /// Two keys are the same key when = would call them equal: text is one value when the database's + /// collation says so (case and trailing spaces fold, and so does whatever else that order folds — ß + /// and ss), and a number folds across its CLR types, so a LONG 1 and a DOUBLE 1.0 arriving under one + /// key are one group. Both are measured — ACE returns a single group for either — and a column alone never + /// mixes numeric types, so the second only shows up through an expression, e.g. an IIF whose arms + /// are typed differently. /// - internal sealed class GroupKey(object?[] values) : IEquatable + internal sealed class GroupKey(object?[] values, LibRed.Storage.JetTextComparer text) : IEquatable { - private readonly object?[] _values = values; + // Text is held as its collation key, made once: every Equals and hash then works on bytes, and agrees + // with the text comparison exactly because it is that comparison's own key. + private readonly object?[] _values = [.. values.Select(v => ExpressionEvaluator.SortKey(v, text))]; + private readonly LibRed.Storage.JetTextComparer _text = text; public bool Equals(GroupKey? other) => other is not null && _values.Length == other._values.Length - && _values.Zip(other._values).All(p => KeyEquals(p.First, p.Second)); + && _values.Zip(other._values).All(p => KeyEquals(p.First, p.Second, _text)); public override bool Equals(object? obj) => Equals(obj as GroupKey); public override int GetHashCode() { var hash = new HashCode(); foreach (object? v in _values) - hash.Add(v is null ? 0 : ExpressionEvaluator.KeyHash(v)); + { + if (v is ExpressionEvaluator.CollatedText collated) + hash.AddBytes(collated.Key); + else + hash.Add(v is null ? 0 : ExpressionEvaluator.KeyHash(v, _text)); + } return hash.ToHashCode(); } // Folds within a kind, because GetHashCode partitions by kind: calling a number and its text spelling - // one key would bucket them apart and split the group anyway. Text keeps the invariant comparison the - // hash is built on rather than taking CompareForSort's database collation — the collation key - // (JetTextCollation.TryEncode) is far too heavy to build per row just to hash, and an equality the - // hash does not follow splits groups at random. Ordinal (CA1309) would be wrong for both. -#pragma warning disable CA1309 - private static bool KeyEquals(object? a, object? b) => (a, b) switch + // one key would bucket them apart and split the group anyway. + private static bool KeyEquals(object? a, object? b, LibRed.Storage.JetTextComparer text) => (a, b) switch { (null, null) => true, (null, _) or (_, null) => false, - (string sa, string sb) => - string.Equals(sa.TrimEnd(' '), sb.TrimEnd(' '), StringComparison.InvariantCultureIgnoreCase), - (string, _) or (_, string) => false, - _ => ExpressionEvaluator.KeyEqual(a, b), + (ExpressionEvaluator.CollatedText x, ExpressionEvaluator.CollatedText y) => x.Key.AsSpan().SequenceEqual(y.Key), + (ExpressionEvaluator.CollatedText, _) or (_, ExpressionEvaluator.CollatedText) => false, + _ => ExpressionEvaluator.KeyEqual(a, b, text), }; -#pragma warning restore CA1309 } private sealed class ReferenceComparer : IEqualityComparer diff --git a/src/LibRed/LibRed.Engine/Execution/RunningAggregate.cs b/src/LibRed/LibRed.Engine/Execution/RunningAggregate.cs index af8e2b8f..12ad939e 100644 --- a/src/LibRed/LibRed.Engine/Execution/RunningAggregate.cs +++ b/src/LibRed/LibRed.Engine/Execution/RunningAggregate.cs @@ -72,13 +72,17 @@ internal sealed class RunningAggregate /// Whether this is COUNT(*), which counts rows rather than values. /// Whether the argument is a Currency, which the statistical aggregates square /// exactly. - public RunningAggregate(string name, bool countRows, bool currency) + /// How MIN and MAX order text: the database's collation. + public RunningAggregate(string name, bool countRows, bool currency, LibRed.Storage.JetTextComparer text) { _name = Supports(name) ? Canonical(name) : throw new NotSupportedException($"Aggregate {name} is not supported."); _countRows = countRows; _currency = currency; + _text = text; } + private readonly LibRed.Storage.JetTextComparer _text; + /// Whether (upper case) is an aggregate this computes. public static bool Supports(string name) => ScalarNames.Contains(Canonical(name)) || IsPair(name); @@ -138,8 +142,8 @@ public void Add(object? value) { if (_extreme is null or string { Length: 0 } || (_name == "MAX" - ? ExpressionEvaluator.CompareForSort(value, _extreme) > 0 - : ExpressionEvaluator.CompareForSort(value, _extreme) < 0)) + ? ExpressionEvaluator.CompareForSort(value, _extreme, _text) > 0 + : ExpressionEvaluator.CompareForSort(value, _extreme, _text) < 0)) _extreme = value; return; } diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 465e9945..2304afce 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -214,7 +214,7 @@ private void EnforceReferentialIntegrity(string childTable, Table table, object? /// For a self-referencing FK, whether the row's own referenced-column values equal the FK /// target — i.e. the row points at itself (or at its own composite key), which satisfies the FK. - private static bool RowSatisfiesOwnKey(ForeignKey fk, Table table, object?[] values, object?[] target) => + private bool RowSatisfiesOwnKey(ForeignKey fk, Table table, object?[] values, object?[] target) => KeyEquals(values, ReferencedColumnsOf(fk, table), target); /// Scans the parent table for a row whose referenced columns equal the child key values. @@ -230,7 +230,7 @@ private bool ParentRowExists(ForeignKey fk, object?[] target) /// can be sought (every value in its column's kind, as a query's column = value seek requires), else /// scanned. A relationship's parent side always has one — its referenced columns are a primary or unique /// key — so the check every child INSERT makes, which scanned the whole parent table, is a seek. - private static IEnumerable<(RowId Id, object?[] Values)> RowsHoldingKey(Table table, int[] columns, object?[] key) + private IEnumerable<(RowId Id, object?[] Values)> RowsHoldingKey(Table table, int[] columns, object?[] key) { bool Holds(object?[] values) => KeyEquals(values, columns, key); @@ -419,12 +419,12 @@ private void CascadeParentKeyUpdate(string parentTable, object?[] oldValues, obj /// when the values are a whole row the key sits in columns of, position for /// position when they are a key already. /// Compared on the evaluator's terms rather than the CLR's, so referential integrity reaches the - /// same rows a query predicate over the same key would: a LONG 1 and a DOUBLE 1.0 are one key, and text - /// folds case and trailing spaces as Access does. - private static bool KeyEquals(object?[] values, int[]? columns, object?[] key) + /// same rows a query predicate over the same key would: a LONG 1 and a DOUBLE 1.0 are one key, and text is + /// compared in the database's collation. + private bool KeyEquals(object?[] values, int[]? columns, object?[] key) { for (int i = 0; i < key.Length; i++) - if (ExpressionEvaluator.CompareForSort(values[columns is null ? i : columns[i]], key[i]) != 0) + if (ExpressionEvaluator.CompareForSort(values[columns is null ? i : columns[i]], key[i], _scalarRunner.TextComparer) != 0) return false; return true; } @@ -1364,7 +1364,7 @@ ExpressionEvaluator Over(object?[] values) => { for (int i = 0; i < a.Length; i++) { - int order = ExpressionEvaluator.CompareForSort(a[i], b[i]); + int order = ExpressionEvaluator.CompareForSort(a[i], b[i], _scalarRunner.TextComparer); if (order != 0) return select.OrderBy[i].Direction == SortDirection.Descending ? -order : order; } diff --git a/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs b/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs index 0e3712f8..24b917e9 100644 --- a/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs +++ b/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs @@ -186,7 +186,7 @@ internal static class WindowFunctions /// equal as GROUP BY takes values to be equal. private sealed class FrameAggregate(string name, WindowPartition p) { - private readonly RunningAggregate _aggregate = new(name, countRows: p.Call.Star, currency: p.Call.Currency); + private readonly RunningAggregate _aggregate = new(name, countRows: p.Call.Star, currency: p.Call.Currency, p.Text); private readonly HashSet? _seen = p.Call.Distinct ? [] : null; public object? Result => _aggregate.Result; @@ -201,7 +201,7 @@ public void Add(int position) return; } object? value = p.Call.Star ? null : p.Argument(position, 0); - if (_seen is not null && (value is null || !_seen.Add(new QueryExecutor.GroupKey([value])))) + if (_seen is not null && (value is null || !_seen.Add(new QueryExecutor.GroupKey([value], p.Text)))) return; _aggregate.Add(value); } @@ -224,7 +224,7 @@ public void Add(int position) o[i] = i > 0 && frame == previous && Equals(p.Argument(i, 0), p.Argument(i - 1, 0)) ? o[i - 1] : Percentile.Of(name, frame.Positions().Where(p.Includes).Select(k => p.Argument(k, 1)), - p.Argument(i, 0), p.Call.WithinGroup![0]); + p.Argument(i, 0), p.Call.WithinGroup![0], p.Text); previous = frame; } }, @@ -249,7 +249,7 @@ private static void ListAggOf(WindowPartition p, object?[] o) : ListAgg.Of( frame.Positions().Where(p.Includes).Select(k => (p.Argument(k, 0), Enumerable.Range(p.ArgumentCount - keys, keys).Select(a => p.Argument(k, a)).ToArray())), - separator, directions, p.Call.Distinct); + separator, directions, p.Call.Distinct, p.Text); previous = frame; } } diff --git a/src/LibRed/LibRed.Engine/Execution/WindowPartition.cs b/src/LibRed/LibRed.Engine/Execution/WindowPartition.cs index 51d30736..5e9aabe0 100644 --- a/src/LibRed/LibRed.Engine/Execution/WindowPartition.cs +++ b/src/LibRed/LibRed.Engine/Execution/WindowPartition.cs @@ -94,13 +94,18 @@ public IEnumerable Positions() /// Per row, the position the row's peer group starts at. /// Per row, the zero-based ordinal of the row's peer group within the partition. /// Per row, the already-evaluated arguments of the window call. +/// How text orders and compares in this database. /// How the call is written; null for a plain one. /// The frame clause; null for the default frame. /// Which rows an aggregate's FILTER lets in; null when it has none. internal sealed class WindowPartition( IReadOnlyList peerStart, IReadOnlyList peerOrdinal, IReadOnlyList arguments, + LibRed.Storage.JetTextComparer text, WindowCall? call = null, WindowFrameInput? frame = null, IReadOnlyList? included = null) { + /// How text orders and compares in this database, for the aggregates and lists computed here. + public LibRed.Storage.JetTextComparer Text { get; } = text; + /// Whether an aggregate takes in the row at : its FILTER holds there. public bool Includes(int position) => included is null || included[position]; diff --git a/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs b/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs index 736cde87..2e2700e0 100644 --- a/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs +++ b/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs @@ -216,7 +216,11 @@ private static (Expression Low, Expression High)? Between(Expression conjunct, s }; if (!sameKind) return null; - return ExpressionEvaluator.CompareForSort(low, high) <= 0 + + // Text bounds are put in the index's order, which is its column's collation — the order the seek walks. + if (def.FindColumn(colName) is not { } indexed || low is string && !indexed.Collation.IsIndexKeyEncodable) + return null; + return ExpressionEvaluator.CompareForSort(low, high, LibRed.Storage.JetTextComparer.For(indexed.Collation)) <= 0 ? (between.Low, between.High) : (between.High, between.Low); } diff --git a/src/LibRed/docs/format/page-02b-columns.md b/src/LibRed/docs/format/page-02b-columns.md index 0cac8c0f..c2470299 100644 --- a/src/LibRed/docs/format/page-02b-columns.md +++ b/src/LibRed/docs/format/page-02b-columns.md @@ -288,6 +288,14 @@ as 20 bytes of raw UTF-16LE. > their own language's v0 order (measured across all known LCIDs). LibRed does **not** implement that > fallback: it is real behaviour, but on input that no supported tool can produce. > +> **A query compares text in the DATABASE's collation, never the column's.** The column's collation goes +> into its index keys and nowhere else. Verified against ACE with Croatian v1 against General v1, stamped on +> one side at a time: a column stamped Croatian in a General database sorts, groups, compares against a +> literal either way round, and yields `MIN`/`MAX` in **General** order, and a General column in a database +> stamped Croatian does all of those in **Croatian** order — as do a comparison of two literals and an +> expression over the column (`S & ''`, `UCASE(S)`). So the order every query-time text comparison uses is the +> page-0 one (`0x6E`/`0x70`/`0x71`), whatever the operands are. +> > **Format-version coupling.** Access sets the file format to the lowest version that supports the features > used, so choosing General Legacy in the UI *downgrades the file to the 2007 format*, while General (v1) > forces 2010+. But the format byte is a **ceiling**, not a fingerprint — a 2016/2019 file (bumped by BigInt diff --git a/src/LibRed/docs/format/page-03-04-index-btree.md b/src/LibRed/docs/format/page-03-04-index-btree.md index be823b62..db79a8ce 100644 --- a/src/LibRed/docs/format/page-03-04-index-btree.md +++ b/src/LibRed/docs/format/page-03-04-index-btree.md @@ -431,6 +431,18 @@ Then the value, transformed: > **General v0 covers the whole Basic Multilingual Plane.** Every character ACE stores a key for, LibRed > encodes identically. + > + > **The control characters, DEL and `U+FEFF` are outside the measured table** and follow three rules, + > verified against ACE for all 66, alone and between two letters: + > + > | | v0 key | + > |---|---| + > | `U+0000`, `U+FEFF` | wholly ignorable — no weight and no record, so `a␀b` keys as `ab` | + > | tab, LF, VT, FF, CR (`U+0009`–`000D`) | a two-byte primary `08 03`–`08 07`, default secondary | + > | the other 60 (`U+0001`–`0008`, `U+000E`–`001F`, `U+007F`, `U+0080`–`009F`) | word-sort ignorables: an inline record `80 06 `, codes `0x03`–`0x3D` consecutive in code-point order | + > + > So `U+0001` is `7F 01 01 01 01 80 07 06 03 00` alone and tab is `7F 08 03 01 00`. General v1 keys all 66 + > from the published table, also verified against ACE. > The weights are in an embedded resource (`SortKeyTableV0.bin`, 74 KB): 63,105 of them, 19,186 ignorable, > plus 40 word-sort ignorables and 276 kana — far past anything hand-maintainable. Most of v1's table can > be embedded from a published Microsoft file; v0's cannot at all, since its primaries are a Jet compaction diff --git a/test/LibRed.Core.AccessTests/CollationQueryProbeTests.cs b/test/LibRed.Core.AccessTests/CollationQueryProbeTests.cs new file mode 100644 index 00000000..81c4b4bb --- /dev/null +++ b/test/LibRed.Core.AccessTests/CollationQueryProbeTests.cs @@ -0,0 +1,155 @@ +using System.Data.OleDb; +using LibRed; +using LibRed.Catalog; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Core.Tests; + +// Which collation ACE compares text in when a query compares it — the column's, or the database's. +// +// The two agree in every file Microsoft's tools write (page-02b §3.4: the per-column collation always equals +// page 0's), so a normal file cannot tell them apart. A stamped one can: CollationV1PatchProbeTests' Stamp +// writes a collation onto one column or onto page 0 alone. Croatian v1 against General v1 is the instrument — +// an order that reorders plain letters: 'č' is a letter after 'c' and 'lj' one after 'l', so General has +// 'ča' < 'cb' and 'lja' < 'lm' and Croatian the reverse. +// +// The answer is the DATABASE's, for every comparison a query makes — ORDER BY, GROUP BY, WHERE against a +// literal either way round, literal against literal, an expression, MIN/MAX — while the column's own +// collation goes only into its index keys. No index on the columns here, so ACE's comparison answers rather +// than an index walk. +[Collection(AceCollection.Name)] +public class CollationQueryProbeTests +{ + private static readonly Collation GeneralV1 = Collation.General; + private static readonly Collation CroatianV1 = new(CollatingOrder.Croatian, Collation.GeneralVersion); + + private static readonly string[] Values = ["cb", "ča", "ca", "d", "lm", "lja", "l"]; + + private static readonly string[] GeneralOrder = ["ca", "ča", "cb", "d", "l", "lja", "lm"]; + private static readonly string[] CroatianOrder = ["ca", "cb", "ča", "d", "l", "lm", "lja"]; + + [Fact] + public void A_column_stamped_croatian_in_a_general_database_compares_as_general() + { + Dictionary ace = Probe(stampHeader: false, stampColumn: true); + AssertComparesIn(ace, GeneralOrder, below: ["ča", "ca"], literal: "general", minMax: "ca / lm"); + } + + [Fact] + public void A_general_column_in_a_database_stamped_croatian_compares_as_croatian() + { + Dictionary ace = Probe(stampHeader: true, stampColumn: false); + AssertComparesIn(ace, CroatianOrder, below: ["ca"], literal: "croatian", minMax: "ca / lja"); + } + + // GROUP BY takes values to be equal exactly where '=' does: in the collation, where 'ß' is 'ss', case folds and + // trailing spaces go — but an accent still separates. Held in both General orders. + [Theory] + [InlineData(0)] + [InlineData(Collation.GeneralVersion)] + public void Group_by_folds_what_the_collation_folds(byte version) + { + string path = TemporaryDatabase.CreatePath("collation-group-", ".accdb"); + try + { + DatabaseCreator.CreateEmpty(path, collation: new Collation(CollatingOrder.General, version)); + using OleDbConnection ace = AceTestDatabase.Open(path); + Exec(ace, "CREATE TABLE G (Id LONG, S TEXT(20))"); + string[] values = ["Straße", "STRASSE", "strasse ", "x", "X", "café", "cafe"]; + for (int i = 0; i < values.Length; i++) + { + using OleDbCommand insert = ace.CreateCommand(); + insert.CommandText = "INSERT INTO G (Id, S) VALUES (?, ?)"; + insert.Parameters.AddWithValue("i", i); + insert.Parameters.AddWithValue("s", values[i]); + insert.ExecuteNonQuery(); + } + + Assert.Equal(["1", "1", "3", "2"], Read(ace, "SELECT COUNT(*) FROM G GROUP BY S")); + } + finally { TemporaryDatabase.Delete(path); } + } + + private static void AssertComparesIn( + Dictionary ace, string[] order, string[] below, string literal, string minMax) + { + foreach (string query in (string[])["order S", "order P", "group S", "group P", "order expr"]) + Assert.Equal(order, ace[query]); + Assert.Equal([.. order.Select(v => v.ToUpperInvariant())], ace["order ucase"]); + foreach (string query in (string[])["S below", "P below", "literal above S"]) + Assert.Equal(below, ace[query]); + Assert.Equal([literal], ace["literal ča"]); + Assert.Equal([literal], ace["literal lja"]); + Assert.Equal([minMax], ace["minmax S"]); + Assert.Equal([minMax], ace["minmax P"]); + } + + private static Dictionary Probe(bool stampHeader, bool stampColumn) + { + string path = TemporaryDatabase.CreatePath("collation-query-", ".accdb"); + try + { + DatabaseCreator.CreateEmpty(path, collation: GeneralV1); + using (OleDbConnection connection = AceTestDatabase.Open(path)) + Exec(connection, "CREATE TABLE T (Id LONG, S TEXT(20), P TEXT(20))"); + + CollationV1PatchProbeTests.Stamp(path, "T", stampColumn ? "S" : null, CroatianV1, header: stampHeader); + + // The stamp landed where it was meant to, or the answer says nothing about where ACE looks. + using (var check = JetDatabase.Open(path)) + { + TableDef table = check.Catalog.FindTable("T")!; + Assert.Equal(stampHeader ? CroatianV1 : GeneralV1, check.Collation); + Assert.Equal(stampColumn ? CroatianV1 : GeneralV1, table.FindColumn("S")!.Collation); + Assert.Equal(GeneralV1, table.FindColumn("P")!.Collation); + } + + using OleDbConnection ace = AceTestDatabase.Open(path); + for (int i = 0; i < Values.Length; i++) + { + using OleDbCommand insert = ace.CreateCommand(); + insert.CommandText = "INSERT INTO T (Id, S, P) VALUES (?, ?, ?)"; + insert.Parameters.AddWithValue("i", i); + insert.Parameters.AddWithValue("s", Values[i]); + insert.Parameters.AddWithValue("p", Values[i]); + insert.ExecuteNonQuery(); + } + + return new Dictionary + { + ["order S"] = Read(ace, "SELECT S FROM T ORDER BY S"), + ["order P"] = Read(ace, "SELECT P FROM T ORDER BY P"), + ["group S"] = Read(ace, "SELECT S FROM T GROUP BY S"), + ["group P"] = Read(ace, "SELECT P FROM T GROUP BY P"), + ["order expr"] = Read(ace, "SELECT S FROM T ORDER BY S & ''"), + ["order ucase"] = Read(ace, "SELECT UCASE(S) FROM T ORDER BY UCASE(S)"), + ["S below"] = Read(ace, "SELECT S FROM T WHERE S < 'cb' ORDER BY Id"), + ["P below"] = Read(ace, "SELECT P FROM T WHERE P < 'cb' ORDER BY Id"), + ["literal above S"] = Read(ace, "SELECT S FROM T WHERE 'cb' > S ORDER BY Id"), + ["literal ča"] = Read(ace, "SELECT TOP 1 IIF('ča' < 'cb', 'general', 'croatian') FROM T"), + ["literal lja"] = Read(ace, "SELECT TOP 1 IIF('lja' < 'lm', 'general', 'croatian') FROM T"), + ["minmax S"] = Read(ace, "SELECT MIN(S) & ' / ' & MAX(S) FROM T"), + ["minmax P"] = Read(ace, "SELECT MIN(P) & ' / ' & MAX(P) FROM T"), + }; + } + finally { TemporaryDatabase.Delete(path); } + } + + private static string[] Read(OleDbConnection connection, string sql) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = sql; + var values = new List(); + using OleDbDataReader reader = command.ExecuteReader(); + while (reader.Read()) values.Add(reader.IsDBNull(0) ? "NULL" : reader.GetValue(0).ToString()!); + return [.. values]; + } + + private static void Exec(OleDbConnection connection, string sql) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = sql; + command.ExecuteNonQuery(); + } +} diff --git a/test/LibRed.Core.AccessTests/CollationV1PatchProbeTests.cs b/test/LibRed.Core.AccessTests/CollationV1PatchProbeTests.cs index 1ed722b8..0cf4f936 100644 --- a/test/LibRed.Core.AccessTests/CollationV1PatchProbeTests.cs +++ b/test/LibRed.Core.AccessTests/CollationV1PatchProbeTests.cs @@ -216,24 +216,30 @@ private Dictionary AceKeysThroughPatchedFile( finally { TemporaryDatabase.Delete(path); } } - /// Writes into the page-0 header and into one column's descriptor. - /// - private static void Stamp(string path, string tableName, string columnName, Collation target) + /// Writes into the page-0 header and into one column's descriptor — + /// or only one of the two: false leaves page 0 alone, and a null + /// leaves every column alone. + internal static void Stamp(string path, string tableName, string? columnName, Collation target, bool header = true) { using var channel = PageChannel.Open(path, readOnly: false); JetFormatBase format = channel.Format; // Page 0: LANGID at 0x6E, sort id at 0x70, version at 0x71 — but everything from 0x18 is // XOR-obfuscated with a fixed 128-byte mask, so the value has to be masked on the way in. - byte[] page0 = channel.ReadPage(0).Span.ToArray(); - byte[] mask = JetFormatBase.PageZeroHeaderMask.ToArray(); - int maskStart = JetFormatBase.PageZeroHeaderMaskStart; - ushort langId = (ushort)target.Order; - page0[0x6E] = (byte)((langId & 0xFF) ^ mask[0x6E - maskStart]); - page0[0x6F] = (byte)((langId >> 8) ^ mask[0x6F - maskStart]); - page0[0x70] = (byte)(target.SortId ^ mask[0x70 - maskStart]); - page0[0x71] = (byte)(target.Version ^ mask[0x71 - maskStart]); - channel.WritePage(0, page0); + if (header) + { + byte[] page0 = channel.ReadPage(0).Span.ToArray(); + byte[] mask = JetFormatBase.PageZeroHeaderMask.ToArray(); + int maskStart = JetFormatBase.PageZeroHeaderMaskStart; + ushort langId = (ushort)target.Order; + page0[0x6E] = (byte)((langId & 0xFF) ^ mask[0x6E - maskStart]); + page0[0x6F] = (byte)((langId >> 8) ^ mask[0x6F - maskStart]); + page0[0x70] = (byte)(target.SortId ^ mask[0x70 - maskStart]); + page0[0x71] = (byte)(target.Version ^ mask[0x71 - maskStart]); + channel.WritePage(0, page0); + } + + if (columnName is null) return; TableDef table = new JetCatalog(channel).FindTable(tableName)!; int columnIndex = table.Columns.Single(c => c.Name == columnName).Index; diff --git a/test/LibRed.Core.AccessTests/ControlCharacterKeyAccessTests.cs b/test/LibRed.Core.AccessTests/ControlCharacterKeyAccessTests.cs new file mode 100644 index 00000000..fd002e9c --- /dev/null +++ b/test/LibRed.Core.AccessTests/ControlCharacterKeyAccessTests.cs @@ -0,0 +1,84 @@ +using System.Data.OleDb; +using LibRed; +using LibRed.Catalog; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Core.Tests; + +/// +/// The control characters, DEL and U+FEFF, keyed as ACE keys them in both General orders — alone and between +/// two letters. They sit outside General Legacy's measured table, so LibRed's v0 encoder carries them as rules +/// (NUL and U+FEFF ignorable, tab to carriage return two-byte primaries, the rest word-sort records); this is +/// what holds those rules to ACE's own keys. +/// +[Collection(AceCollection.Name)] +public class ControlCharacterKeyAccessTests +{ + private static readonly int[] CodePoints = + [.. Enumerable.Range(0x00, 0x20), 0x7F, .. Enumerable.Range(0x80, 0x20), 0xFEFF]; + + [Fact] + public void General_legacy_v0_keys_them_as_ace_does() => AssertKeysMatchAce(Collation.GeneralLegacy); + + [Fact] + public void General_v1_keys_them_as_ace_does() => AssertKeysMatchAce(Collation.General); + + private static void AssertKeysMatchAce(Collation collation) + { + string[] samples = [.. CodePoints.SelectMany(c => (string[])[((char)c).ToString(), "a" + (char)c + "b"])]; + Dictionary ace = AceKeys(collation, samples); + + // U+FEFF alone does not come back as the value it was stored under, so it alone goes unmatched. + Assert.True(ace.Count >= samples.Length - 1, $"ACE returned {ace.Count} of {samples.Length} keys"); + + var column = new ColumnDef { Name = "T", Type = JetDataType.Text, Index = 0, Collation = collation }; + var wrong = ace + .Where(k => Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [k.Key])) != k.Value) + .Select(k => $"{string.Join("+", k.Key.Select(ch => $"U+{(int)ch:X4}"))}: ACE {k.Value}") + .ToList(); + Assert.True(wrong.Count == 0, string.Join("; ", wrong)); + } + + private static Dictionary AceKeys(Collation collation, string[] samples) + { + string path = TemporaryDatabase.CreatePath("control-keys-", ".accdb"); + try + { + DatabaseCreator.CreateEmpty(path, collation: collation); + using (OleDbConnection ace = AceTestDatabase.Open(path)) + { + Exec(ace, "CREATE TABLE K (Id LONG, T TEXT(10))"); + Exec(ace, "CREATE INDEX IX_K ON K (T)"); + for (int i = 0; i < samples.Length; i++) + { + using OleDbCommand insert = ace.CreateCommand(); + insert.CommandText = "INSERT INTO K (Id, T) VALUES (?, ?)"; + insert.Parameters.AddWithValue("i", i); + insert.Parameters.AddWithValue("t", samples[i]); + insert.ExecuteNonQuery(); + } + } + + using var database = JetDatabase.Open(path); + var table = database.OpenTable("K"); + IndexDef index = table.Definition.Indexes.Single(i => i.Name == "IX_K"); + ColumnDef column = table.Definition.FindColumn("T")!; + var rows = table.Rows().WithIds().ToDictionary(r => r.Id, r => r.Values); + var keys = new Dictionary(); + foreach ((byte[] stored, RowId rowId) in new IndexCursor(table.Channel, index.RootPage).RawEntries()) + if (rows.TryGetValue(rowId, out object?[]? values) && values[column.Index] is string text + && samples.Contains(text)) + keys[text] = Convert.ToHexString(stored); + return keys; + } + finally { TemporaryDatabase.Delete(path); } + } + + private static void Exec(OleDbConnection connection, string sql) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = sql; + command.ExecuteNonQuery(); + } +} diff --git a/test/LibRed.Core.Tests/CollationBmpCoverageTests.cs b/test/LibRed.Core.Tests/CollationBmpCoverageTests.cs new file mode 100644 index 00000000..d92f84d5 --- /dev/null +++ b/test/LibRed.Core.Tests/CollationBmpCoverageTests.cs @@ -0,0 +1,38 @@ +using LibRed.Storage; +using Xunit; + +namespace LibRed.Core.Tests; + +/// +/// Every code point of the Basic Multilingual Plane has a key in both General orders — alone, between letters, +/// and as a lone surrogate half. A text comparison is built on these keys and on nothing else, so a character +/// without one would leave it with no answer the collation gives. +/// +public class CollationBmpCoverageTests +{ + private static List Unencodable(Func, bool> encode) + { + var missing = new List(); + for (int c = 0; c <= 0xFFFF; c++) + if (!encode(((char)c).ToString(), []) || !encode("a" + (char)c + "b", [])) + missing.Add(c); + return missing; + } + + private static string Describe(List missing) => + $"{missing.Count} unencodable: {string.Join(" ", missing.Select(c => $"U+{c:X4}"))}"; + + [Fact] + public void General_legacy_v0_encodes_the_whole_bmp() + { + List missing = Unencodable((s, o) => JetTextCollation.TryEncode(s, o)); + Assert.True(missing.Count == 0, Describe(missing)); + } + + [Fact] + public void General_v1_encodes_the_whole_bmp() + { + List missing = Unencodable((s, o) => JetTextCollationV1.TryEncode(s, o)); + Assert.True(missing.Count == 0, Describe(missing)); + } +} diff --git a/test/LibRed.Core.Tests/PlainTextCollationTests.cs b/test/LibRed.Core.Tests/PlainTextCollationTests.cs new file mode 100644 index 00000000..f030e0c4 --- /dev/null +++ b/test/LibRed.Core.Tests/PlainTextCollationTests.cs @@ -0,0 +1,53 @@ +using LibRed.Catalog; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Core.Tests; + +/// +/// Under an untailored General order — either version — two texts of printable ASCII without a hyphen or +/// apostrophe are one value exactly when they are equal ignoring case. InStr leans on that to search such +/// text directly instead of trying every substring against the collation, and only under those orders: a tailored +/// one gives plain ASCII letters weights of their own (Czech ch, Danish aa), so nothing here holds +/// for it. Held over every string of up to two characters and a random sweep of longer ones. +/// +public class PlainTextCollationTests +{ + private static readonly char[] Plain = + [.. Enumerable.Range(0x20, 0x7F - 0x20).Select(c => (char)c).Where(c => c is not ('-' or '\''))]; + + public static TheoryData Versions => [0, Collation.GeneralVersion]; + + [Theory] + [MemberData(nameof(Versions))] + public void Plain_ascii_is_equal_exactly_when_equal_ignoring_case(byte version) + { + JetTextComparer comparer = JetTextComparer.For(new Collation(CollatingOrder.General, version)); + Assert.True(comparer.IsUntailoredGeneral); + + var random = new Random(20260927); + IEnumerable texts = Plain.Select(c => c.ToString()) + .Concat(Plain.SelectMany(a => Plain.Select(b => $"{a}{b}"))) + .Concat(Enumerable.Range(0, 100_000).Select(_ => + new string([.. Enumerable.Range(0, random.Next(1, 8)).Select(_ => Plain[random.Next(Plain.Length)])]))); + + var byKey = new Dictionary(); + var byFolded = new Dictionary(); + foreach (string text in texts.Distinct()) + { + string key = Convert.ToHexString(comparer.Key(text)), folded = text.TrimEnd(' ').ToUpperInvariant(); + if (byKey.TryGetValue(key, out string? sameKey)) + Assert.True(sameKey.TrimEnd(' ').ToUpperInvariant() == folded, $"'{sameKey}' and '{text}' share a key."); + else + byKey[key] = text; + if (byFolded.TryGetValue(folded, out string? sameText)) + Assert.True(Convert.ToHexString(comparer.Key(sameText)) == key, $"'{sameText}' and '{text}' key apart."); + else + byFolded[folded] = text; + } + } + + [Fact] + public void A_tailored_order_is_not_untailored_general() + => Assert.False(JetTextComparer.For(new Collation(CollatingOrder.Czech, 0)).IsUntailoredGeneral); +} diff --git a/test/LibRed.Engine.Tests/CollationComparisonTests.cs b/test/LibRed.Engine.Tests/CollationComparisonTests.cs new file mode 100644 index 00000000..30cea8a8 --- /dev/null +++ b/test/LibRed.Engine.Tests/CollationComparisonTests.cs @@ -0,0 +1,134 @@ +using LibRed; +using LibRed.Catalog; +using LibRed.Engine; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// Every comparison a query makes orders and equates text in the database's collation (page-02b §3.4), and only +/// by LibRed's own weight tables, so a database answers the same on every platform. Croatian v1 against General v1 +/// is the instrument, as in ACE's own measurement: 'č' is a letter after 'c' and 'lj' one after 'l', so General has +/// 'ča' < 'cb' and 'lja' < 'lm' and Croatian the reverse. The expected orders are the ones ACE returns for the +/// same values (CollationQueryProbeTests). +/// +public class CollationComparisonTests : TempDatabaseTest +{ + private static readonly Collation GeneralV1 = Collation.General; + private static readonly Collation CroatianV1 = new(CollatingOrder.Croatian, Collation.GeneralVersion); + + private static readonly string[] Values = ["cb", "ča", "ca", "d", "lm", "lja", "l"]; + + private static readonly string[] GeneralOrder = ["ca", "ča", "cb", "d", "l", "lja", "lm"]; + private static readonly string[] CroatianOrder = ["ca", "cb", "ča", "d", "l", "lm", "lja"]; + + public static TheoryData Orders => ["general", "croatian"]; + + private static (QueryEngine Engine, string[] Order) Seeded(string order) + { + Collation collation = order == "croatian" ? CroatianV1 : GeneralV1; + string path = TemporaryDatabase.CreatePath("collation-comparison-"); + DatabaseCreator.CreateEmpty(path, collation: collation); + var e = new QueryEngine(TemporaryDatabase.OpenTracked(path)); + e.ExecuteNonQuery("CREATE TABLE T (Id LONG, S TEXT(20))"); + e.ExecuteNonQuery("CREATE TABLE U (K TEXT(20), N LONG)"); + for (int i = 0; i < Values.Length; i++) + { + e.ExecuteNonQuery($"INSERT INTO T (Id, S) VALUES ({i}, '{Values[i]}')"); + e.ExecuteNonQuery($"INSERT INTO U (K, N) VALUES ('{Values[i].ToUpperInvariant()}', {i})"); + } + return (e, order == "croatian" ? CroatianOrder : GeneralOrder); + } + + private static string[] Read(QueryEngine e, string sql) => + [.. e.ExecuteQuery(sql).Rows.Select(r => r[0] is null ? "NULL" : Convert.ToString(r[0], System.Globalization.CultureInfo.InvariantCulture)!)]; + + [Theory] + [MemberData(nameof(Orders))] + public void Order_by_sorts_in_the_database_collation(string order) + { + (QueryEngine e, string[] expected) = Seeded(order); + Assert.Equal(expected, Read(e, "SELECT S FROM T ORDER BY S")); + Assert.Equal([.. expected.Reverse()], Read(e, "SELECT S FROM T ORDER BY S DESC")); + Assert.Equal(expected, Read(e, "SELECT S FROM T ORDER BY S & ''")); + Assert.Equal(expected.Take(3), Read(e, "SELECT TOP 3 S FROM T ORDER BY S")); + } + + [Theory] + [MemberData(nameof(Orders))] + public void Group_by_and_distinct_come_back_in_the_database_collation(string order) + { + (QueryEngine e, string[] expected) = Seeded(order); + Assert.Equal(expected, Read(e, "SELECT S FROM T GROUP BY S")); + Assert.Equal(expected, Read(e, "SELECT DISTINCT S FROM T ORDER BY S")); + Assert.Equal(expected, Read(e, "SELECT S FROM T UNION SELECT S FROM T ORDER BY 1")); + } + + [Theory] + [MemberData(nameof(Orders))] + public void Min_and_max_are_the_database_collations(string order) + { + (QueryEngine e, string[] expected) = Seeded(order); + Assert.Equal([$"{expected[0]} / {expected[^1]}"], Read(e, "SELECT MIN(S) & ' / ' & MAX(S) FROM T")); + } + + [Theory] + [MemberData(nameof(Orders))] + public void A_comparison_against_a_literal_either_way_round_uses_the_database_collation(string order) + { + (QueryEngine e, string[] expected) = Seeded(order); + string[] below = [.. Values.Where(v => Array.IndexOf(expected, v) < Array.IndexOf(expected, "cb"))]; + Assert.Equal(below, Read(e, "SELECT S FROM T WHERE S < 'cb' ORDER BY Id")); + Assert.Equal(below, Read(e, "SELECT S FROM T WHERE 'cb' > S ORDER BY Id")); + } + + [Theory] + [InlineData("general", "general")] + [InlineData("croatian", "croatian")] + public void Two_literals_compare_in_the_database_collation(string order, string answer) + { + (QueryEngine e, _) = Seeded(order); + Assert.Equal([answer], Read(e, "SELECT TOP 1 IIF('ča' < 'cb', 'general', 'croatian') FROM T")); + Assert.Equal([answer], Read(e, "SELECT TOP 1 IIF('lja' < 'lm', 'general', 'croatian') FROM T")); + } + + [Theory] + [MemberData(nameof(Orders))] + public void Text_equality_folds_case_in_joins_in_lists_and_lookups(string order) + { + (QueryEngine e, _) = Seeded(order); + // U holds each value upper-cased: an unindexed equi-join is a hash join, whose hash has to agree with '='. + Assert.Equal(Values.Length, Read(e, "SELECT T.S FROM T INNER JOIN U ON T.S = U.K").Length); + Assert.Equal(["ča", "lja"], Read(e, "SELECT S FROM T WHERE S IN ('ČA', 'LJA') ORDER BY Id")); + Assert.Equal(["ča"], Read(e, "SELECT S FROM T WHERE S IN (SELECT K FROM U WHERE N = 1)")); + } + + [Theory] + [InlineData(0)] + [InlineData(Collation.GeneralVersion)] + public void Group_by_folds_what_the_collation_folds(byte version) + { + // As ACE groups them (CollationQueryProbeTests): 'ß' is 'ss', case and trailing spaces fold, an accent + // separates — cafe, café, the three spellings of strasse, and x. + string path = TemporaryDatabase.CreatePath("collation-group-"); + DatabaseCreator.CreateEmpty(path, collation: new Collation(CollatingOrder.General, version)); + var e = new QueryEngine(TemporaryDatabase.OpenTracked(path)); + e.ExecuteNonQuery("CREATE TABLE G (Id LONG, S TEXT(20))"); + string[] values = ["Straße", "STRASSE", "strasse ", "x", "X", "café", "cafe"]; + for (int i = 0; i < values.Length; i++) + e.ExecuteNonQuery($"INSERT INTO G (Id, S) VALUES ({i}, '{values[i]}')"); + + Assert.Equal(["1", "1", "3", "2"], Read(e, "SELECT COUNT(*) FROM G GROUP BY S")); + Assert.Equal(["4"], Read(e, "SELECT COUNT(*) FROM (SELECT DISTINCT S FROM G)")); + } + + [Fact] + public void The_comparer_for_a_collation_orders_as_that_collation() + { + JetTextComparer croatian = JetTextComparer.For(CroatianV1); + Assert.Same(croatian, JetTextComparer.For(CroatianV1)); + Assert.Equal(-1, croatian.Compare("cb", "ča")); + Assert.Equal(1, JetTextComparer.For(GeneralV1).Compare("cb", "ča")); + } +} From e18b75aa52e8922a1efaf24e0c455b835a5b29b6 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 03:22:05 +0800 Subject: [PATCH 33/88] Look collation weights up by code point, not by binary search Weighing a character binary-searched the v0 table's main, inline and kana arrays, or v1's weights, and sliced a fresh primary array out of v0's on every lookup: about 60% of a text comparison's time. Each set is now reached through a slot per BMP code point, 128 KB apiece, and v0's primary slices are built once and kept. The keys are unchanged. A text ORDER BY over Northwind goes 159 -> 119 us, a text GROUP BY over 10,000 rows 28.5 -> 19.3 ms and a text equality filter 8.8 -> 6.8 ms - each now at or below what the invariant culture comparison cost before the collation keys replaced it. Index key encoding shares the path. Co-Authored-By: Claude Opus 5.5 --- .../Storage/JetTextCollationTableV0.cs | 45 ++++++++++++++----- .../LibRed.Core/Storage/JetTextCollationV1.cs | 17 +++++-- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollationTableV0.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollationTableV0.cs index ce007662..e9c2da0a 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollationTableV0.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollationTableV0.cs @@ -29,13 +29,20 @@ internal static class JetTextCollationTableV0 public static bool TryGet(char c, out TailoredWeight? weight) { Table table = Loaded.Value; - int index = Array.BinarySearch(table.CodePoints, c); + int index = table.Slots[c] - 1; 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]); + // Each character's primary bytes are sliced out once and kept: a text comparison weighs every character + // of both sides, and a fresh slice each time was an allocation per character. A race only builds the + // same slice twice. + byte[]? primaries = table.PrimarySlices[index]; + if (primaries is null) + { + int start = table.PrimaryOffsets[index]; + table.PrimarySlices[index] = primaries = table.Primaries[start..(start + table.Lengths[index])]; + } + weight = new TailoredWeight(primaries, table.Secondaries[index]); return true; } @@ -46,7 +53,7 @@ public static bool TryGet(char c, out TailoredWeight? weight) public static bool TryGetInlineCode(char c, out byte code) { Table table = Loaded.Value; - int index = Array.BinarySearch(table.InlineCodePoints, c); + int index = table.InlineSlots[c] - 1; code = index < 0 ? (byte)0 : table.InlineCodes[index]; return index >= 0; } @@ -66,7 +73,7 @@ public static bool TryGetInlineCode(char c, out byte code) 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); + int index = table.KanaSlots[c] - 1; if (index < 0) { sound = 0; secondary = 0; small = false; vowel = 0; return false; } sound = table.KanaSounds[index]; secondary = table.KanaSecondaries[index]; @@ -75,11 +82,25 @@ public static bool TryGetKana(char c, out byte sound, out byte secondary, out bo return true; } + /// Each of the three sets is reached through a slot per BMP code point — its entry's index plus one, + /// zero for none — rather than a binary search: every character of every text compared is looked up in all + /// three. 128 KB apiece, where the searches were most of a comparison's time. 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); + ushort[] Slots, byte[] Lengths, int[] PrimaryOffsets, byte[] Primaries, byte[] Secondaries, + ushort[] InlineSlots, byte[] InlineCodes, + ushort[] KanaSlots, byte[] KanaSounds, byte[] KanaSecondaries, byte[] KanaSmall, + byte[] KanaVowels) + { + public byte[]?[] PrimarySlices { get; } = new byte[Lengths.Length][]; + } + + private static ushort[] SlotsOf(char[] codePoints) + { + var slots = new ushort[char.MaxValue + 1]; + for (int i = 0; i < codePoints.Length; i++) + slots[codePoints[i]] = checked((ushort)(i + 1)); + return slots; + } // 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); @@ -135,8 +156,8 @@ private static Table Load() kanaCodePoints[i] = (char)codePoint; } - return new Table(codePoints, lengths, offsets, primaries, secondaries, inlineCodePoints, inlineCodes, - kanaCodePoints, kanaSounds, kanaSecondaries, kanaSmall, kanaVowels); + return new Table(SlotsOf(codePoints), lengths, offsets, primaries, secondaries, SlotsOf(inlineCodePoints), inlineCodes, + SlotsOf(kanaCodePoints), kanaSounds, kanaSecondaries, kanaSmall, kanaVowels); } private static byte[] ReadStream(BinaryReader reader) diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs index 8a6baa53..2f68fe07 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs @@ -477,15 +477,26 @@ private static int ReadVarInt(byte[] data, ref int offset) } } - /// 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. + /// Weights in parallel arrays, reached through a slot per BMP code point — the entry's index plus + /// one, zero for none. Arrays rather than a dictionary keep the table near 400 KB resident instead of several + /// MB; the slots, 128 KB of it, replaced a binary search that every character of every comparison paid. private sealed class WeightTable( ushort[] codePoints, byte[] scriptMembers, byte[] alphabetics, byte[] diacritics, Dictionary expansions) { + private readonly ushort[] _slots = Slots(codePoints); + + private static ushort[] Slots(ushort[] codePoints) + { + var slots = new ushort[char.MaxValue + 1]; + for (int i = 0; i < codePoints.Length; i++) + slots[codePoints[i]] = checked((ushort)(i + 1)); + return slots; + } + public bool TryGetWeight(char character, out byte scriptMember, out byte alphabetic, out byte diacritic) { - int index = Array.BinarySearch(codePoints, (ushort)character); + int index = _slots[character] - 1; if (index < 0) { scriptMember = alphabetic = diacritic = 0; From 396d6507bd98f3411bf59b53c17aa55117fd9145 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 03:22:06 +0800 Subject: [PATCH 34/88] Record the full sweep and the text comparison runs Co-Authored-By: Claude Opus 5.5 --- test/LibRed.Benchmarks/History.tsv | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/test/LibRed.Benchmarks/History.tsv b/test/LibRed.Benchmarks/History.tsv index cb3d702e..495b2e1b 100644 --- a/test/LibRed.Benchmarks/History.tsv +++ b/test/LibRed.Benchmarks/History.tsv @@ -98,3 +98,109 @@ utc commit dirty host suite benchmark params mean_us stddev_us alloc_kb 2026-09-26 17:20 35a85556 dirty DESKTOP-D1RBTPV DdlBenchmarks CreateTable - 699.60 135.58 1504.63 2026-09-26 17:20 35a85556 dirty DESKTOP-D1RBTPV DdlBenchmarks CreateIndexOverExistingRows - 1346.20 220.81 2045.45 2026-09-26 17:20 35a85556 dirty DESKTOP-D1RBTPV DdlBenchmarks AddColumn - 156.54 10.89 403.62 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV CatalogBenchmarks OpenAndLoadCatalog - 442.17 3.87 308.92 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV CatalogBenchmarks OpenOnly - 275.53 1.11 20.62 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV CatalogBenchmarks EnumerateSchema - 434.11 1.54 309.00 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.correlated_subquery 1427.79 7.39 1758.23 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.group_agg_join 4926.71 29.81 3187.49 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.group_by_customer 918.51 10.60 1204.66 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.left_join 1824.88 20.39 2385.76 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.like_prefix 63.01 0.38 103.92 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.order_by_text 1168.51 5.46 1154.45 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.orders_details_join 3386.70 56.17 1898.48 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.orders_scan 173.52 4.22 277.58 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.text_pk_lookup 25.40 0.17 25.06 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.three_way_join 4441.23 28.27 3330.06 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.top_10 356.02 2.24 247.39 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.unindexed_join 3561.87 67.40 4329.67 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV StorageBenchmarks FullScan Scale=10000 2874.99 14.91 4760.56 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV StorageBenchmarks PointSeek Scale=10000 1.06 0.00 1.55 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV StorageBenchmarks DuplicateKeySeek Scale=10000 4.66 0.01 5.63 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV StorageBenchmarks RangeSeek Scale=10000 43.35 0.26 49.74 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.count_distinct 3432.84 21.58 3801.79 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.distinct_rows 3760.41 48.68 3584.31 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.global 4873.54 44.60 2491.91 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_high_card 9932.56 88.29 8056.54 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_low_card 5265.49 33.28 5780.83 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_multi_agg 8775.84 111.69 8125.08 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_over_join 59409.94 1488.98 25580.25 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_text_key 103212.19 1742.52 104444.01 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.having 5572.40 64.99 6026.84 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=catalog.schema_columns 8.52 0.07 15.41 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=catalog.schema_tables 6.18 0.13 9.65 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.arithmetic 8633.77 31.18 7607.01 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.concat 2482.40 12.10 3682.75 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.date_add 2312.50 4.91 3387.22 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.date_functions 3585.92 42.46 3784.19 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.iif 1772.75 16.80 2062.52 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.math_functions 4649.28 18.74 4802.63 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.nested_iif 4176.28 10.80 3039.93 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.string_functions 4585.53 64.75 5555.90 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.cross_small 309.93 1.91 755.35 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.full_indexed 36829.20 36.32 18556.18 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.hash_unindexed 1812.36 12.73 2269.84 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.index_nested_loop 86.09 1.09 57.96 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.left_outer 15483.23 27.93 11075.39 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.self 3499.45 87.62 2911.56 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.small_probe 686.84 18.60 482.16 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.three_way 135.79 3.06 124.72 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.arithmetic 2630.59 47.07 2374.00 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.boolean 1271.17 6.66 1670.78 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.composite_eq 37.55 0.09 32.61 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.date_range 2423.57 16.74 2184.40 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.in_list 4671.08 93.41 2145.89 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.index_eq 26.09 0.06 23.81 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.index_open_range 49.85 0.13 38.65 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.index_range 63.11 0.14 42.44 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.is_null 1585.73 22.02 3097.08 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.like_contains 6764.21 9.93 12124.09 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.like_prefix 3813.77 22.01 8388.61 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.negated 2125.30 7.38 2140.19 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.or_two_indexes 17179.87 98.44 21654.10 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.pk_point 25.58 0.08 24.06 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.text_index_eq 28.18 0.44 26.81 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.unindexed_eq 2014.86 21.39 2138.51 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.count_star 814.22 14.75 1431.20 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.long_text 1717.13 31.60 3212.74 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.one_column 1039.64 14.74 1976.93 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.star 3022.02 68.18 4769.36 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.three_columns 1643.41 22.30 2840.79 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=setop.union_all 42.36 0.23 39.23 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=setop.union_distinct 574.19 4.00 485.24 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.by_indexed 10404.78 91.83 3455.19 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.by_two_columns 11570.00 129.29 3454.80 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.by_unindexed 9157.58 168.14 3455.25 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.top_10 3567.29 63.07 2292.06 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.top_10_indexed 20.41 0.36 19.33 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.top_percent 6665.59 158.79 3398.49 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sub.derived_table 5549.58 241.14 5860.23 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sub.exists_correlated 137.45 0.95 107.31 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sub.in_uncorrelated 2198.91 23.16 2598.19 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sub.not_exists 143.61 2.60 108.33 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sub.scalar_correlated 129.20 2.74 103.16 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=window.rank_partitioned 44.61 0.49 39.31 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=window.row_number 41.72 0.12 35.70 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV WriteBenchmarks InsertSqlAutocommit - 415.35 3.69 791.69 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV WriteBenchmarks InsertSqlInTransaction - 522.88 5.98 1076.52 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV WriteBenchmarks InsertStorage - 122.22 1.51 300.09 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV WriteBenchmarks InsertStorageInTransaction - 231.48 2.75 633.12 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV WriteBenchmarks UpdateByPrimaryKey - 44.78 2.63 51.37 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV WriteBenchmarks UpdateIndexedColumn - 97.00 1.17 190.46 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV WriteBenchmarks DeleteByPrimaryKey - 291.63 2.84 724.47 +2026-09-26 18:00 108e62c2 clean DESKTOP-D1RBTPV WriteBenchmarks UpdateSetBased - 440.94 8.62 545.73 +2026-09-26 18:16 108e62c2 dirty DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.order_by_text 155.96 0.80 125.03 +2026-09-26 18:16 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_text_key 21893.97 180.84 14751.64 +2026-09-26 18:16 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.or_two_indexes 14148.37 243.97 10883.46 +2026-09-26 18:16 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.by_unindexed 7225.64 111.85 3455.44 +2026-09-26 18:56 108e62c2 dirty DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.order_by_text 158.58 1.04 125.08 +2026-09-26 18:56 108e62c2 dirty DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.text_pk_lookup 23.36 0.33 24.09 +2026-09-26 18:56 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_text_key 28524.39 382.66 21080.05 +2026-09-26 18:56 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.hash_unindexed 1824.99 13.60 2270.03 +2026-09-26 18:56 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.like_contains 6851.32 15.33 12124.09 +2026-09-26 18:56 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.or_two_indexes 14699.94 37.39 10883.56 +2026-09-26 18:56 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.three_columns 1682.98 14.30 2840.80 +2026-09-26 19:00 108e62c2 dirty DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.order_by_text 158.76 0.88 125.09 +2026-09-26 19:00 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.or_two_indexes 8754.67 43.89 6981.04 +2026-09-26 19:10 108e62c2 dirty DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.order_by_text 118.78 0.15 124.28 +2026-09-26 19:10 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_text_key 19333.33 83.06 21157.63 +2026-09-26 19:10 108e62c2 dirty DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.or_two_indexes 6803.73 102.95 6981.00 From bf1ec21c941579f59732429913b1f09da6606e1a Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 04:29:55 +0800 Subject: [PATCH 35/88] Key a file by its full path alone, worked out once per open Opening and closing a database cost 275 us where it had cost 52: the key the page cache and lock manager share a file under resolved symbolic links, and was worked out four times per open and close - joining and leaving each registry - each time a directory search, a handle opened on the file and its final path read back. The resolution caught only a link to the file itself. A link to a directory above it, a bind mount, a subst drive, a UNC path to a local share and a hard link all still reach one file by two keys, so it was paying on every open for the least common of the aliases. ACE opens the path it is given and relies on the operating system's locks, which follow the file whatever path reached it; that is the route when LibRed locks through the OS too. The key is now the full path, folded where file names are case-insensitive, touching no file, and a channel works it out once and hands it to both registries. An open and close in a loop goes 329 -> 75 us. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/IO/FileIdentity.cs | 34 +++++++------------ .../LibRed.Core/IO/MonitorLockManager.cs | 12 ++++--- src/LibRed/LibRed.Core/IO/PageCache.cs | 13 ++++--- src/LibRed/LibRed.Core/IO/PageChannel.cs | 13 ++++--- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/LibRed/LibRed.Core/IO/FileIdentity.cs b/src/LibRed/LibRed.Core/IO/FileIdentity.cs index 95b1c9c7..a78e5a4d 100644 --- a/src/LibRed/LibRed.Core/IO/FileIdentity.cs +++ b/src/LibRed/LibRed.Core/IO/FileIdentity.cs @@ -7,15 +7,18 @@ namespace LibRed.IO; /// (one file's pages would be written into the other's). /// /// -/// The key is the full path, with a symbolic link resolved to its target, folded to lower case only where -/// the platform's file names are case-insensitive. Folding unconditionally is what makes Test.accdb and -/// test.accdb — two files on Linux — share one cache. -/// Two cases are knowingly not covered, both needing the operating system's own file identity (volume -/// serial plus file index, device plus inode) and so a per-platform interop this assembly does not carry: a -/// case-sensitive volume on a platform whose default is insensitive (macOS can be formatted that way), and one -/// file reached by two different paths that are not a symbolic link — a subst drive, a UNC path to a -/// local share, a hard link. Each gives one file two entries, which is the staleness case rather than the -/// cross-contamination one. +/// The key is the full path, folded to lower case only where the platform's file names are +/// case-insensitive. Folding unconditionally is what would make Test.accdb and test.accdb — two +/// files on Linux — share one cache. Working it out touches no file, which matters because every open and close +/// pays for it. +/// One file reached by two different paths gets two entries — the staleness case rather than the +/// cross-contamination one: a symbolic link to the file or to a directory above it, a bind mount, a subst +/// drive, a UNC path to a local share, a hard link, and a case-sensitive volume on a platform whose default is +/// insensitive. Only the operating system's own file identity (device plus inode, volume serial plus file index) +/// sees through all of them, and that needs per-platform interop this assembly does not carry. Resolving only a +/// link to the file itself was tried and dropped: it cost a file-system call on every open and close to catch +/// the least common of these. ACE itself opens the path it is given and relies on the operating system's locks, +/// which follow the file whatever path reached it — the route to take when LibRed locks through the OS too. /// internal static class FileIdentity { @@ -24,19 +27,6 @@ internal static class FileIdentity public static string Key(string path) { string full = Path.GetFullPath(path); - try - { - if (File.ResolveLinkTarget(full, returnFinalTarget: true) is { } target) - full = Path.GetFullPath(target.FullName); - } - catch (IOException) - { - // A broken link, or one whose chain cannot be followed: the path as given is the best key there is, - // and a registry lookup is no place to fail an open. - } - catch (UnauthorizedAccessException) - { - } return CaseInsensitiveNames ? full.ToLowerInvariant() : full; } } diff --git a/src/LibRed/LibRed.Core/IO/MonitorLockManager.cs b/src/LibRed/LibRed.Core/IO/MonitorLockManager.cs index 603ae170..25ef257a 100644 --- a/src/LibRed/LibRed.Core/IO/MonitorLockManager.cs +++ b/src/LibRed/LibRed.Core/IO/MonitorLockManager.cs @@ -43,9 +43,11 @@ public void Dispose() /// The shared manager for a file path (creating it on first use); each call must be paired with a /// . - public static MonitorLockManager Acquire(string path) + public static MonitorLockManager Acquire(string path) => AcquireKey(Key(path)); + + /// by a key has already given. + internal static MonitorLockManager AcquireKey(string key) { - string key = Key(path); lock (Registry) { if (Registry.TryGetValue(key, out var slot)) @@ -60,9 +62,11 @@ public static MonitorLockManager Acquire(string path) } /// Drops one reference; the last release disposes the manager (and its lock stripes). - public static void Release(string path) + public static void Release(string path) => ReleaseKey(Key(path)); + + /// by a key has already given. + internal static void ReleaseKey(string key) { - string key = Key(path); lock (Registry) { if (!Registry.TryGetValue(key, out var slot)) return; diff --git a/src/LibRed/LibRed.Core/IO/PageCache.cs b/src/LibRed/LibRed.Core/IO/PageCache.cs index 610ed540..b1c9318a 100644 --- a/src/LibRed/LibRed.Core/IO/PageCache.cs +++ b/src/LibRed/LibRed.Core/IO/PageCache.cs @@ -61,9 +61,12 @@ private PageCache() { } /// paired with a . The key is 's, so relative and absolute opens /// of one file share a pool (two pools for one file would reintroduce cross-handle staleness) while two files /// whose paths differ only in case, on a platform where that makes them two files, do not. - public static PageCache Acquire(string path) + public static PageCache Acquire(string path) => AcquireKey(FileIdentity.Key(path)); + + /// by a key has already given — a channel works its + /// file's key out once and uses it for every registry it joins and leaves. + internal static PageCache AcquireKey(string key) { - string key = FileIdentity.Key(path); lock (RegistryGate) { if (Registry.TryGetValue(key, out var slot)) @@ -79,9 +82,11 @@ public static PageCache Acquire(string path) /// Drops one reference to the cache for ; when the last channel closes, the /// pool is discarded so a later re-open re-reads from disk (picking up any external change). - public static void Release(string path) + public static void Release(string path) => ReleaseKey(FileIdentity.Key(path)); + + /// by a key has already given. + internal static void ReleaseKey(string key) { - string key = FileIdentity.Key(path); lock (RegistryGate) { if (!Registry.TryGetValue(key, out var slot)) return; diff --git a/src/LibRed/LibRed.Core/IO/PageChannel.cs b/src/LibRed/LibRed.Core/IO/PageChannel.cs index 6389d424..960ab454 100644 --- a/src/LibRed/LibRed.Core/IO/PageChannel.cs +++ b/src/LibRed/LibRed.Core/IO/PageChannel.cs @@ -20,6 +20,10 @@ public sealed class PageChannel : IDisposable private readonly string _path; private readonly PageCache _cache; + // The file's identity (FileIdentity.Key), worked out once: it names this file in both shared registries, on + // the way in and on the way out, and each working-out is file-system calls on every open and close. + private readonly string _identity; + // Page decryptor for a password-encrypted database (null when the file is unencrypted). Applied to // every page as it comes off disk; page 0 (the readable header) is a no-op inside the codec. private readonly IPageCodec? _codec; @@ -70,10 +74,11 @@ private PageChannel(FileStream stream, JetFormatBase format, bool readOnly, stri Format = format; _path = path; _codec = codec; + _identity = FileIdentity.Key(path); // A test may inject its own manager; otherwise share the per-path one (refcounted, released on Dispose). _ownsLocks = locks is null; - _locks = locks ?? MonitorLockManager.Acquire(path); - _cache = PageCache.Acquire(path); + _locks = locks ?? MonitorLockManager.AcquireKey(_identity); + _cache = PageCache.AcquireKey(_identity); _cache.InitFileLength(stream.Length); } @@ -707,7 +712,7 @@ public void Dispose() { if (!_readOnly) _stream.Flush(flushToDisk: false); _stream.Dispose(); - PageCache.Release(_path); // last channel on this file drops the shared pool - if (_ownsLocks) MonitorLockManager.Release(_path); // and the shared lock manager + PageCache.ReleaseKey(_identity); // last channel on this file drops the shared pool + if (_ownsLocks) MonitorLockManager.ReleaseKey(_identity); // and the shared lock manager } } \ No newline at end of file From 0fba5b7be119a4447d7ef782aae66d2fffe4b764 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 27 Sep 2026 04:37:15 +0800 Subject: [PATCH 36/88] Fold LIKE by ACE's own table, measured, instead of the runtime's casing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LIKE ignored case through ToUpperInvariant and spelt out ß and æ by hand. Asked of ACE, every BMP character against its upper and lower case and against the letters it decomposes to - 26,440 pairs - it folds 973 case pairs, a narrower set than any runtime's: not the micro sign, long s, final sigma, the Greek symbol variants, the titlecase digraphs, or the letters Unicode added later. It spells out four letters, not two: ß, æ, œ and þ, capitals likewise. LibRed matched 442 pairs ACE does not and missed 12 it does, and the runtime's casing is not the same on every platform besides. None of it depends on the collation: the same pairs match in all 405 orders LibRed can create, Turkish, Azerbaijani and Lithuanian included. So one table serves, measured by LikeFoldTableGeneratorTest and embedded as LikeFold.bin in the sort-key tables' layout, and LikeMatcher folds the value, the pattern, bracket members and range bounds through it. Both halves are read as arrays indexed by character, and text that spells nothing out folds straight into its result, so LIKE is faster than the runtime's casing made it: a %contains% filter over 10,000 rows goes 6,764 -> 5,479 us, a prefix one 3,814 -> 2,930. LikeFoldAccessTests holds LibRed to ACE over every candidate pair, and LikeTests pins the telling ones where ACE is not available. The screen across every collation is opt-in: it takes the better part of an hour. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Engine/Execution/LikeMatcher.cs | 95 +++++++++-- src/LibRed/LibRed.Engine/LibRed.Engine.csproj | 5 + .../LibRed.Engine/Resources/LikeFold.bin | Bin 0 -> 3370 bytes .../LikeCandidates.cs | 115 +++++++++++++ .../LikeCollationProbeTests.cs | 160 ++++++++++++++++++ .../LikeFoldAccessTests.cs | 28 +++ .../LikeFoldTableGeneratorTest.cs | 150 ++++++++++++++++ test/LibRed.Engine.Tests/LikeTests.cs | 37 ++++ 8 files changed, 575 insertions(+), 15 deletions(-) create mode 100644 src/LibRed/LibRed.Engine/Resources/LikeFold.bin create mode 100644 test/LibRed.Engine.AccessTests/LikeCandidates.cs create mode 100644 test/LibRed.Engine.AccessTests/LikeCollationProbeTests.cs create mode 100644 test/LibRed.Engine.AccessTests/LikeFoldAccessTests.cs create mode 100644 test/LibRed.Engine.AccessTests/LikeFoldTableGeneratorTest.cs diff --git a/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs b/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs index 3c6518ab..8e2056c3 100644 --- a/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs +++ b/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs @@ -20,9 +20,17 @@ namespace LibRed.Engine.Execution; /// [] matches nothing at all, so []] is a plain ] and [[]] the text []. The /// Access documentation gives ^ as the ANSI-92 negation, but ACE does not treat it so: [^ae] lists /// ^, a and e, and ! negates as it does in ANSI-89. -/// Case is ignored and accents are not. ß counts as ss and æ as ae, in the pattern, -/// in a bracket list and in the value, so 'aßb' LIKE 'a[s]sb' is True; _ still takes the whole -/// character. +/// Case is ignored and accents are not, by ACE's own table rather than any runtime's casing: it folds 973 +/// case pairs and leaves out what a runtime would add — the micro sign, long s, final sigma, the Greek symbol +/// variants, the titlecase digraphs and every letter Unicode added later. Four letters are spelt out: ß as +/// ss, æ as ae, œ as oe and þ as th, capitals likewise — in the +/// pattern, in a bracket list and in the value, so 'aßb' LIKE 'a[s]sb' is True; _ still takes the whole +/// character. The table is measured (LikeFoldTableGeneratorTest) and embedded, so LIKE answers the same on every +/// platform. +/// None of it depends on the database's collation: the same pairs match in every one of the 405 orders LibRed +/// can create, Turkish included (verified vs ACE). Width, kana, superscripts, hyphens and apostrophes, which the +/// collation folds or ignores, all count here, and so do trailing spaces: 'a ' LIKE 'a' is False where +/// 'a ' = 'a' is True. /// A bracket that is never closed, or a range written backwards, is an invalid pattern. It is only reported /// when the match reaches it with a character left to test: '' LIKE '[' and 'z' LIKE 'x[z-a]' are /// both False. @@ -35,14 +43,55 @@ namespace LibRed.Engine.Execution; /// internal static class LikeMatcher { - /// The two characters a character counts as (ß as ss, æ and Æ as ae), or null for itself. - private static string? Expansion(char c) => c switch + /// What each character folds to, and the characters spelt out as several — ACE's own table. + private static readonly (char[] Fold, string?[] Expansions) Table = LoadTable(); + + /// Laid out as the sort-key tables are: the two counts, then each section deflated on its own — the + /// case pairs as (character, what it folds to), the expansions as (character, letter count, letters). Both are + /// held as arrays indexed by character — the expansions only as far as the highest one — because every + /// character of every value tested is looked up in each. + private static (char[] Fold, string?[] Expansions) LoadTable() { - 'ß' => "ss", - 'æ' => "ae", - 'Æ' => "AE", - _ => null, - }; + var fold = new char[char.MaxValue + 1]; + for (int c = 0; c <= char.MaxValue; c++) fold[c] = (char)c; + var spelt = new Dictionary(); + + using Stream stream = typeof(LikeMatcher).Assembly.GetManifestResourceStream("LibRed.Engine.Resources.LikeFold.bin") + ?? throw new InvalidOperationException("The LIKE fold table resource is missing from the assembly."); + var reader = new BinaryReader(stream); + int caseCount = reader.ReadInt32(), expansionCount = reader.ReadInt32(); + var cases = new BinaryReader(new MemoryStream(Inflate(reader))); + var expansionStream = new BinaryReader(new MemoryStream(Inflate(reader))); + + for (int i = 0; i < caseCount; i++) + fold[cases.ReadUInt16()] = (char)cases.ReadUInt16(); + for (int i = 0; i < expansionCount; i++) + { + char c = (char)expansionStream.ReadUInt16(); + var letters = new char[expansionStream.ReadByte()]; + for (int k = 0; k < letters.Length; k++) letters[k] = (char)expansionStream.ReadUInt16(); + spelt[c] = new string(letters); + } + + var expansions = new string?[spelt.Count == 0 ? 0 : spelt.Keys.Max() + 1]; + foreach ((char c, string letters) in spelt) expansions[c] = letters; + return (fold, expansions); + + static byte[] Inflate(BinaryReader reader) + { + byte[] compressed = reader.ReadBytes(reader.ReadInt32()); + var output = new MemoryStream(); + using (var inflate = new System.IO.Compression.ZLibStream(new MemoryStream(compressed), System.IO.Compression.CompressionMode.Decompress)) + inflate.CopyTo(output); + return output.ToArray(); + } + } + + /// The letters a character is spelt out as, already folded (ß as SS), or null for itself. + private static string? Expansion(char c) => c < Table.Expansions.Length ? Table.Expansions[c] : null; + + /// A character folded by ACE's case table. + private static char FoldChar(char c) => Table.Fold[c]; public static bool IsMatch(string value, string pattern) { @@ -128,19 +177,35 @@ private static int[] CharacterEnds(string value, int foldedLength) private static bool Invalid(int first, string text) => first < text.Length ? throw new ArgumentException("Invalid pattern string.") : false; - /// Text as the match compares it: expansions spelt out, then upper case. Upper-casing keeps the length, - /// so positions in the folded text line up with one another. + /// Text as the match compares it: expansions spelt out, every character folded. Folding keeps the + /// length, so positions in the folded text line up with one another. private static string Fold(string text) { + // Almost no text holds a character that is spelt out, and without one the folded text is the same length: + // written straight into the result, a table read per character. + bool spelt = false; + foreach (char c in text) + { + if (Expansion(c) is not null) { spelt = true; break; } + } + if (!spelt) + { + return string.Create(text.Length, text, static (span, source) => + { + char[] fold = Table.Fold; + for (int i = 0; i < span.Length; i++) span[i] = fold[source[i]]; + }); + } + var folded = new System.Text.StringBuilder(text.Length); foreach (char c in text) { if (Expansion(c) is { } expansion) folded.Append(expansion); else - folded.Append(c); + folded.Append(FoldChar(c)); } - return folded.ToString().ToUpperInvariant(); + return folded.ToString(); } private sealed class BracketList(bool negated, List members, List<(char Low, char High)> ranges) @@ -160,7 +225,7 @@ private sealed class BracketList(bool negated, List members, List<(char { if (body[i] > body[i + 2]) return null; - ranges.Add((char.ToUpperInvariant(body[i]), char.ToUpperInvariant(body[i + 2]))); + ranges.Add((FoldChar(body[i]), FoldChar(body[i + 2]))); i += 2; } else diff --git a/src/LibRed/LibRed.Engine/LibRed.Engine.csproj b/src/LibRed/LibRed.Engine/LibRed.Engine.csproj index f4084673..da2d8d09 100644 --- a/src/LibRed/LibRed.Engine/LibRed.Engine.csproj +++ b/src/LibRed/LibRed.Engine/LibRed.Engine.csproj @@ -13,6 +13,11 @@ + + + + + + + diff --git a/src/LibRed/LibRed.Core/Resources/Cjk/ChinesePronunciation.bin b/src/LibRed/LibRed.Core/Resources/Cjk/ChinesePronunciation.bin new file mode 100644 index 0000000000000000000000000000000000000000..c6d191fa6c21b7b7b6e168fb3eeafc951e1ae944 GIT binary patch literal 67223 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 Rd=kuns{J``{twB~?^pDE=Y0SG literal 0 HcmV?d00001 diff --git a/src/LibRed/LibRed.Core/Resources/Cjk/ChinesePronunciationLegacy.bin b/src/LibRed/LibRed.Core/Resources/Cjk/ChinesePronunciationLegacy.bin new file mode 100644 index 0000000000000000000000000000000000000000..2232f7d9e1aa8d13e4b7b7e1077b767c8b781371 GIT binary patch literal 40396 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 literal 0 HcmV?d00001 diff --git a/src/LibRed/LibRed.Core/Resources/Cjk/ChineseStrokeCountLegacy.bin b/src/LibRed/LibRed.Core/Resources/Cjk/ChineseStrokeCountLegacy.bin new file mode 100644 index 0000000000000000000000000000000000000000..64b44bf34a6fe9bd4771174b1e94dcede2b4c964 GIT binary patch literal 38670 zcmZsiV~`}#)~?&OZQHh|ZQJgd?rGb$ZQHhOo70@GHl}gwd^gVb=iXgEDkCzpq9Swe zz1Di)XN{%>zaN*&aly3}&GW+8)S1K%orDd5-@`_zwCR;#Ww% zsB&&y#JGqw0fqq`209#wI>1T-mjj|0#K#wAtyB*?C8AjX!GgvE`Ff!E6Pqv15Z)ZT z37j_|Nrbk5g9Q!;F$Q!D>^R_Vz~%s<1AhlROceYb@iXvC?0eyt1hkidGN?0HXP|%x zZUF-eXaAG{8nJ0N?2@qp_mbO-zn_+N13fbaqG z16BZp2b=(S9yoJA`hgz+1P7FH0SY-Nd|=p^?L2_V1400f2s}QR$OB9OHV@1fz~KQU z06#Y1a^T5=gdG4s6nZf96SadzAAt!eJwR@#bO8RRO$UNL787)OIQ;B`n4m^Fr zI?(kHyTR5&zMlpiMEb;zAlxDNgRzH}KV3Q?chDRm+rzd891h)ndUt^Dp#Ft$59=N< zICT8!-T}RX{ulBk?B9Uzq2Et`4-h`W+`u^rtHS0*-gEumM0~^s0ZbCuh0%+Y=c>PP zy9f*d>m;-as~0)Xb$=7W6_CIcvP2coM4|es5E#Ux(L}0RgX{(@(F8ZqggZ)b7mB7x zL#j(~7waw5TEh@U$O1^zr7(ZvcjcZ-KT<=XfJcgs7@NYdrr<~+P)nhZN2-iinA-VolGGo}tDj8 zkJK2kHAQSp)0PH-kP(59CxMVPhES}bh6$peArrADu?`~D4kG75%eIa}TEMr?vb0V+ zJCA*&;*)uhNWijA+>>`IY+oYPEwa!pY+u~IApDJ1yM$M}%zt6<=;(z^ScbS&^0M^r z{PPi%PfV{xfw%;DG5o@OtI*&b#y3SsikRACtGM_P_MO5v>iJ^%5%-;euy}s)Pw;jy zf6xz6&I-tix9 zT3B0XF%a%ZNh!j(!rLH(L)*f{NNFj;Qx@75o)_YjRRJzCh&H5xE$l7yE&MIi7)ZFJ z>@D0a%q_ydNnR;2V0C^li@BW}pDUmLq~SdY*;z1iaQ_aM57`f!54B%&bV+=K2#}3nj1Z`Y(#g7q zX%AO^>Y6L@2%^Q|pt3h{H!(K}JCmSAY9CIc@;kFQ(>U`a5&R-^JdruEKG8n$7>Sl3 zrzWE&FDLUm`F$dPVo!m^i_J?MDa|CoB!xCPpOi1nMA_4f4?I5 zNC~N(J5$U~LU4f82dxiW9Uyv;0e4IUrNHtcsxIu^@W0Z?1*j`zZRq?F6k?Pd?{vPg zeREc4%>bP@dmlF;==|jQ(LZMo08eo(F)i`+l_6Wy)(TyDMxs8VAMR}YIyp6pT=fZB z8%~fM5Lh7afszBn04fjo9z>2RM5G?x6gEH~{q(#|KO()N0t}pc5eAA=Cq62*VtN zJuG)n3Sjf#?Ew>vAQD3^o~#J&I!Aq^@j?)c^RmVe49t^IDird<6BNHD9SRZ67%?;@ zZp`Nqk${PAhzuSfJz`0e=LwM`AdN#Ck2ro*c*pY12AuOh8oU$v=KF~Vmk=+;Ur;}) zz2oM`&(C8Yz~6_y4*8(=(HO-r&!r#8-Iu;D`QY|37{xKqYaP(vr@l@V;@e&<%r4X} z+$<3G$(}o&8=m*+Qay?=R;R%WBJ9KLBkaTNBkjZPBksfRBMV39$LdGx$L~b!{BGjs z&B;+@qHa$fpS(J8x#x$>2~s4jkI-77Kf`>{8}BZ~_(7XBR_Xwe1u8cN;mFc-l-qGbh@71>Cn@QCJ6D547e%Hk2vp)?)g zJrelL@e}PYF+WTtBf3B?=`9&kM;5%L~m5Hw3hb zGq2v;Lbev^{;0+C!|}uLBk)7>!|TNK!}25QBE zaEIs)h2tG!!1Dl|F(+_>B7T8>4_%p{^+xWAITgD{Y>yDWQ~yfukp3?6U-lO>dJ8+$ zI$RflZlKVHV?ay;{T9V7h+%<*K0suN-IKg6d|8AZ@jU{)OYo5RF8yZ3&4`hwqGI2V zaGK#h%ns`d?F;t{@vGuo$+Bf^ZPTTGr2mkr#rMSX#PTHaMD@H#iRroD9``ovgZT|H z9dtTyuS`Y5o&srUjO8mMK$VB0B;GIXzv7j+u1H&e-T+ktq6tACm^w(OtVx5PLU^_y zJ;Hv(dlxi0WNy*Kx|vBJuSkWxALB&ica<8~3cV)ATacs7M8ll|Jq3Rnd@l}J5Ts06 z!J-a92ZtFPB~DqWs%%q%ppHccof=Ljf>|i5Y~exP1B)~yz%W48gS!Xj6e`{TG2H;> z6x=>cS2M`hgT4oO8}>3_vJs&0$odZ98zm&3U(CFaeop_$`3~h9-6y_ZOuP_(PW{OK z4*417C*EHyypVs+{K)+d{Tcl=%w~++D6ehF0H20A9cD4^Y|PuJukFZyoQ6FeemV|k z4B05AZBhfHi9sKRIWB8V*{G^*Q-h$1MIW9zPG^kSD64Hz1E+~u9i}mEi^3eKHDY&! zc$elO-9s&eLL8|yVs!+6m-;H@L+ytm9BDM-M8$m_= zPRE*>NhhyShNqTWIk-Y$$JUy$B~Mt!q!wQ}&tZ(m7Mn39k6uQ*9Ah)rVyw$nJE1FY zSBAKpW;5Pm#m7RJsVHw%2EUwoG4|Wai$yTgP~NEzazDvwwEb)w;PB?=zVs+R2#`~e~MkvU)|~ zlIbbIOSX3<%$B^haZAFXic2lKnsFt~mcF%hOUj|TO>MiHa3#)`y4B@1cSY}#{7Iv` z!eG_W*1a`+OYNrgS@U0o@2a1zziR-WGGPglW_X48s>$U~*APBc!qPa+@d~rmUzZ-P zL3}Eg<%+7*)oLr1mmHP-d&`5lpmZQj30s@ zzzluXtmS`+exTbI{Vx2G9ihEQBE3DPo=%n`~_V6^wZpm)R ziOGn`^U3nbjmV5vPZOOIZxe|V^An8{_Y;K^`xE^&q3DZKC#UQ!8k=-CYpyks=*v^5 zryNcj?{(j5KBP3tTQmmftTCJ5^dxJFG+C=VRAy<9Q(UKbPjlf6am$O;CM&Pi@22=P za%~k|%fr-`D^FM4E_qyYy%c@R1D6+UPB`4McocFi6ul&*9A@B;A(a-3^~Ym94@Yn*GOYpiRuYy1-+e8g+aYt(D}edK-Y zee`|Yd(?aU-^jnQf204#p+}&{px?xggOA10B+*CFMvukODbXm=OVLWvEj~=oNc^pa{P*39vn7&qhWmE1`9lX55K%(0v3H)F3ApGmxvdMEbGiJO==<5+5#KR1p;en(ddw=NuRJccEdS>ekpW$Kq-S!)y4_f^_ zUi}0Ao56>pUp$989u;D`lni;P%96EB42LEjb!*|9_EMcC zdz&~8tt{%hR4qsi zb?yff*XS;dqm=}@%BHG{?UkELq~f@&lB%k*%S%Pq^e(;K;xG+y&e*O?!{R)ZeQRS5 z4m{l0SSl&C;>@mjYsVHAUA)?uH7We!+Qm6*r{Bsi)zC3q;5GM}kkv&RlU4Q_ja7KywYHkBz?i*^YrooLtLIwNr72k5 zw>n^J&Dn&fr%+R-$!?agx?^k7+MH^WNx25{ zafq>qiHK40afoq=F^LI@(TTGwGAdFkaw?L#_!MM_)4yN?1}~MxdEyj?sLa$FDmOTw zj79bmxMZ`{^#m)3*ur!LV@>7=tm@W@MGB;p&0fK{atX#oX1-wpW=Y1`VVv&0YYiJk z?|$9g(Vl`L*n$~_WDUW-qUDGwl2G6MD=8SM4v#(5bgGVDUC)F*Fh*>k(#Ed=2d+HP z*@81=_zKDuI9tTlK(#{-9>P8Zqrm#1^@FPod3!7_{OsV_(X+(lN!k;2dm-M~9K|_f zWoIhh=$u(>0km@so1XC!oEF2KJxaG#KN3Q@Gu*(EHS>Z}?*RT7n3n9DSKqVq(+S*SBWUxtYcJ6Ud`^n}fsw=+opM;)0~vfV`M2_G7X zC>ogwgA5sktSNUPi5aL?TY6N`}LXnbGcazN$B_n#e%L$2?jJza&XZYLKK_qE4$Io6aPsRZ_jg`4#uu zpo?@j#j$c+Ln*e3dL?^x_p)WQiroFPi?;X*He&}ib6&D3I?Y}S8>@7_toD| zOz|H&+_PY>p9_ z)v?ZJI5Dtb;YG)cE`ggxHm(5c1a3|3;9Al(WxEW{9XylR7Ac+LTGlaHyBf|pJST+) zD&6Fo$u$hSD$dy~$C@TfZT4E7FDr5dQt;_I_z82|=05cW-< z{jLoCiur-(2i`y0W~|qULS@2+OW~x!kMsh30SLBkN;ZqAXp3zJhxR)*rk-OneXzpq3wOGudt= z)J7{a#Va%8|Bj7`jEqZ#xMXfR!+R!xZp8#Z&WM%|>KrLy zsr(TCLjBJ09}+&zzng!<{*e2^{LbaP}F$-iWNYJZphto~YNv&L91%gB8Hjy?GGS~>)0V5XSaY@u zZC%o}sBK&00ksMi*RvyBJIsmQv-i*J7Z73;5)>j=VJ<}*5HmQO2VlR_x5016-jKeq z*~s&KXMPX)TQEKBmmn`mGe)OrOk38vaDftRfY`&h26u@I7UTiAU->_RcclMX)v;)0 z+UdFlbI`zpaDinHNCA{yu|M$P&7dqzy&HW`K+pJG#HR~SIly$8%!1J$wO;81Nj7lo z!GJ4d*uUX(1C$|1CnJ`Sg1kg>>qPV(#WO?R(vht_V|#}1jOjb~d+__j|E~W{_`~Qc zKS6YY#y3ew&Zwkb*O4*)QDNFH5bFdGcMvgW>91k$VK3ohDSP?|3xdoQ`V9uQfp7`_ z6#6duSqKUcrT|YuoeVLTY9i4|w3l(CfKEc63?&>wGMmf@?%Y7Vrb)t|3?>|oGb6$p zRWzn_KLJ?4h2^_ko=dS`?Wd2a(g=0dFbnand4Y;8-VqB!%3E~s9Cuq*_o*_MALZto50kaes7;vzmp=4qzg&E5FG^8mQ zvaqG0Y+^bW-H+n$w4X_S@TVohOGX#@IihnkR!QvBIwv*GT)f>m;&Zfrl3b=ePkNk% zc>DE5m}sz*;HM=|ikw+HJM_hvXw#GArY%mIfMKLZU$m~`bot&6vL#5Dw6;N0g1!oK zIm(W*B}tdIwnbBlzB+Yz$_}$7N|&;>NmH`AN`u|Hwo6mIx^i{N*3Px1OINVAZ%xFm zf^!MaPNAhtm%X-gP0Y@Q1q(ch zU~RCz(N%@(5}%zxOPwxvZTFh^Rprx?7k*dc@`lAV*E5dEa07*08-`uBiSBkSp=MI! zx+>!=?>T`p68~h}amO60oO>MKc#p=Oty8Saj^0(V(Ajb0;$GM5hlH9)pcJWOLXiyX zz2Mv62DCl6p19eDsV=cGKL-{bbZj^k$!H>)%#Uk6H}LMr-62Oa_lE8b!E2#s5bp@V zAtE!SiTD%hd-k``4_QC6=;ZxL;uEENY`q}H$uv{BW{Zs`d)~q_Ww{E*71T?(r;U3t z-$GD5QAZN)6#U89Qz~b*&deQsN7D8by~*0vK4<*S+BK!Cdc*}N+u)ebE#u<=R91%0 zT&|IZ*x)rqyY$xWEi(c(Bs~-IO*V$*ExQ{R0czVDcSRoRT(!8{9Je`b=$vMvgFvE_ zL87C=M%FvXyU<9>$g@Z++Evxc$9WNKYcf?h+E~HSyu|)V;S-}avIcN_@U~DYQj3N^ z8}QeZZ}GJwnpk+cs3frV&r&R_ttw%51gvp*<1xo+7Mp9L$S$U9q{uI8|E_$=I1{kII7GtBNHr>Q)S24Kb~NxE zJfv301=zT_xS0BAKH(eCz2kicgqm76w(JQDGk0QZ>D|-4t<7T4*G%X)dDFWaYUvd6@H3sUywDynq}JCIRj|==tbB!w<)hfFO0+9uz|ypcgY5eK>lDrf$WPm`A6iQ9!4fS}~=L*#@bRS|z1nrKieSk)_U9#ibGMAh}w4sq$0_ zWp4+3auE*VoMl-J4x4WF-at7o=0=3GTvwT~Qd8Xys5zwBj=Pm?sS#GhsZ&?6Z%5wD zxD|Fa7;HMKYgOT|=H4*1n0Yq{ZW`Jq{zmO;D&ANW<90*fPa_skoU z9h@__g7ppc6Z1C-U`fcGoiq8<^sw=E4dzxTAdf?VfE5NK9%?f5)3bv`42L#8K^m?& zWN`?`q=rS2h%yG73I;v&$B?;66pIECBT%x1XAj97VlZiAktQOK$2^-lKDoPxFv(*v z%A;XOpq@-Vr5zG8No>#tKD*^Io8{Asdjy9t0kyo+*%Yg3my^zW0nff35yOhvB<#uP z*3Ek~&&nQo!@Ai-tqJ^48HU zqg!YWmGZXJ=CVi=_${Yfa1N#N(=jtsIc>6fwY98swsx(qc*Y|w->r;@Ybc1TEQsrC zb|Js!cdlCf4i6c!{Ln%hP@N*p*PXS>WxwJXQG>1SoznfNcpp#L?Yw^2D*fN3HBo9w`dpHZo*#~2e-9vE-;U3FAP`I;P0qNldx>1qsLwd&!fHn{Q9>G1(+X(j|{$m3`od~ZMjRPGBz7;cgU7vx3w#=Wf;uX}#9u8j-D;p}XdCfle&BXS}%{>zD4^0b&wbR^tCeQXBX{6~e`zh~J-+RBe0D?ru%{1hx z@RMQZrt6719|<8k;^g@0Jc3oSd)T)UAK7PMK92iVV>!wCmg8d>H;sJ~?JU+uQ9e=W zoR!05lvZ!u2K2S5jMAMZxKH)&9p2)6wEC#`(}}I2TgC6i-x5Ex{m6t<^C#y|vG3*H zQa^P0Ybd9kIHX-#$+uK)GVToc>pXSU`3rMb6m9z4boq;OS53EgZ+fuSnXIu}L)?DI zEvmM1FdB5%7%bM=uD4umA>Nd^$^C5E-Xc5>chl-F-{tnRMQ#b-BEM;T9(gSlT+OqY zKW)F~zX^R-^)BsQ+p~6Rb>H&734T`jSK_Wy!*xl{qtGiN#)> zIXmNcw$NuMJd_(i#mj zEn4@10(H;xc#W|p2fa@H?35*DbCu3)jpJIwUIu4tza`^UtcrFe7V z=7zl~7iYH4ZOsc%oiBcxe6RUr&(<@lsbAy1>V6uwH~Hky)9tU>Un9P%e^Qz^wrXJC z%DkR&!{V>bo}P8EYHr@h`NU zBzvC8Cf)gAyL1=KQM5ZoZ-V~>%PqP?a@W-HS9^%w660x_+jNJ;uCtp?dyn2=?YX8i zeYeI=l_O_+m)>yg1+d=lcHQZ6<89B=+jltN`iJaMk9r?H%50-d*&^qt1PV|B~<_&O6UwGTMu}C%Nx3_ul8W#eMANAot1CQTi6R zwcnh$JsO&T?_BS` zEiQR5!jEMLV$C9-BatHGA{8U&BAFuVehHLn%HE{#Rq9`kttimCey5AHe-r=vD}$*z z+ET*7sUWw!OkJv^_>J^j#`9c|w@r-Jd2ti3bW&%^#9qbpFZx@YE;7_eyW+A9`QKR!QHNnNJK} znu6IMO7nGp7F;enA9uWn1ncwW?Ti0#Z)e@iJX=C{HyMo9T^hJ`Z>{b)I=Z)qZ!h1R z8r;=+t@m63wVn$98DS!${PVPPr03ys716_zpR&va=E`hznU6tUq|L?d3Ky$_NQc7FK$IJPnxr5apo?3Ut9K@v^s5>4*bkDDQE)$U3`^$a$e zmgS|h=*lAzGn#3w0B=H~Xz@oT$h_J7&nGl6F$E)9FyPNqnU{|R;`XFpv+c$Ww) z;<&~)TxS~H47&3S_8pD6TktnyZ>ZeVfUjW3(WL$Nzpl@nufHJf1^g3)*GRv@yM}iT z|H8PJ@J|um?EV_}aPRg0t9{$|GT?nKcu&-QymRM9-`)B$;(aOjkmt?ZlfFlP+v-1+ zd+_he<~zoJ&HH%#!{zG@=Vbg7o|PwT1e}$lk2Gxbf+zVr@jUZ9^*r}H`8-=y*uiS6 zu4VT^ZTY$68zNl1Iw5g_21s>f+;5~0;g_ZWG(E~wG^fsIAJ4qtdrSAG2&~#0x4oRy z@B+va7S&VuL~~0N8|ydLukoL20fw(6J{Ur@7NPU9yrn&=qLmfxYuo1zPi|h`EGXrNNEJ>Ra zc_;MEiP(sB0+{L4pB@qj|{5Yz`; z@fcz5h@cO3KLq+05%y11AjUJ;t}5i3#V4LyYP!OEDS%ndq>Nn&;TkDhs9MQEm)j+8 zP*|tBUU9X8cu7T6*!ON3PNE16fcF7cmofkI=6pGtqF@QTr;5s5g78HpN+3yB@~l(#Nf1&{BMzO8rCk6SGrR-=YpqE46w| zOdlwIaQz|sL&S&m0HqlcOAPkN%uxnYP1fuT zgBj8@4425xQ2|p!*8B`31`;gP=%~?=BOpp$qAkn7n1(tXbvkly3fWks{iB9K6SX>O zb>tFAR+nkZ)iAE1UPV2PyqkhI7H-JcGjO41N6n6$=^HhMZX{h%x1s4pJB+lM@;CNw zh+fmUq47r>bbOg=HD+%Z05@|C@5tU!AycEq{0$?50(Dh7dX1DSSC58B#djRv8X-mEdZh*G z%jA)}cXQt;UeWw2rRDM~_Q%Y32H!Rz>HO;X2mzKwknMpZM5=@-*am_QdP zwNKZmxPEp0A^_dIA&+Eo_$9RIhj)o;x|kFitwzR5)DXBRz8=S6)w z`$bNx;$0=vT}4jI?N|Jd6b7rD2J7yZ-7}HzjxI^On>nV6oaLFzv$$vSE@`~mIcC6e zGV@^;@=VMnv2#P;l!-GvbNWf}3{KF#jzcT6dY1D{*CiJ=qX0IuAu^+a&4^omW@PrM z%La|gM%8j)$9Rd#tcl9Xk%Rlfd$wRQJBT(bdzSHC$fde(A0ab|Hap61V8_yt+dbn) znXPS^<6jQ<%@V5SGZ8kKZJrAP=I!nL8-zECZdTpVyBhHuyVrl+ zkRKW{?O$5FH+ye{+>E;OclU{mkz1fQgKtQmmAx|rcYql|i}Ysk4ePVEcaGp*p0N@K zp?A{7R`Lz)v$}Vd;BKDDe2Z14^!4U5fp?wYe4Z0?19q15jpVZ_a1yQcJ%I1*)QduD>l)(H}Q|=+CpC=ayhVb*||JR zc_$P8F7I64#r-Sqp2okOdpzg*r}JSK@~@bCV)ur@5tD0r=d{Bf)(&QywUe7xSM|>7 zGnRX7_nN^UFs5@crfV{$OH-!5b?&g~ziGJ-9DmxR|I$fsZtue1RdAa~+bNj#CN>zv z+@1Z4SeHbXMVCgGN0&l(WtWH%i}5dY5oHl&acNO$u|<)^ zE}^4pi+)Q_wG_Psy%fD9y>zuiwN$lawX~gtos^xVo%EH&mDH8wl{CHtz7)PBzI3)k zwp6xc_P_KG4B=I!hvj0%k7=MEX4_KgKimIzAK>)f{;|EN`9=6$=Rep~cP>-%ke2)Z-40NR7 zV6dYFql=@7qYoc>nR=K?_*j{mU!ILrq=eDNs^XP%{D@Js$-+rqVkq@AwU>=fNu#Y* zZ7*?DKrEG|tyMcs;i7d=OKtg?m1tRZqmZ8_BG|z z4>3*}i!2(f^CxWre*$$vI{pCS2^1~-2<(x>uOuFw95O`>EV}3sn<-O^$R>52GQF-^ z;uYkJ@F(+K*5WhRhZ0X&cZGcchdkE#t*GAFJ;$+Y!n(hIf4g^f?+Q9@a2Dkj<$l&L zGA-6EI(~6~cYl*xz6&3Bt8N9Nli=vYOb7Y|eguLA2JIY(kpAl85f?5ZZcUzXIrC(9 ztggHG8|xN-14$i}oIu%uMngb@MuSI#MKgzZ80I|OakvBHSi+;0LpgUQMLV5}IVEe6 z+N7&fT%)meG5&bmRWAL6{{G`Lai;6%EYYgsx10gf)8;5-18vAPSBs3dSG`flfRMOysQu4Ml*x zh;@jlcF3?hDSo1Y49X!YDC*trE0I79en;f(=nEv>!M{6j4~X`k_?t1rBglv0cjo5s ze-D`aAcUichvSclh!E9Bln$ux)H>qx$L0^L?r{8=lwpPA$M4`@B?SVFhSUHnA{GLO zO5@DXRR>yt>Q~MWK0k70Okj`6kkhd{p!-$uL+A^{KSFqj_*e;09%vhQ8Hg8{wG$&! zD{>=}E3zxnEAlB4EHW%oEOIQ8EV3-pEb<@{Eix@qEpjE2EwU}rE%Hw!Tx48?+R?JW zr9iI0u0XHAy8!x&VS#;teu001dVza^c|n+kge4(6R2Ufn4gm=P76B0f9sxN92+J5j zNI^ zs^kAVN5jkNVKU~4@FtTcQ}!07tr7!GsLWqTpz`s@(NX!2~`D~f(q5d3Dw}SC~o=z zOV>k2Q`DrhS!Ayv7F9A4Rb;Q$M$ex@A*sP3spUr3ow7ScL|KCqRe7!PO#3et{78Ya zNIR;QKplxTJk@+uMgKIyS)I3xKudtKDo0t7t{lUNw>C#*j*j&-$JsEF9y=vkR;;t6 zgyy^6`LimiPG1w4DW}>^xqfDO$o1FlC_B=%r)*CVp4K@lb=H4KFY0*wxM(wZCHS9i z*w4*f;4t8y>#7LmD`5K1hox)a9E6Vrj)sS4Vs0i5c>4406MepO{@>n<>#mY_!ZXW_ z%GPlUwT;qpUa6pxcj7bKjmFkh3%8Ba@?L4LQh%Nw`HlGd?4#aA`|@qczQVr@Kju%r z_u$9yi})p>Qej2^q%V$7o%e@F>RiW<)xt~Fr;2xV zaG>u*bHl%yVJpU2nx!&ZiKQ-Ag}It#E8bcPQ~)cH)TOJ?S2N$^FU4F-yHs{7iPYy~ z^l;nR|C;a2cBKCjJ{BDtj*-TVXU{VinDvl)n)1lIBRi&C+HqO`ar}aXPISs^N4K&VICDM$?qOS!1Khp0kZl zFSzyxqPbmT>n5MQK^u`?zQX{QIeug8CYAj%2>!u_C8*E=at=%m0tA?t2!f3L!$9T0 zg$N4?bczVgPpY3>1yHF0SPS5P!2Wlgn(-vn3AA1fTx2glCa~zNd#MMUkmZ=?lo2lCrj59|)~9&8Tywz(dN9+(`c9I$Rv0Vn_* z00w{nfDFI}pabv$!W{5D@IA0Sh+k1&an!-{gLDsEl#zX4eGq-%eULw3J`g_OK9D|O zKQNnMH$w>qT?QXS#V2q%g0X`R3ov_dVjvt`B><`mu$EQi!HdQWIM~jR%|Vd|rw6GA zs|TE!S%5?f$Kx+Q;~+;huVkShZcqq zhIOUHPs^M2cFUedRyCe+g{-~u_PM>~U!U1QM ztU+xZDqMx#DcB{SYj2n6ZiOS;eMR0!Au00w*yh zEKx0SInh`G$tZ#*QTZf>o!6PGNU9}*i1&~DuQI-6vOl>O|3PWlr!#M6ex30>9bHx_@J zjz+#tdXO-g&rWCMx02YI=*yccoya^Y)%-w^sZpqr)BmBbq?^*R657T30{SBQLh7D{ zujH9}&v&XYwVmQld#ff?;v@f-a?g7zH?^MPlxLB5nunQ}nx~Psn#Y;fn&+DLl!urX zo2QUBo5z?(5kK9$di+e`-R7w(5Mb496>e2NehezuEzsSYrS+2ckoSUTxa>WM|E#+g zy%(|mYYN+fJIsw$a$OZQHhOHA!PNZDXf-?>^6e zt@R#j@13v7hdJkwYu?uw<9D7T|AqM8=%RRKyi!Ruzf4fhH}8e?-t3}!Wxdit^?O;L zoNxXM$-T)%`O18yfogx5pWJ)i3)#KJMg7Y6N`KW4(#?cRX+(0WB)G}gBbnF42ezMD zTfbX`to##uqy_o$@(AVwPsMKzJFm7q3Hh1wkmlpD2H*}tuZBEPdZ@p^GsUG3$Q_hk zEqUVf(0@T>ibo%y+rzk){dZo6D~?bWrYu%zwBl&}i6aSDBDQQy*`(5G#nT#$BQZ-{ zVXRt3t)g;;-I|vpI7?=>$Xv~(qH~4cTCg=hSDK*+Q%$a-bcNNLr!~lKyv>5IfnZb7 z*5W}3$lRs=9CNq$&!8TdfkpiselUA&eBT7U6n01E%g|*&Hw|wP+cdg#bpO;EMlcy? zEY)nX(Rj)2&d(Q^JE3Sy)vQ+0da3S?+ZvZU2gKEynm0Bt@!ji`BXX(GF{7X*LXAb8 z3PY4tsVP!0r=ZP3okhJ0gOpV&$kj2c8g4_YL^%q(l*KA08Ii4FT0?n7^$YWq4J?VV z(xN9Mj*Xq2Izv9IT2ceSFSuABKdjT4^Z831u@*Y+bn4NRGe&3CXN5Y(^#q%#)|K2U zDTr153wyWt&)+OdypMTl$=Owm-0HiOc0QPFiD|2hFF@}LyrmV?oKlr#?i;+t1**!b zQ)H`SE&z&ad=)-bgC|w%{Q{=AadE`5Dvv!2XLdDUamca~R+6r`b#ctnriE<_kKo_b zJT2}Aj{eYo14bWqT*%L>(gw6Sc&XvW2$Z<+W>WJbQEn6%shSB13yCLZa@5A$)VZn& zihmNTcuqNP`*d?JX9H&iXM5Nf1gOQuVkYu4SOjUU)V%zOY>9OHaybgyvbt$4+lTkU zyW_i5yXDd%*1ZerbvCXFy8D#;%&HMv2`x*n@b!%Ba=OQxKYZiR&9Pu-kG&<^OIGuI zcd>TUS0t^`S~K-W7|qigXv9g6A)_kqOTa%3C{Qnt}GPI!T!r6tiiSiZpZcE$Kb0ub1C3(vV z)I!Go+OJNuB*%=DY9fWP2ySNKB~9dO6tsFzJ7k=38*p3jUCI4bw%w8~^L+Tw8DIJa z(FD2~h%c;2jFs>$8S)792JAWDO&GL*SQ(o7aMX^Ik7Rfx(c^{t9~ zF@DEA79hi#tt(>#q?$BA(GqnIFlg47BPfe&__1++3F%Jema-#W+MK<~APQATmOau% z9knynwQ1U!xgc{!&t=gL^8us3fsTpX1-o;E=`7cox}bkX(w_Eh!tU7J`DdsNjtxp`^plw>?^ zJZ^%+!pYc)C<2kl>#5X}PB$A$W#@>(xa7N?yKlaOul$J`79BhaoZ??6iZUei4;20) zshgaeoQF>WO;Xn({1*Hc0##DCXL&Apw}itc-i)3@gwrl4w4X4-qJ$+1jg_4$KB=Qt z!ljB{h?EwxD(h6_P=Wn7zghvSjsaW{!>xrm3U!t3DiGDNs{YMIB}z)0l)x;ZTZOSp z=@K&}YD(LbAOPG$c#9M-F+rlDw8fE73EUFKd6=t|H?ctC7ik9|NsM(K{y6r!=X>pk z9{VWuatMAE_(W_9B8W{CgftqyKYWeyBn^xYaEXHBN5@IPudkmz{rB|lVEO7CUF^BS zWW|X#*qA2nfJO+Q2W23r15s%rc_nhof>LJe%h74Y0f11Iwp4`|xMV2dq{!$O45diu zk60+9OqDUJAg0PlY!X~5t_=rWGP-khhu=3L!!>{1^u6@sOqI}18QlQC6nQ|-9y_2P zOu96BfW|P1Z#q}cDb>z0)6QQ|JEOS61mql>CZa-x{mjC@Na4{TC-p-IhmdKb1sLHm z!Wwag9Mdo)p~*s3hAca?xAlLKJf?iDEZf%mMfQj>Ht~M+=LW>9fLRlp3MmO{S}f%H zM)^KyQ3;_sjaj6(Lh{H+(tlNxsWOg5n6S>gN`<3r>=?y5ElZiHS}P`SA8(A$K}M?6mAO?X>MQ?zHZd5U_h70*_wN+JZG@JRtgsXpID%aM9d? zxd~`fa#mQJ*cg#9(!+%7iBG1atT34|@`G3;YS*Jrs6I3R0~A+3pr!ghGoXO_B0|;p zit!aRO=G3T@=ap8<|t-`491y+@u|ZVz6<^PJeJK2zyaWe?R8`u9~YbJ>Xi37!G1w= zFP}j(<9scQP(TTgMhGj{{8#*U{5Sj${MY>V{I~p%Jy$&;e}cP&^$YElJu0A9&@LeE z!+^0;p`Wt9CCGoz!LC;)@J$&3$QlVj9FrI>QDVZ_6s8GXCyZW7lb9}1W5U`Lp$Stb z{NJ(I{S%}QSs%>zm=C&|Lq;FEUjUNA1h6P{7aaFtz^7llt4352ze07v2882feT)0l zgevk^m@c^RjDcaSPU@<5uE2Pn1rUK@YA{+eW@Exzo(9XD@;d0_o2 zk-;#7asqoB@;n4)RM-&FJN3n+9xghR;u68AvO!*(u?AUl>hKumM%*ijUq_olDY1NN zIb`+5)hM<>Q=4iYd2O2QSm(m`KJY~u7^7+CS17GoT(|%)N!7QsPxE_)!>Y%P?S1cy z$a{q!YZ>nH%KVtCNf^s=hS?0~$%IQx=NP|9UnB7w{Tu%q=^N`C?HlhKq8twpx z^5$ipvm94OUaqgny`#Hnr&G5_&o^LRg)b8CReo%klK_P?mUPvA^>`IO0XKdaut>wQ z=(cnVxfxcJ=bQgJ4>d0}Pc?603C50+^+_SDOjxbZYQfbKk{vAzVm9nV z*rm|xeZ;h2SCQnLJC+Sdu_5bc zFp6T8MlX(@qq@p;q3dTcilvuMFCL%6UV=Z6yhixW2pZ)#&2O9nNn(oEDBsyWqwh_> zHy$sc9>`uJ-)H=c`kTIQ{Junbpn8pdXRt+6-4FsJw#9OX=7Wd_Y4Q)%|IBIB-XL`a zM7rRvP*dEO`DM;skq;O<+q<`dl6x>X8vv@68E?Lf_ z?CLE)rJL7EYGX#Kv{p4joDN+pPTfVSA32>k@gpgKGLI>*eu4I%;d%7ym5Hla7i{{n zGz&;|0C=||s6wE*u**ExyvID!ywW_=yu&=vya6*_2EhzyZSa~HUP0^vr6pSngly2~ z1?G1khmoNnz~}G0!*d11SKf zOavh%CT+n=Oi-J!B9EAe2ntDHo6r%LU>BE~BRNF@XGw&U#%@fA_Jjlm@NZ^@apMRG(;FH zFh*ps$yF>w7laV9Ar%t+f={}JpYk2$xfxBN>DQI_K7F}@qY6nD6-NG~l5oV}oOGGg zK4s6N!%dNs);>wYO6-6Ybb~08<}pPmk|^Sp-lMlm_SH-}EHt42bP}wFqM^ z%2L8iovsXHUDlGKRaMstXL-uftkqf9t2RherMg68t;$j(_U9O)NxA0i6?Q@6^oHFf zOAlm;2A82pedGLwLkVs~AZ~`!0JkN66D+P3r2T)y0+2=+%~5(3HA$K@)h!xhv^J^H zK2bh#K8;gSeYzN=-)o{0e```yJ0D3MSsbM|bKQK~JiYW#CG*E05h-RXVk>SgYA$vu zaw)zox-}^`E;kvhS=F)mT%OF?8DtL%L{K5OIwT4h77+gx)p!j87MTVRZDjgSn2^!J zuLbYQv?0ROAtF!(!3!khV5K75;=<&v*t|g-3o1vbDRJD=l>&_$#KhF_erMl2|GXKQ?8sa#>UDiLhclGFk+DZO3%CVp2 zJlk>d8Lo>9;88|fPqrNGI^92itpi_A{4wHo@&|1W+Ts{Ih}JTQF$Zl9F3fNW>R1f2 za&^Ia%1W1^w#`Yq+615+bTw+KG+WPt&W1AZfvMe+c|?%q5w2h8ufwFk2VtlJvIUqk zcua`sz|lceLzxD2ZCqnwB3F1`L^EOf5fT2Qa7AINLJ)G|^~CH6h9gdAsLtqZv0r3J z3E>lDfMw#7R}{Zk9vM?Y`UKq(2e~Nf4Ww6e&v5<0`qhsogf~h*xc=DhgTD_RuU?

oWLR;2p6y^PddB*i;(6IDC!*V2J1fSwDu{j=P+8Itf23)nTn?{}{eM zesOy51l<<4N9M}F8$vh^a~keM0m``)M-gbaG)I*P=r%_g#}IiE+K~`{B{xygvCwH& zlH?ns=LjEl#dMm?G@MN;BDA`R@niVfI!&eOdX5EL-18xg+Qc<&n<9?YEUVd8OkKI! z)U~Yw_$@>De5V<1dsFP1L7blb_9{J&nk>!Q34`i3AntE}+TgO)S;OmY59NZ$4P_!r zpVG={lPl*WH{dUf)c~)JNe`74t|(GoNK7sUiyky5Y?1gl0VORVNSdE8KY?{bcZ2X8 z_XdcBl&&qFTmZ}y)n6jMaD3_T?E1;Z8%!WEZzT3)%+aLt?>G(&C|E}nPRgLyJ1U3V z_QM?2Iw+c1scxGL1WEE!z;*FzYXL5&-fe-`I=5wR%YO`}Hri68NaD8r7I{bnNXs^gu@Zi4c2U!`S|xehii{JQ*T&- z)I3S0atlXb2)7OY*6XO(LAH(Y3+A!4F3dECRZ$NihaD=+~n$vVA#qpr}3IbkXY|pBuHqAcRH=l^QbHkGxX%q~yl}BxIplLpJ-dKxB-n z2TKr|C{$_4Vn6yyzXzO($WVn|vhtXbqOTYy?{JbeU?~*Q9<_-!H$|fVTv05!*FzisMxwEQi^UwIps) zKX-BJ;uXiPj#(YH!T@%gB+gA}YqgfjFk9*^b~4i(mO0$2LLCHz9d3&pF8E<~MgVVP zWyD1pUMs^(haaC{FOr;zz|+}+Co}rIW{#^BQ$5CU*tI%gZOhS^r8`@5q6t@Bx;kZT z2e@-|f7U#VAyfyiE?$@d(O=!%+K+!V3#&VG7N_lS+5(qB?j+p}`Xe1ySgp~Uqd!sI zWx7-Jw@C1p7){dM5WgMgkj<%3?G)l~8Q~ka%D7s&>bQEjhPu#qv6hXsWDxRU@e#2Z zoAaZ0Bg2i?JY_y@cHpibcwZX7eO`O1L5TFx!9#mSgZL+okQs^hYuOOvue2meOh*~4 z3m(vKimn&@SU@#a}ip~)#hn3RkyE>U)bC?Pk+O> zi~_I#XZi!j$U+uAcN!IDmKg%qXIT_C%izq`_^#8)V>6XT-BMvRH0711olSSkVs9lsJ#)p;zUJEu?y6(i%@65cO<;AV^%?w=fig>FVwk;YU~PZE%AT^ zw44plidyD9EK{8&y9NMS(X=zQJ1bu4=@!xOGqyV&-BJ$S@_DE3j-747xoMF*udO{p zdnP;Q*N*qkwBe4_;Z9H;!P_K^vBNw;eFKTxMnI@EMedY3u51{8__@Pm+lf)Eh8U29 zMW&9@8f!FWY3G`Z;u;ip5Ozx7*2X7A#0ZZZKDM}V z0Z>g;5nZ_Jxp0H{3AssT$LuWxXl3=maZ z_BiXtAHUAY=NrsD0?QeQ%o)i&P>e1fSv&gsChZM+yyYMJ?Zu7S{FjK` zbChRi-s2x&!}D5c_9hpW=e>b;?KrECnWQP!Z}%VH0MlN z9hf>rai=biS{=BSR;emf)~zfc{^f0C71-)C=Vu%+nc;L@YQ6sExpycr=eKfNN9{mO z0-Y9=&am$A2%z3Y#*5JwB1h<5QSY#FV-!iu(Oe=rNBCW7S@Qivg-Y|CPCIUM1~a!I z7-w|0C7i-kZc-()6U5XmSyyBdo?ejX^+CmsE3k{v7L0?gisL3y=?%eqa3E32hP0HmY-A z>lDYWl|wa$g++az3MngAnz*EGRm855WfdZ69o|X?1h*U9R{#X36;mg=mSRoSCR1nT zm!-#F1YasCX`7Q5>oR06*ag{UFXznXCHNIJ2W}4GH1Kxlc^h#9XD*&m7^^6HVe|~u zn4vLFTgjG~vqk6m26>PCBky0>zx4g= z_Z<>raY+VgY-<3`E(qzG9apf+f==h{L18JZCu zD=>O!bnn0sri1)5EF(gC;B*k%o-;t@f_+Z>1ECc}H>|N&<;c;&3&_S{)`M(@-S)a1 z1v&(`LF}Qqg7AhF_R1XDI(W8&Hsb6h+Y4kW;C7ky_yzw6f)T|bv49aqW3I;V zD^z4qw6BZBmb{Je8%iap(1EDfP9UWbfQgngb^s=bIC6Ld<$yX-QTdYsb4Wb5# z%2}(UdYj`HuSanAr1~L1RCV$J-FZTf{I3H7M}$uComAUuzi=LNz7DFNesQkPzy(6Z z2#;xB$9qo&9f>-Xwk>|aJ!<@oRdGe@bvPI&ogMT=>q)d{(A%S_i3@Kt{zCSMA}mc_ z(s@=oCk>L-D$REP3zJ-hZS$HQ-i-H}WOw$?j!Lb9JCc-(>uHY5A0ov+)~!dI4C8JATpiFQ%z>50UqU)oABPTbMeJEkD9@==Oq`b+m z@Uvq->(iE7bFuF7S4?iK9H=>|cr+Y&G!#@Lr+QX(oHCp>^X~Ll&Smv&s$U-8L9qtx z_SzizIs~!;0cZP71<_ygFnC2kR7FUhs1h3eAPWCJl_D}#BnSyIDGAGL#)&*!8O3At z>+JjfpPS96OCQ8C4B)~~Q5F>u5tYIE8K*EtU6Q6am1RnMR(;G0E9F{u0}xShW@jkW zGH50pH+}u)d)enMWK(Uj@%|Gu?zz6|D}x^k-J~y;_9sU6?9ShXA_wTpTZ9?M&fmWe z{TW`hVcc<>bK5oRkY9(;+ISy$UwEH*-+2F|ed%}McjEWUZ{P3S@7V9B-=W_Rzf(UX z4@ZATe^-C7G!VH!vjJzr&{wtwRBebFNI)?d=r-VO2;P9Qar6hw7P1XQCID^&wjMj{rUqBv7;6h27iD0M}h zfv8H1-~ooS0QGlS01h#nGcl4!*hes6@fQ)=M)(y+>xk4rwG9Ux0TD4rgASr?B0>

Pi%gCLeTh-|5&;`@%ah!K$JqL29q;6uh2bdg@>2-&adEsst_&@0f=1; z-R?bKfq9bjKm$38!1ST%z2hrbfW3nKf>0m0KJ;Vn{tEK1EO!_FV|aFu(Ua~nEP3C~ z5f*;n+djdS;s$u|6(ay9Lp=;3dqWCDS5<^voblZ8TNhAzgCZ1TdZWY^aKaa-D?`nS z7hA%=L#>LU0TMk}0NV=tBIQuPwWxPycZb-6)f>Gl_PZEqA^eQ&j->}_Hy*g86wNQ3 zp26Kwyubo}Z;|gszZV|Qpzg?CVBTW@D|FHKh2LjLcT_KM@3DVGK8!aTF0~QqsnX!4 zV~+=KP|G8UNzhFEkhK6Vhs(_2o)=W*@(q1GFm*OAyO_x%oJ6HRB|Me zo>b&BWgW6sq|J!i0i;uMhr~AJFT>}^?h%0l!Rr7|X?{{dG^GZx5w!yxAYdudL;eM& zK9Xxh?||Ss?@8E)To5Hcl4(Ti0Pi~YN#u>(52Zg+XvBaOFEe*WxQx&lQgfE(5CFtA zNn@kSNS&cI=V%VonFdf>HoS~jpeMlA94ce{c}8<=mYnTd20El+8yjCnZcI4nWY!F$ zIahP|H7hbW+t*Cr89&NY5$u#f;8`gm$OQn1)2D~Gw-8T3J|un21|dvSSclQK6i-RO zAdlT8f@wJYc>4PI85U>|pc-V#99CaxJX-@^0!V}8`qA~1A6IVA-afv7hA+WkgzF^x zN&zRwVT|ik?}`B@=pQ*SGr3N*!*u)Mw_AdzI3KD$whyB3p}(g*4!ds!p7MN{`nbM_ z^UORRzh1vTgT4s^w&V<=A*AE*>)~gVH)%g~p)4RTeLNls^>lq8a*gaFE|2Dvh%9s6 z0OV_{ieh&r--WZ76bMM45T z(qg2!h}OD@F%ND2Z*muFq`@*0Yc#?#I$d092EEu=J|VPaky}V`_wOXpxl&e`7t6FC1jOGcoORCqDk6B=ovQ7y-CK0t< zO6e4ALPH*^)7s|u5Vf2!uwgK4DeZz6GNaIwSr$(qH9mLiEnS$tV%zyW&g9mU6&B9#XXa( zW@n9X+C~J^_GjyZ&TFkY9E+50g}}s64fqN5j{>&|*=EPD4HDWV0waXhNX_Bj$SxBf zsK2%Vl&(#D^Y}Nk%Sa$X*97Q(td(3CHyLfkH(KCNKI{HWKMj3G}8ngO-UalP~lW9Bb1L?z}gs8#Zg$}(1syx zlqOD{@EW-_vLDF^8Lob`V%N)0=Lc$jVL@k`_xRq50_i+s_%;NJF^>M=*vufX1<A>KsHhW&}2El^SeKe4rURB5PFI$f~VVgRe`+RlLzNbwZlw2$L4bk6%OTfe`4^W*9M4bqPc;2CBdjBHO(53gi;qQ6gdkDdbN^wK9;9|izHjru+E zcmQ=xmQU}8Dim!nQh&g8-Tfr?Mg#P`qy0zv|8aebP0<{yD*xm9o_0vSk$El!1K`;N zb!}QwjMONZ(Na`Z89@nJiqtd!GAwn59Rr^NJuYyTmQiyAa`u%y&e}Xh_!>(XvV|Cu z*EHiPhq7Tqv^hg`e#arss#OJ_&_z&6qoM$xxFVg1geE#g$=}p`RS`{>x(rWwju!l= zIwQ5ws462ZN@Y`?-hAkz&=XQ1=RNYdifaikdwS;h1lFara|%FLS=O_D%-nmU zxdgib`zi=pml&ps7{+dN#%E$rD1UUM8Z}~CT&I2Cw#{u&)CHAm5SMFG)G4nmp!g=o zFbb<cY!(xNY2Uv(;oHWES$Gd@XY#{yBOoM6<@D7vG8+{Cx?=3qb{{WkA z2yzP*)8+!!onUGj#o`CLje94MO{!=s)nKjR{RqAveKC4(1lx?g|A=1G3G4;oPpD&ly@C=diiV+$hIzqa(Qq`!;QqLXt z3Zg2-eKV@0^cSxhB|2XAit?IW<{y47*-|B_tW#I2V&X`^m5nVKn=(6&S`oLd`kV^H za!WCR)=27#uJtfc@)qrSiM#68<1S~hi-K)+dz$uCzA63Q(iIg43OChXG>`Xd-gw7> zyP6K={OUk_DV56mYj5zb%5T%IncA{%@@srQKF;-L?7X$uMn9#LpSZh(cP`&$6m(8+ z+x=qcr7ZDaGwSW`)B@fx+oK*oJG+(JO65l}jn|(=eg5b8&(A;LKQ>%X5N^|i z>_;zm9`c@A#cB4ieGsz)XUWqOrzbIvWPwy}mX}nW(JmWNM*IwwV6a8$+>-ylZhx;b zeyuOmO!s--Oug+lvcFA8DXooxo741qZ>vyP;F`2;Pv8a2Q-bV z8&)@PE0H=Qx8?tUk0&uY)16Z$PuiKn!X$i~BsfxB5(_$Fbmr;|zcaRxi3pkOJrX>j zx{ZFGeKY#=()8T)>ip#V;(RUg<>C0@`1<(%`1be_;|k*r;|Aja;~L{0;}+vFmH6>h z_C@wt_I2uM>SgMA>Xr71_J#JD_OZR(r>ec$m`o;Phmgr#q%EBvk-7{BcJpUA7 z_lW7rX!n5W24(kz=^8PAQtEl5U&ReGe~;-_>#6#U;}5TYaR21@1BY9Wr|vg_KY||t z{u4q6L~_2ReyaXf0qn}S?~Ug52-pAyEl4q^bHYIB`S?y8$UC-cY5*k4Fb#vB}!}MXKMFW zFRs3l6Mz;}t+6Kmi9Lox_231UKR>>^B zCoI05UfVOwI>~Ha%MN+WN?D+4%o1v>N}1)gKh_X_l(|jAXEjx7x7YHm>Ho-c`|fQh z&_bvaUrV*72IzaX;WI6O?>V_2srOrot-Bi$z=5_X<}@dYDBFapeS9F{n;o+oqRgFt zFaq*LD~jzj*=;!AEQp%pzu^E_HmAdDF4LUWY4$(*GFLyI{#>C4m{SMywwcR^$r=1i z9yS-VlewAXOfEJ9^FP#iI>fT#lQTh!!;55##fx{;sXEjTG!Hxv6c6{`!^hh<9yj7Q z=AUApW}j-Gem>*w%f4c; zC1@l37GO8RX_Vc_ppAMm)Mji?kMk|8)DWbN;9KC&5q@mxvLxupBy6t$erZC|W$7Gp z=_G!6LX!BTb8?9#aX$-Xf_T>=42fJXb%PUPT9LYGLb z8e7QK+0^OOzafI~wng(RnkUeINb2HNndEV3s5J{}6PffM#s|1;*xMpaUOPN{6m}JQ z7rvC)`%cm4R3u0%;zP{^w=*+MCi?+djU@Plp!&%A%6c4{n}2o7XdgQNSsEaQAd2u; zn*bPjzZjU|mRb3>^=$+(fI&hUF}J0sOsMsp^kMd;^=bC4^>Myo{h|Aa{h0gM^S|jY z`MYjFp+cpi@gLy=0tX9IhS73UUlvxaL)%@wZW{vg{Fm4-`(HbKiuzsT=~Cm zzHPqqyz#tyy?MPqzdgT0yg|H&mb5PMTJ-{kIBH;`EvHvPK8azCYmI45XpLfxV~t@= zV2x&tXN_e|bb)+8+g&UJ=&ukd32o*%Rmli_ZTa+`FX zk!ijXqw~F%2vDM?-+y~CdFMC;*vuA!7Bjs@v4_l=qKD0=Lu#~piFk?l4{-Ij$oE)_ zXp4BENTFHL=7ti2Nr~iW3M?o_5Th$jPpBR^z%3VyG!VW=cEtjuyI}>v^M@bzUa!EP zNZz3R;DrM7hvxTAui&02+(Gyv2!@uJ@yVD``C+|dT9ReH* z91J{1}`(;@B zhSw{&M`oAQ;g#~W?5XUf?78gK{K@>q{Mr2V^(_Fdsk;eGaP z*0=hd;f>+F_O15a?#=FBNB&VNF-+P&j(pXs5b25#8K{!rB@!BiAqrA$7IqqT9(D?L4t5510d_KWHg-C8K6Wa0E_Nn%p%&s6<`(J} z?iTVE^$!Cvi%s%T#L#`(?Z1@mKR*PRkmdn)rg)vpj!bwhcyeR}CE%`UlL?s#R)fGF zFbVDZOjJ%)KKkZytm)G2Ce^sDdD{f@>}*+5WB?UJnk}Gy)zhn~T@%t=$T6c?gUzd3v_#p{6EV z`mbjs(}T{EFw+q+S@YzW(Es;7Zf${e5w-aD*??1MPREi;;jf5hpV$g5ezJX@<+J z*NKlyVCTZi{MO)2lMlWBo%x;So%>zf1fH1xaHXG6yno&mh)RmQ|K9_4ZpkMiD zx-aKXwBLUGXn%x1&Ij!ux3}yNBp4zzK?)zmr=%;6eZ8T_5F!{MG(QNHEAcDSEA=ba z1^9!#%#rrU->BcK@m4=+XmV9%uQFU{-V<%0xCiALVQpZzvms(b#fp{Lp0AU^K8dAT;=5AZXxc0BRs&Kw@BIfMTH1f7*Z4f7yS~f6;%^|Eqt$|GfXW z|7ZVU{}13dzy0_8&-{=4FZ~bvFZ?k-IREIsy*?pb6CDbT`p12sesKKJdwYB$x)wV0 zAN`2?Afrf5lA|d}f$2y415|ED&agbCIkJij<%!Ev=SO$WkUYgXGIR76iOo}+N0-is zKx=@`lhG1FhMC4w5F2*MklT}FA}G<2eIghum=qgsON8nSvUjiaKZJ4X-u+(e-t1oO z2vQ^wnh=Gb;#<-a$F<(U;|Nit5Sl;5AH_HDq`)I$2x%Bqz#SUfrLa@?r}|I%FJ&~1 zf?w($mEEfX7hl}+0Ya+FK%ju|lhvx&g%L(o*I!JKP%XY1@)F^7ps#`G5(#P`&=DCQ zK763i5gQ*-3Rv%3>{#M7!whel!>ou~9wZ`;L0-i53m;TS`^g+6eQ+rVRq5b$yWH+7% z(=Wg6;Er&oc%M<9QC|ul3g1$nQeQJ4Gv8yMV_ys(4Bu2?UIViaxDUAxx(}NVybrw( zzK^dTP#;nT7#~z079Ut2S~RICy0T=YsY^2FrSPhfG|4GCQ=~v&Rc5mkUsaJ?cZ%XT z?MB+G)W%?di_&(rG0X4*U6Nt_M zImB2K2sZ&e#Q9MGa3XV`;;qY}QIn@KUv;W3N8w11UrsrA?Fg0wO*J^_2%7_2K6vg3 znPUYc0csL?Jnb#vp<-yJvQQbq4jF^Xj!OX&5i)FZeI0FSsvbvQh?3 ztczNU>qQmRn0#nJxl9R^r;y+OiztT_f)EORD*&N}h7BAlfTo6x3Op$IL=7WbdAgb) zyV~F_VrDHKJEY^tnFD@0{)tN_0V@-7gx1i=lGEMTz&%L+2L zmTz6{chp{0W2#JFrMu9$&+^vqE!Vleari_ger(ul`bsqqhB=M%lx$bD}+`JE*uVVKf|UEa(Cd@1qgPa)&==t z2UPFlc3_T)!PEu+*oW*up%lik56WH@a)jH4{RkAP7_f8X1FpVM9s}agdx{-!zrw!m zed$1|QVxFI2lYfI3<@U+;pGR5A2PY(W+x7!+LQ5w%?qO(xq|1frU*VQfPj$?I*t7? zhHzNsG~Hph&GHNPG3Ip){IKx)|B}99*~R@G^Ed{D%_@kSA2UCOby$5p`(zL7V8rc@ z`94OJ0#6}Dqb!>OTPkU#teb*ZDutsgnF3=bsiUl!f?y`aqpT>c!gw3vwZw0h$Ekpp zZ86h)!n4d8nQ5BGB}OUHIosehXlgzZleOJr3Ck9CQ_xdp$6IFOeL`{D*R5;R-Ns*?MIx`OBbd!=z z$@nK8w@{NZ&WYRPNRx6miEkw!G}5q%L+lhZqH2pPCDaOH%S*280<*$wOY-b&z>R&$ zmYr`_#AWF-JJ+n(?UE0>5Q8w>l4uDwgM9QtQ42PMV$?Ef3ps;?+%jtmJ%iNrGHeTJ zt+?#MR10jaY;|H%3vaE6&5~{lXRVm*Vpj`ctrFfsSPP2XOjYAW4P^DCb>mhIV)gVz z<4X;w%>-`aSPhKLRMsM84UdgTc2imnhK)jYBWDeR&G=R`Vh!1k5#HsK8n_=*U5&p= z*nbRqH+`w$`Z4^{8jS+%0JjHPcx1EozYuePk>qhPZ@^j5?-@8{X%fH$~9$0<{Sm{O(`qPP;r3xsP*D01`GybK(=X ztxI=d4Q!q1&>EsUPrV8AF6}kibp%}D zOL)f+J*&HbRvr)*9uYhgNbw8&YOJrS&-y6;NdIX6P$JF*e2f1zkx=!j3{qFABG*Wx zoBH2YfTJy-FIWeF3`|@JcvG=QW6o%umD}?6jP1&*bXF-Xvb+?%Ng|UKrmC!TIw>{( z*Tg4H1-dFMokU6zuoJdJRg?xsHz2J5!xCXDpsj$w5{Wk;t^n-}zAK=v0PhUZJ0PzB z>kQ!~ps#@F3<)wI&=46LJ}jWn5E~m&G9b|q9UEaRpwSQ?8;R0#vK3udmLWAwSFX5p zb;;_y6<$}8AvsNFy2xy`$!fC|UssVKJxy=A_;~fk>bVt6SEM#UQ=1NK_zKLF8LT*A zI9hoC`z@LzfbnjR!V~99pw>QzC&6w2*FJ+M-fp1BzJMo@pfJR~m^w6uq=d4HIy{CH zg|dt~Y^tQ0vW_}psuYH@ggT4{r+-%e%=Z(b9C)g68b?{+kvnGPsGEaWK91uksT!6- zaV+fxEVv?1l{QliRi{|Bl-r7GE3>oI+lp%|C$N;-igY(O+0f>Ui?u-85dZQ89#bp_ zRZ#{&<}u_~qK2eOy09-&blViGD1kjEH2-8}nPKE1)dtj=75;x__Bp(MFVj^k+U@A8&yMSCWX1it^TtuqgY>hJrwtf7cRwo%sX ziR{DJml&mxb;??lv5a9xltlJ%tiLeImQ?mNdotESDElx&H1=U^h3@nFUH5fA&;4A_ z{B`D^^Ut}?=X}23@8|t`_2Cx=%awNhw`ktiJ`oE+UrKs*AT)8X9N@xhG0M#AjA-Eh_i^B z2xUYdf*o-UA&c-q2qLCnzhKd@F4$|>A}j;;9aab1g(blTU}dm%ST2kRYlR)dB4BzY z8MhqYihWB5d#xg75Kan=-u4!~n%>mo{KGdOxF{ct5=rl+g!YFmv2;q`<|T{{D9@!uVM3TnTBOT$`@OC{_SknhUqWP3EKD zKnI?0&(k+tSboAz>S#ErV6K@0FI1-JH>UU&YEksCDK3R-6oX&Gd4(dv#?ME%P~yTy zCL>BHImf@FvEiWqZrClvCgt z^SpP1W|^Mx5Cz4I?^Uh*0uYT?zAYb(&jy6cnbRphIDH6Chvea zb*sU0z9IJI|2c3@iZ0*+w_&1;3zY83l_jpsH8w!XaDsEu4P3q!#=zGGR^Pa6(qaRX zZ>%Gr5_QL!%Kyhb&Vde=;JP@3MFAaS_LGV_5Ls2L$XN%0C zA{77M++}oyA@exAE`H53bljy57ff=-s1fmdb5M*TF?)}zy7PgO$>($1u8MBuuH|k> zS0r8rZd06<-Bq&wI%wqRXgPhmFI2EusTB>rn0ReCMA0f)N=822*4(EkaoF~LagCkk zCE<^%6~c$?$0O1A3~KL!8OCR-)`CU7k$FG)H~6u%F45=bt6Edmg>M zlDKzn+ihc-wk7_sM%5T3lEdb|GOBP{>ZkfkO}~)4zyx@817>30y4YmDwi|i3a+Q{al~=JW zPi$HS5BkAeSt}8SjCozae@`+i%CkV@p2B+cB5A#WB?}jt&vfsi{nJnqv4L|v{x|73 zgbotJ0u;IP$BOm$vXRAPdu7N{C#tf_F8K5~a9fEl7syg%mc5dhe|k4YmlaWO<*)q2 zHb(fmYie~QRk^7|2p3;bKJO|uZl)8%=~=vg2A#!u?pmjHrA24#k4mI#)MjkDYdWaY zKKJ{0uVT-D(N)*2ZTghLmQ2Vw=)}|lA!FIUob2*@^0y`<8%rEx9CI8~9E&-lxw&u= z+}f)o9;~dAgUMcf)e94#(XUL~Dz_YE(6bX&Xm&4eRr;a9 zQ^Si>J?JgP1KjF`SjJ?$JKxcB<`Vt-uWK~KFOeC&v(m4$C<=|)>v^FR9dwd?LWMXw zH|o5{U37#JXjJK%Fp|8Tz&Zcv;G|9pj$CkXUMBTixquiZj>SK2=Sw+`>Gq0rdk zI{e2HH!mJXe50>qaqi98`*SKN0gXhwB>ZOai|mOn=`I2_s}sBvXrIg>s7?j9G&_}j{Dm+2b1&< z^Xg1DUImYW+qXc1YTlO+1)BqHD(+;T?8mzgE9(p$-^!1W!Ni+gi{RwbIx^;`2#+CK zE{<<5Dum0$0-XvCVg2?qeDMm-*){XvQR@@Mh4}Gvt$!(U#f?kWO})tBMSE(+j|_v5 zQJRcwB%R^ZrZxK8{U3|g%s%})>sZbj;iLFt1Fh?#^!KDDqcTbAReXxwvK5@=o$o3{ zSbB8wxf55VSbFpVVj!ODR>AxKjTNP*;@U6&RxZFg@-15*)Qe2X4z?Jj-iTl&Z z0bF{5|2E+1RUfEcidTg@7UM^VU)C1=w;PN9K&0P>#9d)r>w3rPUQYlm^YJsvL{*PB z^)u|UpJL#+Dy>xogamrx!|F7g1(RN<4{B?NLs!CRt@k)5<99EfE5B&ca|4?IDP}gJ zF+zKfv@f9Jp|0h4lhwfO4+k^!u(98iqn#7KV1;={AXPJFj2wbGc`ZcP1*bNbsD7e9 ztDNM6(azl0Y|TZ^31a9o4)qA8siF78{;2uvRh4Go#<|E-Mi{|ZWWs@SKDR`d1839Do@Yg3_fpTfh; z%Kph8hMyIXc7B?g_?!VaUsL|bH_9R^7^@D&yTFmfxQaD%IHIWvO!RD=@?+MF%=zf6 zSD&ohDZk3=AK&PO&+}EPZ*c68r(^_NYx0<`^3|wsR{mT%mm4Ub*wClYZfS2$&- zD_HkBiYsH4z?w%69PfBG-EFq7&2iTplKgyu3uBQxcNt@z3oI=#Va!~BF`5L%fS3}z z06l=|q*yH|ALzHHAQ!X_^ukjR3u<9Th!HiKe3;4X2q#VK$WV7gktTa&+%_UYlR7eb zJfcf-CoPb8NNJ>7q*0OzsfL6i(SS7I7BC8^05t%LbPAvWr+@^&0{9Hb0q=naz$zdF zzkK4So zbSTQF-SzNT4Ltx2k3}rn?c0PWB9_$lEyLqy7lZem!*R1qoSh!W*PXcR2v_}`r_ZPn zY`kdYY9}yaDfHxnVh1#0WcT&a$u~F3ZfMYubA_fCfw+f`QQGB6W#6pcjy@5Pg3W2 z)K6PHsl7VsAn%e|6Eul^7+H7YW!~VZ7o~bf9GGa(J>)L5R(v<5GxWbI$%FeX(N{( z2p(s&o!ua?P;We{0!0bUVV8R4#8U;)i1MgFDvlgK#U>uH4c18@O+)&c^>@E@Kgb@C@dO#Y ztr>Wg|Ji0~;RCaY)By1x7JX63=i5=+&)E(8qS%f5Ok@mYwyX}T_hnjRH^|TTrNd)q z|08WcEoyAfeD@WsFCFX2rmWH6-q|Su&)KPp*c=sYCv|ISYYpqD$3JFQ^}yVD%h~#I<;WAS{~((}sHuw+pd_ z#)Y1RwuR_Ic%g40)T`7c=sZTjZ1Fl}N)uU^3Xq46me9O%q%f8e!H79K+QET1d zcKb!pK#V2d=Jr+-=Pto>P5@jA&|_`m{#b4G3)-oL&M0XhpyK!q#g2 zW<%s#Y>D>=Y3t@27bj}DL2EfWERMYxiYTmh6w)sV&8 z5Kn`&!wc)eCnMQO0lp%fEHcskQ)h+R#wN4Zz5rd9*b94qanSrIN?}sRCHu84S#c0T zSW*qG>?zA!Ud@?5W-{gX|1j8IieQwZqoWI%T>E(9GHct8=cdx& z%c8^Q+s-&m#5x!0uqC%M)}tIYeK|08^UCKs4V>qfR<6Kb()sB+K2*Pek5evZ1mzC@(R^@jimG^Zy}N=PDgI!9M_we1qz>FaF)CmjNpZ>)%67mxHf7yP$=uy@5nHy2_u!d7`m&#AH7|B}vq>u=tlY%F?27Q^ z6Jtrm>7_xSQ4w4%*vJiS1`7_@jK!BW4I<_XLKpYb$f9OmuK$U~WaHie-YZD>!bK^6 z4F-DxeSU@}*r^}yrqvkkwHP2^n(8-IigxalCoPFkguF}WAYi-J2KHFFHPWq%|lbDoQdVh#Ai3Pn%e^pMyha>5N=C;j5 zvAxV-sDt$gW>0JQ+S3XF^KQ7=9=04`ykOrT5B)ykzQFfUs1SR{}PYy#q zceoDF<(&533Y3*D9g9zWaK&CplmnZg3jH#W8)X@@&v!$1`JASYC8zWj#rT^~IX)G( z5Hp(*vb?LFW113HwU&q{F@RuX$aVtz*-}1iwq#v=>Q$`~!C5-=;h(V%E!aG6I1cS_ z;OSqvbh8j_+)Te67DHviLG;{Zwa^v3!I{ zhnjS?-mzg@pxJ1`M%pzNRTU1x-Ci=O;{{VJ)%hAltV`Z=?BYc!B}4OUvs)P*Zol|* zu&^X=(8^ju6F6hSo&lTIQ^;d`kyxo9t-=DDoKD*%v&UW965}E@t`4;$My0hw^G)l$ z2yhv*rAIH2rnUhpo3?tf+sv^TQ=<)!3hBKPjhLjOWo*R=@rw`M(&T-7eVE%*9|?@N zSd3m1CREl87Owf811W1Q1dJ;BvWAzAkdQf|L$s#m_GNwxDv?SPDabYoXU3~o=#6bO+%_+Lg(TVfL zBm|@clX}8`tk=M@GpeBX;Pe2=?#ek`#T4laf`(8hD}l6xqH|bo+Vxm)jOlJMRG=oe z(DHp_flg@L(On+Zxrx}D0oU=jnm%H{wy~=D+F5Lklb11XW$I)F#O{}>S@f*xIyKQN z!=>`+&4s5pzOVD#>*sIRt^~u~85s@95A*O6=pGz^_MdDoVw~?M`|b(3)Sb8;*W7)X zrSCOxa(Lb>{Cz;AMnByC_xL$ZG%H5?HO1dg`M^CaZ$}H<)J2~zF~3x5E$9CAXZoLy ze*j~BZyqlI004N}?ZGh(0002Pprrmq&&Cd*00000000000000000000000000001Z zG@kFgp+S%U0C?Igoe6kO*B1AmM@k|@h#@3~7=j{_YN#nCF~m%WDaIOtR@JSR7B$qY zxZ10#v(gxPt0*mc(amPbVsokyQ&DpoB8rIgQ}z8$zVGItlDzME&)IwJwf^hB{%c(= zWxL4BUcA@P3(*!DXfw<$P1T+{Quf(Dw2LO|Y;)CXZu-b(4Ur;UXD7Szy4-lK(2I9d z-6=0ro5)*szn!8>U1ir#?zz(Piz%|}-6@?UCB2tSls#fLdZ9L3GUX@jCp+!yI$g_& zXq>Gt33AW8Bc*M;7pYTqg4bVHcx7C?JkmHXUOU+4ZlBwwjqE{52HQ#2*U|U8H+O9ldbJ=36OIJO@ojtGx+S^XomL|hQ%L#W%k|o)ztv#j4jI&YBUrLI03Kn`i#CIg-nov~}s` zMRVJ#8>OE1=Wc^}O;b51V#@0YQ&R?+AbU$D$~-ela_wGCHX&XU2{PqfzI~u8^lQ6b z)8tF9zqFOMe9{n!vSM4?U7Bx3xGP>1cRY&Ki1Hd}jEx20F_91GqO0&%Mclllwucpg)VO}@$fvlG>ea@_8 zjYU>!x=!X2~x*;w$*mFx=oOW5^s$u(&jwD2zja-HOB4aeOqiTudb{! zFG@W2(M|iyd^gn#b@MgR)^k1VB00gjB}xuE(%oj;BkpIbUR~bvp`__6HB(2M zc_}*B>*(?{R8#FTEMGm$3RRMZn1 zZcV6A-dnzv++8{Of#QJg6_5ZG>RuEY3F*wHC+PS9BC>k=COP$|I!)~VH>$X`&029 zN5~TsE;BV%UUOwMOIyhiyH-wmplEboGpTQownjyBIobbweWo$WlDuLL~LAS=_vi>L8!EE{4Bx*-HTIX=J@=}ts zoN3~fQ7*4kwn>C&Z)j&Xft5o@zt8)>@PqMu^hS0va4$QgaX2Ffq4 zhZ)54_wlOR&%F?P%o~k`zsR%Xc;)dj+u2jjgZWDCVcR2ZfRx}2_H_T%0Q0T+Ld$!d z%q4S4JDFJfroAo~y?*+kd()Jb?OqK#P{cjP(;V@VOrV>BjeW>!3;R+|?I;u2i?H#Y_R3+F_SALIhEygle?!G?zR`F6KxZB+nv)*I?v5z zJt}irugeAed$5djoIm}ad@sLiI5DY$~jkiv=~&9r5(IF5@1%#Z*tf) zYj@!RJFtIA8s|Q;nO;LqXAA$3><2~CGO@d^nR`sK_M3{EELvrysH{?1VXbFC)h-lwd zN8?KhWh^_BCS%QMc~(1sHRs8EI~sg7-qiD|+J(B!JRwSC60ghIo8~>R(OD41V=ckS z8s|Ohf`}lUbcMao{@ny^?3Sa%%k_BCS@tlfC0}~8La*3Twz4ku>JbeJ-5s3)LQ2B> zRR?_~kUKO}LHF!)80)xRPGh98F?h=UQiH;B5`7vR~gO zwjAf|HI-?2i~`;0r4s39m=DZ0+3j_)8)X+~>8g2KlQ>lu&BtbfEb|)M)nK|L{f_my z=&pfr?y{ya`14BMU{i^>dESd=A5Phs_B>Ziu${bkv2s#Y*hHN#pMXY&xdh3Q`kL$w zlU%Gn#?&y&%v`sJNOfN$u;GakXiB;s#ESFS9M-Nqyef8=$#xmocL~=|C%S3cOvLum zMB-SyjWQ1<*DEQjw6$C(##h%>AfhhzRWHhXtM8alJ6?O?f#N0Fj&jLzR71@Y_B30+ zb`#jS6ZS)CFX8T*iEwA|hMC$_8*ys;;LA_QM$>@IWS7oy2Xuz34sN(f%&x{h4mF*L ze-DX_bvV<}Sny`~NFL}#`Q6R}kMy%qMC2Hq$aUV(iRQelcmD;G=Ck4M*yI6P)S*Xv+LTRJ_)8U%p%G### zuHM&Fum!Q({l)7(1cidN*w0fuL592`BfW-ZF8+t}1D>er)dUNEXaABn+1YosxwPR~ z6TEuT3~$;>$J=e3ma&c~We0*=PU1`3+M8f<^^$C5_oo{yJHb?Qu}x=cnrQIweK5&F z&T3`prL(|}nVjWZ{Bb_^{I^DNUb2WAqotJTVk%+1?d^W7Ih(jtN$=`d?EgJC*^4tv z?5if$9db2nysUQC^)!F7jwkJN?s@yUJn84Zmsi^>tKVpjHGM|H_*Wp8Mxj+|AM%WU6 zR0=G%kF_em4#93l{L5nPNWBDtz{CaMf57X7@^Oy>HgJ z-#|UV{5+9Wiq$20#rz6NPWR&N1^1ccxG5lj8fLEPs0ri;)wHdg1c}wf5*Lw6#uD+8 z3VLc6fEXRM1ad4b-XR-1Wd# z-An~=Y!0V$g7%R0pp{gvR+alc>4utNoD?jO@1o@ner>9mO%%?>!s=l`7iuEwjj+CbBCXT_scAtBURHB?=FhZ1=Yg zm!77)DPfLEPuthlaBtxM+mfYhkU`|xckzQKL0d1l6nl$!Z6wppmF@VVPUI2^c$otB z<%0gf>!x|t<&yj$yKMuP=%simE`}T-pB%2W9ZvjET}y7d)I9bY!NJ_&9do%tl(zDo zmuI~=Rwowj;#nKzwI-w7XU6MP`?{SXQBs50wi3*~UNXEGykG}!v^``GxRU(#uq-jL zvKr6v4tBoOZsxbCFe|n3rZL3Oz3g`@Y_SA6V+rZv`taM^F2TNU7V!OPRvmjWi@Xp% zqm7xO5vHQl;C!vZyY=#($G2p{^o%6W%QaU`dDgqKJCCPtX+ynayhKk}n>Bo&Zhp0w z@$L1o#Eb7WeR%3SvYwODfx9ilc5B%=j@`BM4>ou2naA&wI#C}S~4~M#VaqrV9DoQJ8&>Oi#=uYq>*-pttgK@d`E`dO5WBeyGP66i%ytM(nlUi zOBZHdmgB6f*%*c(LqU5qHs0yMQu-OpaHB7;#nB{SnHI`L7>7&^kSth}p=8cOvC3CL+PmEg?0P5S z#bGJ(O2YiDlPu0p4l%nvpS1!U5=VUb)WX~mVfLApaCaxok9LQQ(c7%i37te1-@v>H zey?kT;TTV72h&JufOBZEK3yuQY6^y4TcejP1=KgW4*6`99iR0SEp}4%SOI zaSGDF0Xx9-HOxj3X)3Y*3$9j%XX-8k&AaAfooSkKx4CvRE0FJdA7WDmr!)p{SVK<9 z7UFrTeIJWlN%oavg4_)ASXUJLqBhzayp;z>xYdjFQcVq;!X2yLA!m$q^T>X;f(~YB zHS(iI8SsFOP{5yeQ#+h zgOO^&v>75|AgI5$Zvn;6dWdZTXi(VS$UtHd5>kT|{2p z8Xn{~t7f(-DQ#iYvRRW*dllbQA72%xnI_le!3?g}m*tTcq4ngyu7&Ie8OhWo6XbKr zT1o=6J+|50M>sB7bG*TF(5vqH!9`Tpm*Im4;oJM**^lA%jb6|TCeLeaKR4UufL_Ck z494SvHOS~MaE)voq<;3%2XxRABzc6)x(EI{SPIQ9o@%o!(URoOQRHRWtp6{XLT-`5 zb=KnH8=9#)R?Cu~)FNA7s!Ju-E_0p8)Jx%uyW-E*!wDV3|F-~@&nGimTFetR;FIr} zTzHgFa{RjPh<$=Tf75+JK7N%5S^}OdpBNEg)^L7;q`5S+Gb9Wf{aSjuGq9r5`B!VL zz}*Cs+3j*aYOF5QGV+_7LzWQ-R+%PSIl;5^G3T-}*9!6afQySbOI36&5hercdkjVi z=acqQ1(wr!ESAm`xkT9T_hZw}ic_o4d&F1Rdt@d)#od&pgv!&bMzoW?mg z8Fxd2?Pb>P5mg6#2i!s~xm}if1Zr#VTIt8m&#q$ewFlv~E}1Hzh4tJ^Y4XTqZN%=J zF)hpjm&x6Kj&AIGW1ba*f0zzp{f(0l#BbZ$d^bbKVtWO;%Ple)UOk&hJ>w)5g7)NYMLG?WFwhEEzQ`*jdFx}uA>NhT52 zZ5r#4Y7%7t@iN+*wM>>ruw*yL+iSQduD6}w z=D2@TIaux90QZJ+-k&w=yfVDHx!09y!*qOVdFepTyWI_j<@}b6w#cnC6Nn?X$?i^3 zvq}SZr+_rm&8yN*-*zo@4m{3FWIdaRtnd2R)o!CwQ%@r_ne3voO@|q5!KWm!gXwtF z8{|Ec^dH#IXmE8Z(QP1^1KFAx$ew4~;pC=&g6(4T3Ji6M{N#2}M>t^~=vDani8{{3 zf&GO1GMG3UMuu`;e|BeG5}f8I_`vDz3ehMKxiG{B$h&HREu+}2 zR&XD`yAdEK@-|~UpXYC4c9_BDW9PvSB}rdXPTMFf3vB8WeDO+c4GOzuPEyk>>Ec}@ zv(jX;)_16<#e!AmntSAe^YvvaE>UD!orr3K?0(L~BaI-lyGNxb6igFJoL=CcEAM$7 z4F|Sbv&dnum?_d4R%nM*la}xxO<)zi0M$~L^idy`#&sa%60H7x*rd`Pn1(vw8kdYc z?f?s%<#eyb*Jn}wKJ}Jdn1+_{o^kl#bnfo7%aeO}|4ncfE8S(RH6Il6x*T;C?Qu|s zky5bSg?150Aq8$Y8u# zzWodjwwTx!YPYh+WweKgA)~D04(V+w5zX1vm0m}1TSaOfLFCDo-D`cm7Y6i_-GSwd0v!z_w@I>J8kkc!pt)f4SFuKRms6a#1imJxE!NClchu9FWM2=)1A}DuB0CUbS<7>m8q|z z-P`&OEOKXF*^ZcB)2-%rp&;Q1c^xmel9h?WqxW%}^tgNE=5q29^f;BE+*wQ5`PBG*r>&dDnn=9rD{%b!BRU4ZD4!n$g zWeF8D9ZWX*EVb2S7ld67wlTI79AiuNH4^;w9VhoADl*}mg$yql|ItjRdqeT7KI@rD zEz&NzymSk5-~@_{(0P+LdBc z@R)^KXj9C5{OD@A>U^ZN*Xv-1kR2W5xdys+UVYHhWozJ5CK7$OlQ+Sq=zaGt`*zn326u&H-&oC;2B4Zk6JW=4eFv*L*soR*6Qi-=0Na`B#yN8Y%PWl!4WdeM zj%&`LK0TFBXhS}iY|Ef$sl?qB*ys7(aeJF;*Ho%N5w@4?=82;5No{2^8Q)}hsZ#jU z_p~&zaFExEiboF}=GMblUWMuULWkIL;O=yA;eJVgMIB}ClL@sfzURew@}KnpRkA$W zLB>;GZp0a12bQ?@^5Lwv=ngFiZK^}dJY$FE^%qa4YbgVGnf%d@K zp&)wWdSbbosa#~5U#KyC1-3bfr<=!_34#y&R33Oky^1cJ*nCr0yU+R8Fjvc7A`7Uf zCER%phMU`ObEqej=I)R1{Rk6j#}XxPs(}yfPKDz!m=3-N=A{=IQ%~@6ca!gyn}>Me z-e_!^QUlAv24M8SB|*&gCdWstAs;}e=vCliyeQB-e2K|fdUyjkdzYJm&CA_GY! z(x;-&=u9p z22ml2(z4jtDR5pjILwi(O(Pdirj)|n!-vCsmLtEt3)ftzJz-n(y{h=(5$-YPWQRKh zeh!7bc+p~&x{u#{;SI*egc658_R{#wA>>aj^$OahWq6f}JmD!b1{UiLx1LP#7iI`8?vKkOaaKT z6wJ+I80To$z{J7oq}i?bnm9Q~zTV8X!K(K1eLt-CG+bu`;(1MY^8Q{6caI&&gB>p* zha5!BbQSRp9PTrqI*{s4BIxqX;`&$~d)}?Z8 zEu+8}I`BzB*voqFCGtnS2s_hXQLW`ZQg!w`0c-FpcPwSt|`0b#D93OAW-wUSdLO#KHzdr8Mj}*Xx90XEGde8?Onq*AQaEKPG}aqzZAnvKOEa-G|tF45)=q z;#EmRIM6UkpnOpM1g|oc``@TpRrHwzs%kOz2Hq$DM179zFb+Gspx5E#TY~_r!f$oA zpTnG#g*OdCkI;#Xo}F+3<}}|&nFd&Y2e?(ia+a90UP-SMI*LNA4|^WUD_)`QAE9Ze zi$<^_U;9`eRFHtmF^-da-|mMCT0wO+ORf9IE=2uslKsDr@4xB}nL1d?edlBW9NI}) zMFlTLGhw^tf`#Vd87Ast*$+c<-;RO-D-C`~;MCl(IVf(1!o@7JsBl2)W2t7eq7D)y zshs56?Cv6YOTOfU_P5_VpTR0J_I??ryHD;DU&^*V(Nu__xMn|G9XR0bXU>4Q?DzodZwdcZ3LKTl;rWA zGpx`;&RUv104sKy$dd==?j=Ks<-JTVY<0Vy(^bNAkY@}=ZLlBK_al%VIz%#?KiIt= z`P@`+?FQo5PBX(j*1hf#HB0hOs%@#-9SvH2bJR=)Z#*QI3ehy4r74KI4(~3)wibQq_(y&VQdJ^;(Hn6E5Ur||UpmyWZ~;#I!^O@zaR7ob^L zME(C9r>><-L;scqsvpL)P2$84aUn$768N1-c**KSmajm$-DRf>aZ6ac_W0^3(8DS? zf(5$D^J`X@E%}J)j-Q-o+k&qWu&+#faS70NAI@ivR;RMjlsHmIgiNH$n@H7Tqk9dF z-ASFnYd2BbKP#_M7as+F{7t{ccRs_pdg#h&JUcMY{p9}(Ki`7-0=iw@qkFL75k#zf zErrr2)BFP~c-#ErMe%oYNy2g}dF5f11zR~ucK18hxI&kpM!dyox(cQm1A`EPR(K-% zw$pGz5vE9Bc6 zO5KHsK3Km2Rga<~6^!RDLLGG#ODhLY_bzK1Y7U!C@LXkhUbKze-#Kw!bu8;sxY}MO zj!2It8c&zZx<9eiyl$*fXBkaog=OLOAQ7_*|8>!Xfr;QE``MRpN+csM61ABL_pYRHA;@q5tc5{X!ud-fy<@jqrhEc8-%u?YBqH{1d$ z>JL!fzlq+Vk!_2`%p-26Xo2=+#iHF56z*T5&0ECTz2S{THP#)S*!NiK0kocBUSquM zE@HHLWv~p9HS%AyQ*~@2eELG3=ME~t(l&)k`538e_i%PIWP)_W?>;8i{tEqBYH`)C z03=)5KB3C`4W4)eXnz_EemfK~N5xm42HG!Kk(E?vyTg!g1Y?wwKu}AL?T)G;g_AO! zj4##>ME@TL3eF=l=nDrv4itNjCplwpxqqRN%tklxlU#!(RMc(1;+VIqt<<&}3&;MJC({|zN~w=hY~Vf-Ue{hWqF z*~1RME0yF$nDt~78sTOY(J>BA_Z(Hmp;SdbKy&kuvs{~c&^7D9BSfH|smcmmx39Y8V1_ZE>?2x+e4!m0m#cCNdr#uNo4X{?M|<5VKM|o=TzN@LISMt%hESKU{&g4YrM5oNaG%>mJ%1>lI@H~O>-d=cn}Tl&(Hrd22E7Mw zI#;r#dusGBR(3%iq6&mB z!Mhy>H(p{7^T>z_xouFI%+9R!24q%Qf4pX<9;Wb*Kxwh)eDy=_jlWS^ajw*Y6m(IA15Ff>#7 zyFJeqhW`&EAMK;>Vl81XcfZ*_T(=(l;T@B2a$ITjlviYiYea=)JZJJY)u0lRY_Gy_ zCBufbH82&DV@r@Zp9R%q3Y;^wknzOoI)P9_nb*?9x zs25OEtOeh`NTeitrgE`e24h9*Y=o`PwKGsp<>H%Xc;WUJ(+_X9&4u}T2tMta6SVWa z=&^{^Pr-Ea=~%f&7fe1H;@ZT|`fio`lbrOX`JUfz!V*JJ4gJB_=j?82KPxHzI>oAxv|sv4Epy*jo6vtW2|e%wJ%;9dmEp?f8fu#tP_C-{O&mFlnkUU|M^&GI7O$sl z1mWlUIy(01GG5^r^?X=Ts zsfnC`u?*#GU&S{SQe91R8$l<1sM(akw_S4^K@Y9*B0WHUg?yia_PitY_QO13X{>k! zJ};B+(I2^rpy?2sM)YrHM)6$*9WQGUrh_fBV3M+l;`NMQtx+<=i#`Jc%UPYw=PRBgQvbn& zFEX33NCUf=FN3_sUJLAI894p~@0iE#_u+lZhyvTtsvq;lcw_J%eOQff&S)=sQ8w5g z1)JfjsW3rlWa7iTCg=qXnv4Q7fh!z_IjF=*s^@bBu+KuZzB>5o7J2V{Dulx%2ut{m zYJ5HYn`oa6Myk%47{NY-n+h-{JLn!6A~$S9GSF!t=dSRB$>6Xv{9R8rm|&RJug!6M zbOP3LOp`{2J?!t%%Rq%#B^0_X2} zRFYY~O_BFEB7Q9Oo`swFhq@0vEg<1L_}^6gNEGpl>{hGro)6Ku_-fXP@*zBV7<)IC zN=hHE8vMv0`0p%qGau+VG7IAkA^Orgf&Yy+Pt~y!7vO}bhjNm#^)O!Qf7Ge!5#ef} zt_jo{S_2i;Kd7Ec7k?dbrO_vBBoaiymmJooRHB1iW6ehkvO=r7XV}eVrXSoxq@PEl z0_g-pIEZ>+b^E7GG}oxzjz&|8J^-}Y+t$UG-{FdZraf47nL}R*FP%+K(J8bJUCGYY zVDVvKya;+`Dza)%-1p=^+vNkSuQ%M^%cwd6U5@)p`?D5zbq{rcVbpwlX0-yn1izRb za0?~uaQavBWEI@Wr!JbB82pYa<2?_$`vEUF2n2o>ol-Y>Uk-o{BT>lSAqYA-@J@oup#y>?rV)yaJP8c zf9$Vz9QCAmsE=Avukl&=i70-Lfs4D^UG5wny(LwNFQk!+l!_q8XzscxT)8EAwjrH; zdGrhRp}Xh5nkDm`&wx)f`}o{MGgAM=Gl!ejCeQ}jqo~XiY#aACJ|<7PQ8npcr#mVV zsOPs)ja-Qqp@==-M(08X^|JbCc_Lgc9&bI&-zAg*yHOX!qP{*Xuj^OzGTo>8x}Ofc zx@Z^kyaCkS*29WNQ(sQ?xd3wEji9v+RKf?i5p))|q5imlceT?nw-01=O02e^1F*W8 z$!T0d9Eozrsm3RA@|qC)2E)?_<3q!U;m2)}*T-w+_V5|Y;7b;g&4#+C<`Fo)z!l)r zrs*rN6oRhnIdav9oX;F=s2_^mlGL4)Rc&R`!FXlKC|j|9J0%`$*q$uo5GS-970a1u z`*y?no}$*;7SB45s_YrMLk!;?1`)qO4e%{r8A?nzV=rg`=dB$og*foT2zLi`c9^@{ z4hFhTy@M68!>FxhdzCrUMY7Ds@;QIfz1F}?l|SSoI_>ItO*uI;z=hL60Gs`4vws@h z2$X60g1*b$qmv|8=%q5L=B9+8$ggJZxc%lO_UA3sR0ZUYQAE>r@*Y*js+_%Ookl#Q zmPPI85GuX|dk-&O$*bvin8Mr6rFUy0ar&}XtXHQ`XEs^WLGzk|8T`HyD!y*U5vwuWR z_&q%m_3hil^J?UNX&|kay%EIt^}K2bo~${&8`tbhV5R1A7i>TX)5ZIZqpz|SIBcDomFPrA;@a0Epi+|bPUParR%&8)}fC93a zG~N;F{0^};tXhcq9h(XU&8{$a=~6C)Z<~vEK1#e9YpP($P3Q=ja;pG1?R|(dWrn>!C`$Pd;$k`5n`x z=v(Ry!W;zO^pJ{fK6&a{qI9~R!`nQdHrCTpS%jB_`zCtqgL$n_d?-(pIV~N@F3Pbl zZDBkbV?Vp4COEc;h}?i4u>jNu0Wic9>=ZK39_q8Fk@zoPrSq)&hiG;V_N)<}vbI4% zN0xYx`tI-aV;v_JXQCSV4U5=Ce+sMx%G+5qzaJl~24eTSM@G}VI6JmVAkBxY(Z z9NAUQ)f2aZZjai2{tJ(pPu>;^yLOmP&_Z{RS1**YL`PP~`kjI0vB~;)f_&Z)Yx@vo z{v^BYMNFzs$J8kLD&l#fkLd-fXez=QJiu!or$aR#t}%@|TB_`aX^6zXR3d^dAoB=D z_56ei{W{cuv%tvp@lr!T$ldAB_iSaq3z3r(3#XCEGxtRWatcM|Iy#(F;Cy@Gv!nDY z;`C4yPW#*|bR%{K!6}$5j6D246}P%Fgr5zeo9ME2dWcunRs%MD;2x0$&E?#GgtDYE zy_;Xtchm^vzm$x)h^%fX+TsAX|1sP}JP7at-R~jhM>4`_TMM;S8JL>YR0b!)=mt@V z48U3kSXg@McXMqOTgAOYPFoSQ9g3a(X?9|h`)wS)zL1)jpoJaoa~SX$0dmO;r+PF1 zl}aXc`PS5>27`rT@e23p`?!I>?2J9lu=ml8#F3A+rjq)M>%{5Lajognu1j^OB2nf{b>WRCgf5NG2Uy??Kh zsZRziwDX4HxlY-##N(=Phc)>sGG9>XYe1L80dv6(w4KPJvQY~?1S5QcO8d5{X6wU8 zHlTVLLO=8GTOjYVJ ze!iv+V4WU#DJGGVkIn#H<#i&}aJokGjic68o(k6%yTz`hJ8dEy*I8RbPQ$Z&LqBtS z`mB$*>aHE^Rd4uYdQ#|-$d+q(#W=KXYfWo-=1ek*b!bQ~Qpw#67Fv(*{M`PV{T&Z? za+K#twf#_94Kc@U891DU`W6xW4LYzVf*E39x5v;ez6kI0kQi1PtbQA`)DI334WtP- zKfsFRizgm^5AoXGL#xGiO^ATKz4k=J9PV=x`7?T3xdgu>sMYAj#5bO#V)X-g=viM; ztYdUGXR-seiH<0p$8&zZ_iOr`rP@5tH=2aM&hvRKu;ev#kq5mO;38g!aeYjcHc+ueSGx0=H9tv5aFYh-M5s1dA!#rT~3c}A3Mt)L|^*~Y($1Dw6hFnl$8&#{ZQvGVYL(SC>O|^HxmK>#2!*O z)l9B{o2QeMYVuyXofd-555t&+qT@IP1Duad22pRX$oqbTi&}0Y=`~*s>-~wHXQpGx zyHP97$4|e;)3w8<>sZhX?A~(v^glL~bMzfXwwXJXAwoj-r0 zj`@mAu?%*&n0~)L;My*ropx~6|0iLL9DsbkS*tOc4Om&e2!=!F= zJJG{bh6O8+LUj>#+M7(VwF%~qUMZfb(Nu!zpIk~buYrwE(r4k91K?zPxtm@m*NpRf z5Z?9^5Z@B;z(E+w7s)#4ssSlfBpc4d<_E)()uyL3#CCwePv%V2=kHc-FgZ&_`kJCa zNlkcKLyh`pl*4dl`l4G-El$8ZnZ9;U24k_asGyt$v-zDPi>URk0(-pM|R4;!*ZHSygW%h8O9pz;_qin zdpM(P|IX>Hx`eWzgL_9ip^yrr=G6vY@B|$-U(_$ovUbLW!spN6bCU1{J@`Vmf)>F4 zR}TqAo5TzPX-Y?pU$3}L&-T~UJQw2k%GnjZwuX5Chw0bpj2b9`>R~hUHx;-TpMN9I zpADW|NL4S!{F{?5=6RoG21}!Hfi(=&SoG|4`SHBDJXbEii31%C$8VR!>avOc0U*0a z?gfy@YMB3H?gDy|{+#5J`W!zy;ZHlEuUX-^3f(7YX$o1rU#N_fU_MHJX2ayrC)HbO zfV={Ucu92E)`bIWhY!d{HPo3Ny{lrV9z}qS6PeBM56Z}V;(4T5N_N;2&3zu{VK}w8 z0{SFMQ-R8~)mi%=@Cu2XjdT#jDXq=V63P3U@$dQeFTYaA?zS*3=vt`nvv_osoPt;E zfMV$e&jJ3BM(pDXlnD*sFr5qmS^dEYkD|kGqpw~kE^Gx^zDbrE3vQ_9Rg?Nu5uU)A z=i0J#F2s8cYy>*)`LN@WW+eQ_HM~zvGN1^0tQv5J2Ev7>;NerGlEu(*>L2 zKJ`xv=d~|+L_D9Hin2eE9@wYoQ_Iq8x6EY_L+5awF0-#AQE(KwkBPNC!AkSc2hAjF zTM3sL=H7B|nHyMUQ)({(aNS>+K&(0de`4s*%fWM9KpS))P4qkP770v33gFKS*t-Gt zU#JbL;+OK#O!`WW-tIT^J2Oq*#zU~q_AWVWfBaEH*!^tk@Bvs;ES~QM9wSa(XaRrU&PTdJBnC#xvXRxU*8A&>Cs zd*BR4>l-k9A$a8xVCoTc4V`vEiMh4$n!7;{HT@|#sQ0^KXPrPv(Hd%pqx5J? zzgsylc`DuTpOa^FwE^JJ&GIZ&yE9b5y82c~H+?#)jWX12TG^hkljEo;cjGj+Fko*lX<2TZY{B0CC`j=wX{Q}6??{e(QCuQ+>Lo)Na;DrLTL+9mt z{N(p23Rm;|u|5-Kr-2}ElDmVy*)1mexSu#vNBo*HYyO0q?`fh+GwZ8UE;9$93>;1_ z`*6lQq^6JvqTXS@(%JZsue3H==f^Gyba4gbIv4KtGN&kyzy0|oh5YLrXl@sJl7Z$~ zkZm5Bctvb`Exl(Q{myc}+RzhPz}HatgLHU}oph!afAQM(Xz;pH9gpT~09wLER14zF zP}sP1oy$|Nhr3)s-YRC0uH|WZQaSw^On=(zg>vI7s*KmnWg_)(SwpYjRtciM*8*Eh z*S>O%C^`q^cF81>ZDhhWq%$F98kNwBoW)L?>&g!+-~pQY9lN?hH+)jBc5Z2-Eb!6>QM;lEFs1Wxmda1XI*E>36ao zEvWt_f+32iGQPu^_>$FXOOOR z%vuRb{K8#l7Mv2f{5oAtPDdVHs=kt+Ze{xse8U~GtDR(Bq2>~6D@>#DC)u?JMfHZS zdPwJCGOR%i(T(1F4QE1F6=}ijoLgwF)6gII>LupGkonXhe@)b^xDmZYfUBF&2b;WQe47^G6f55U^lv{icm;jMJwExDRxWI`R*ZlwLx#O8y(AF za+)yown_MpRMvkmtkO3B~%!(L+H`_^sy-uIn4TZ~* zAnvwpoSPXVEstvXaeM%P_FxDXPXx@iK?WvW-0mgZdw?k;p`qt4vpmcOUWl z^>EeC(OEYFTRD!`e4Y0{hd$*RR&)~8)?gdyt9hAd@je`OOOSpcO6*Yk2YvDPWGk6V zyvFMW*5_GsnJ%Tp{0T$r2i`EN;pNh82K;j!m@|4CWI7dAI@HLWGC({#d2J?Ys5I9G z9jDKBpy%h>H&HatMthJe4f(2oeK&_!Vg3jVTPAhzU(m~*#cyr_QH)}xj)F$|qrNx- zKa<8@PNvp&56wUruRBNGY5)96o}k$!$fhJ}~sq%0M`!gB&Oc2jKf zKHjz*JX(g?N8KrauVttX0%6ps5m7&C>(bDj%%jVsBnxeVRHP28bnym|9 z^E;u(3`H0GhUv-7#Xn>;tawi%PZv~eALzfh!%C>0#=>e(r)RDcIa_t!ISzKA5>=f= zbZ{0hMXHnQYvz!n{7fBg7dUhqSW!7UGs%)}p-ldPs>-(FtA?X8zhl#-6+KQlD7*sv z&JOzls*)A-@&=f4d=;P?@TVVjhiCbSS}r|Py1=d>*Xsi7P}glkkJ}$F)`E_#?Pij` zPv2y^2_||z&;GpPGoSEIPeJC>&_IWQ0;%I#zc2bclcxfy8?|8PZ^9rRMgKnE-Q^C} zQz;A*U-dH!3xL0Xy)evet(BXNZ3zd!naP0)hNIZc zz@o>9-&Y<9pE;TSRzg!U0ux6X;?rV2|Y$6iWKbn5?9ps^hGzbT(-Xy1Dt) z)c}VT@ktYzQxp$h*B;DNXf4x8!qIb#<9aP*v)g1}fuXqs#{#2|A~?NRksBlfT_uMy z0c0=>wq+t6{9{1Umtn7KvQL-rB7e}2Jlg(jt7r$;nOT!%u#@2^nU1)h?7w6!Q7{TD zbk5DPA2L670f_udGz>?I`wwojn}NE8oo;1+V@})|rfdb%gS3K9fqv-qDiAj#sPwER zA9#-)jRLFgwco%*qpHw<@ro0E_XlzE-$dDP*wN!KOcOb+F;bBnJs1u&2lkjAV=Sy5 zr}KvPf^%6!)iwi#6L}P;bPK1v8H_{)&fN)mA7+DpPtda&YeL*%`+#`X6yMi}$hOxl zVLis$56KSh`EyR$#oci2)RT(qE5XEdSRVRHTIv7Xx9pLxoW95W$SmG<)5f})_|K2rhXF9@_@Si={5O6mIA% zo;(bTNQK4P2G@B8OIy`lTGNOJ}Z-ufmuF zN0Q3kWfp&-Xd-JkLkt-XzD&d-Q>CokjuL4#HL5~tp!k(jPp~ zRKcp>LQ**4ZbpX?=N&ut5H67hD}LCp%CM1ZDCp_f(2lSz)UODz_BN@!f%sTzlEwT#s!jf z#&9wov3hOQ?_N$v_tF`LAx|xsvkg4oV32h)aMpG_`8H6&0=V076tq>SqQ7USqgdKV zS4CsiD3Zwk7Hg4(E-Qk1l2DCU;a5~y@dWcBI0MZA7;mog=cml|E3lkh=Bd!@-Jbop zN-a8v)s88imPt-hd;+t0G{$+ zC~fCbff-LH;$nL2nuB^0{kV+|Dw`h8!)Olb>5DK03-QZ=c<)qZ2JRXK7yXAJaDd^kdIfy9M^2+djzyF7DSa8Cc&G?w9d073 z9*4azMJ;4Ab@KPAVC`WZUKNn35Wz0Ox*w*aWF$}9%L}Bod(`jfrse}9gWh-vcQV_H zpeEJ{#%}@ZkM5jY=OtI3RU1x(XiwF73cfRz`LV6we4Qv=GTHt_x#Hc+!Ozq~GZ(@N z#!~^_hTb@Z6SR$^xiFC_=6lg3)$mtR`>*cYm4YedYicq!JzHkAhZ5xpl9&j z`XGUMQm43&G|UZxB|A-I_>}u?D1L4>1AF|3sWhlabvas^bf%Gya{Q0pxf4uV8U#~$ zj;t!%SIscRuq*d)jdh3s+cNE#co##hvf~IAQw0Iu z04x0>es@qMVlJ9u7>q2q`gFW|78S~PI~=wkgy}(kKhkn$S?mRU`!&TH_+fHNkmPVs z5EWx`$pJ*5pTMEjFEs!XPebyAx%qgt3l@*~!HT+%^XH>sp|v(x<-gM1)$%BwGluIu1XDzSH&Z~2VW5epAb(-jOHEYjVIak_%(GrV9Bhb%E~ifr-WfdZ zU`^WC8}MhNVL@8pdBRbaY({aP={B+FQRr5FCkNauK1&fw5AH3xH`?G)S5n_Ep!&PZ z9;2SZj7z44gpo^q8<ZO`xEomQ+RI4L_ob(!c! zf>EcI!YjWAuEPnV5(p>nKBpDUO?pWh-L%4OY68MA^lj^gCyfcnzswZI7Z}VC-TlnnxZULNLJlSL`{+j?}Rfc)7`#}8r zlu6J`h0JDJXsSO?%%3*lcH{3*wb02k7w)$a9UknG zo=t^cE`wi4BER2B??Ir+MFrRyk00XxYwEZo)am#043#WB`}9Oaxg$(q-N}^IQLZO* zhTq~kBf#10D(HMP+M}&J?HXdjXp7z#WO0ECV`HX?pC-GAK(ksOKeGg__hURx70>S) z%tPf>g$Yf=z2UAlImGis^1FD1#gK(d_iRUi1iu0A|4vU?ChHpq-dO`m zDnbYJBK5pruNl~A1bt2IiNu%aYHQ}lcDiwbspT}KDtisStrOAmAQQXmvi_N{?6t{= z*P4Ma!@2UcUCF&4E$&FK05^r69G$_Z!~+5k%EVge@iI7N55Z^eqYj9I3vCW(*a?)z z6k2i(f7X6q^v;*Dg#Mgpbc5jdaH3Idn~GQIOV6I)dmMrS&TzfXMA0|V2F)NJ*d;UM z0k+ms1M!l~@TATkgina3tN0YBWEY;a3Dv_|)HRlp-K}Gy)dHpye2rxt$4}+MEBULj z2Oj+fUT!Hl)qWz+AP`hPc+&c4hK|79wnxvFM>qSl;*MPUIb52rCIgjxKt|M5buEi7;7t#Ft^PRG+}lWV<@7+kPgxHvn<82U9=e>Va;v^j+?8 zCOCK%>{tY<1?Eh1om)&ZX7)MfPpA~+g9u}Yx`o8h7UYLHF!0mNF!Cvj9_Jys=>|BH zH~ek{W*5<=YlfKpba5qPG0YMM&GaLt)y7J%QC)vR4f?Y+BNy@C&TbXH{X;t#z4K1|JvhkpM?-avNh~L|i?5`FOUZ*(nS~ConO{kOZMaPZ ztUQ)@0~|P%Tri8>ufvR#0IG9C;H1Opho}Pr%%k31SFcbb?`Ym92V!n4=;9(cA|AAJ zhS#pb=YFQ2!maf|8F$WWVSktEO#9jn^2vj*e9F|1w>)1Fv5Xw|A-*;YPs5A7Cy?&Jf)lCh4mk1{7N4?3JfyEX34DykulFrFELxgOKCcY1B zYD-k__24UZGM~Kx^N9lV3h^xtrtwA2e>7FQbn26X*^dxB574yJw@D2-(;LmS zr1#l9<|=a@E6`v5(60(&4}q-51!~Sat-vdVqbbUQTO7gvkHro?A&psuEj{MAgO2Fg zbs^Z@b@W0lz~r?IbEr9+E9nsYn2EJfc#3d+SLdQsOa+I%LX@t<H&@Atoc*IXFG%=h^|&*yW0?)!be-|xE)RPKYckNd_4&rb+mkln@e z0$9XIXIN8ef&Qx6!puB8d($eYjB3+wJT9y4f;qauQ!9^7$;-CunY^!tS(Vu=AH7Wf zZBfxEPja)pIA4!TKbbq#V2ZpC(vj45>pa7DcE=9!UCy8~AAs+DKX>4xdgi#E_$ALc z>lQ!=t%9sO4U`?SwF zCl44SmwQ3Y_Bf5s)iRuGbbV&|&N^}MEIx$`FdvVj07w2J-%|hsjKtC|>ct5`sidQN zJKXO zm6$RoWL#KD8Fj{IUX4SG{kR8em}d3RyNVk4sRz^ccK#Q9Fs>CDcG!&-@~kf~zv}6L zUBt;I^vXUFc&y)zVI;}Pw-#jcUM@+m7y0-Yy4iOd*I_I zp_X6mEXBLWKiisWq50|IGLA<%BJ{#Y8C=O?Pk-xFbx!-(QQo+bLUjV2WOwUM~Y}MmaX$J;@6?H~06Nw3R$ z^m8NAePWE^h079gKeCdMdJMk zv6cyMwVgNr_Efu4bzIMLsX2_dzSHy&7mYBRu7>PxLG%zb@P)VmuhR&M^C~3ndAo=+ zv$e^wRdm@S%e~Uit}grkfrnvZUh<7Sr`K?W+)z5#g5cpzJvTM|0z&ybUi}zK(*5p8WgWZG(L`L6=E*0rh6i=2uA+g+xOgzFO$`{+#@((D+?KJfaI%mEY-t76bFQ3PilmE^qU}5HJ6bD9Nx{ngAdhjGR zpM)DGc2LNT+H*swh@Uf?;9Bp0kJVQb(>X8GHhmVOBsN`T0ybcc=#BK(`t%j}$kQ_q z=}K%b`Z?sJFTLwrdupG!JdAqnVf!-4EYR(17|c|bM!Rk`hF|ncvj1O z)a;a=GTiUHgBi|0nk(nX+cNsnz*KUAw$sBHjA_(B*Q7v1HEGJb;V6hjXU<&`yh7fv zQ&-|oy-tbzaHfa%e=5d_G>4#l=404Jg;k9!h4K83>N!`PK2(O&Pbc=``20A`#o2;k zRgruLMcoNQK7*IhT2-(L@3fYDy9JI>=!Y*9fqKA(XFE3^pbIL)(W#dHJ7{y%0(as( z;I?+D{!MRQ4TUn3*6NPnG(FpT?1Ncf9C`uS*$yM1 zpF7meJJk?$u(IY7*Z6>b>AU8I4V9z+1)r*SS_@N=XCEA~5ya&!$aI;hC9xB|5&W~< zUhag8ch1j028S6PT^PDb(VroVRiJx+#+;i2@%k+swS(1q5dO?tU1y`GxY`!eakqcV zzzFOBA({8=HNJ4m&cgS|no$2WQg)TbOHy%T22$fTZX8QW8|-{EZN zzzOZ3%wS&}upW;6_~@^}T|(AWY~J4uTng68z4p@Q4EDcHsjbgfK|BtJ30$;0y)%9< zdZa;VIW8^GcZ5yT7k4m=;Uk&HDc7CIH5SY6yQw9Q%5c{5T*?)*YC`+RacsRTZ7=6( zr|(cZ8WI)p1G^pWVwbBLAF*yu;Jhuxu6-lwkA?I|)Lb{}8CZXQV9mr^qH_MxuP3iY zBRP8AZ>5fjzAw5xR3w+G0|(Y6Y?@x@CNu`K}g*)>#1fgNn(7cyh zM4>NLP`VXX$HRPe-r>wWAh;m=c+p9+pR2(rIDlDBe(GOwyRH)PcB;!8M}sJY+Gf9Z zzaGsrYxBkEMO=$ZeeZ7hN@aVlGBmA|pL#kutw+IC#1lWAc_;dN@al(A2WQeuJe(W# zmcV=U*|99Ue4Y2S$H(26*zgq*t11`$dvRWy=-SrMm3Sg)?c9AM*(AoC=gLZTdA|&r zGN;+Rs1FpUAr;8XFaw4@cpCl7cx=_6@{h2fE5Q%C;Mup)MWg+|kEY>+m3PILTOdA9wMWbo;tg>Q*CpJ+ zRTOE};bvWn2Bq)f9?ZpgFSPp>;B9|ouSdEML-4Il1kL;se%%xnTAH0qdb7%~7w+*P zjGZ;^*@t4tI+s^YV|m3WPIccoMZOAqES*h}Q75Z^ELP>$p~!UViRPc+ zVexaqVkr*wqA8a{(m%=AJKzU(@|9w~A+!#%jftZ24$rVr)JI;nio*Ytdh#R<`;&TD zPsNq>QO~G926K~nR3<$H^3cMssu7KaKXd#VlzD*S3MQ;u7UDx8r)KEzHSIQURCGij_2TxXUu{WCoKG8oQo9&KS|u7%Icm6_JDzg|q95bHkW9@ml! z+3HNU^bYPgyZE``f2*21=vdd`r}t|+e4Y2dBKb7>BQNt|JTCv@jN6jjz~O&J^uDTd zX7EuKoo1N+_fZ^+Vhc>qF&9&vxQ-*)Sa}gmBEHwD^dkL(ed^jhRE?Ks&-hk$Kkp5h zu;Wq>eudbz2r+0{bnNX|I%hp1^%NtDkb4E_@sOxnGJv1uBA* zyeLYo37LQ^T7va9-v1je^DCfptA#;#FI;l3d$KM}p_GT82^c2E)g$8dDf{kY6>~|{ zoZr;Ls-!Zjxe+x;j_jZ^|AEG8)$TBxv$B@0IOScv$|as8eT3C8+!GC0TqCn)zYLz2 zK5q%HxInh~u8CSFob5MQrHy3ZC-omck5}n@%*P)ZE|*x8c`E&b_w=k9{8*9~b&7MW z+;gxeT43sUJ{g=Ht2rxc1|&cJ$*MWeiBvuLJf%>jOUZG@-L4|f$9%m_T1 z=|t^-`#VzixFT+;&$8WmZRDLjrHlRwysRvprGxef+@*nD^Qg>2=~rBf-g0WR);T_G z-`x(Is_xY6$4TH-O0l!*tKmTb>**ayKBw;9fCY6{RrmuZo9Sj}6?-qaqFFuNq*Ji% zW3I1~NH@@ZJP+5b#5&p@R+pX>POkSq6NY{-m-%hF-8dz5s z-|Rye_Z4wX?ouc4Ehm_eu#^5RkBipW^gB5B*;!F-MPjARg7rgm7 z@WYnD2Rc(PQ;(wYo8)t`N|6)106KHTHJ0n9u7(Bb<;ZJSiY`0l zSgn%Kh3x>}nw^FiF<(YDKK#@;XbPW(;SF=uL#?hYdj0F=JRPm!9r{s=y;4cq9t*nz z++$-}K;<%657pRmA;Zp<`jsO_H(d>^#bg z%hW6;KyfdA!0fV0*8hy4_`o@ChJ!Z2UAa_`@3zdlJe|fkaaV-*A?oUH2X2PC;_EPL zNzJ04_nbh}ojN##V{&{%)uKVn#m^ilor*LBk`c3zG1b0%BaBV12GvX0B;P(7al zYq&5x3%tdxdQ{W(%{HlUZ`GGICDpk%SOzlzw>*^>>hkizHM!hf8%uz#R4T zwm1*D&N=6PqD0!OcA66U=+KtE@jDQ+HNoY=6Dl<8>DDAn;pl~B)l5#=KDZ^x7KdUQ zmZ)W}g_{pbw`LCNQ14BCM*|gM^*;=;&F33iKWak}F;{iZVL$~iLA6_-d-V>y!+(eb zh2k&20M#^0qHYl~Gmn1fdEYws3+i#>LWykN8Z*UY}m1HQMP4&bA5gD>vCYGEt*b_8h*NP*lx1G1phKm4iFevw&aumhb-kY2 zEPSwS$rEwFjkaL_e5X#^sXz4;g!oD)_->WeEp+{Rqt2#;?7@I*Zbpcyo>4eEYO0c| z>SzAKNt~-QTMx@1a3D6SvgUZV`;y=H`kb#*ea7|8RpVC*KAvzN>=%51#{A?e`?&$XIHs~&216gB>S+@u zAu^mTiZHZcGor zWX6WMlK8-5`2seASJD$M;DQ=U}yJ*>MuEZ_sM%#JFlb=WcORFY3d zCX>qmtJrVteE%sCd~ooAk*QXKTL=I1VRSi1ow%2#Q$?;8afN>$2H_hjyDH~aS(%rA zE&v(WF(yujxir@I8F@TI!ZzqSKL}4#F6n;eRZ#Cff3eDs(dDn?&)x zCpXAfm%#7GIT70V-nsZk`8p$<*ppnWTXVF++@vTq4RtjA)-tuANM`7I^H+O1BRFv-I5wsKjrUG&<-)5z4f-kQZU3WV5D`OhZ zfc^#Db9@nky0tGn@-K$5hSY|5g0S=;zA%|ZXveZ zTkGxx_qbm|tQ%))$D!+Xg*&@5bGbX-S4P}4_}N472CFvD#$)w{_w&|-JjxllPU%@s zqXLGxQE2Hl>ktGbeltwf4t#<*h=*&&8{ADJGMU2afE7O0-ad`BUnpn#3zlJt752S~ zV4$2ThqtZEKcHQ_^%|$C z|Jyo!+k5S+qq|<8ZyIJ34PiRWPU{ANnnvq%HFxMd4ELqZtsT1jEqrageOQ%~fWT7 z`gTv+0!GoDc6v?l4@fQ%<7@c5&}q7x%6e^zKc~Vkb}gZ2`6oC%jt3TRp%eaRdN@-| zf|!cM7Ul$Qr1zLg-E)PCVtsb%W^l92r=00!zfTbr&Wck%2239pqHTfgtUI$bg>%^sxJjp!o@Ls+71HKFY$WI*|7=rpqz4h2?VtqcR z@`G^0_0QvUhJgkPy9c&kmc=+ub6pLqu`d;GQ|InlUR9=`XP(hx8|)<8rk|{a(BZlO zEARs9g!NFt$GA0bggRgi&@|r0+i?@bVYwK!)7d-J{rx}ck-`%KPlB2H_)29yT4Uj@qMgd|zBk7T#NCcx|#a4kgue&OhZqI+-g(HK@iGeSnea%Q_M{P@fGd(;MYL zoz>uXs<%V0y&-?In>_2WyiZD^jS$}+(Vgn?uae(k#7xAJ9hp*L7lr)t=W?;pVv^^> zd!;fI{z-T8xT~+kd9I%9ubMpSDtB@`7Ozg7j(3ST@Ql4MnUmC)GOn>ECw#0jSxP%| zG40+!lP4QZ9XnBY8+Oa}y`=7~{k*AigmQEQRe9KTF)JUK|BdQHkot>3V-W(}cOh{AReanIcjx^LOf)BkAXQzxncq z$D@|I9FIDqvQt<2dC1m8q?Pd*_3$L_WY49y<6u>>Zf;5Mfui*Ae2T5fPAZr>`T+0w ze1fM~lbi3^tU|ttF2vX3>0lmt_xb%;c8`DUv%YfRuk}aElOK{FocDYiHW(Rq+|M z{z{Rz0xwf^SD81F4ee5qbBBx`4ytsz*_W{7db{JNOts>_t{Ukf=B;qbHgu;jtxZn9 zHs>evf%Ks8F2tK& z&L?$=+@n6$(J1kzE^f@P^~<94W>v;?J@!UH;~zZDv5JQB3oeVVf)13!(x@|3=l!y- z4K>8URrcV;;?Ldbb(sn3uQ~2bYwILOmDm}tdXrW2D5lk6dCxq#=@R{c+3sj{NK0P8 z=Ix4K!#xw-&v|BAd?0fj0FipjT|2#7aQHH8<0HY;P_~Io{K5PodCtwKC1A zIi+QZ(|w+Ey~OuDijkRvAN&@!;s2H8&(;MQ;5Fa%9R8X0R``+6`OTOijr_T_sGkV` z65f6{2*O0_i{R6}jY4&;8g->k({H(t1*ZG~XK6k6bQi7T^>&`DUGL_fuDy#mJ;l4n zZWjf9q*Pi5W7{pqTPQcGq+;wZjxW;ht-+t8x!%|hW&}f%Id(i|Li)8<$Mtf-4avoP zUCee6=Ns!n?80}xfy3J^5C>g13YvF$knV?OZ+16#W?fodj~>BGIN{7)i)&WZ8ZOM- ztm?Wi6MPbL%*ySjH`p`GBZl`M4LT!v#*m_))2z4mwS4TChR7+t zcaDNuZD~3wL{PZwKe4VF!w8?D1-+FbtA!5lS-InMa}4g(1s;hr#H$=mF&FyR6~nO+ zepyqM!2xL5NVVQq>hsq<=PT0hvOc^=akm<%R&R%l><{Rco%jIcbeVZhDa__Rxc(IW zb`$A|@~Lh{sdRQ>EOiw#ev+U4I^FMitftHytOCe~?o$OgyKBjSkLxd<;RsqH8>vp^ z(m}=dAP!#__qPmU(gpkXZmPKv`UB2+8RH{5bbB!kx9OM;Nll#&(>Tl{%EMaPug)Nm6y%}%TQ(tT~RdJRq zkzc$h_x&gS=~MA08+|!*Yd#Zs;R}Xp>yt&HE*ZT_w$`hhfC&-;SA?Etj)bZG*D}53MUr)`^ zUcIuO3&Oc{zie};SU*Pm*o*`Ej>>CT(cQk5j|Y|OE}sfs3fX+!nu*VuSX+%Y*c27i5$%BUGt_rKJeJ528~oi@4A z-KrLU=*n+`0gR^?YDecbjblSI+3^Z$cK4vm@g2##_&_IADYbDzw#y`^Lz=tl=Cl%fi^H5H z5#~-;xlM-TT!|}jAN)|Rdx&4)VaQ5F7D3PRZr)IneS%@}qxW)_$JaGDZFHpS+Xiy? z(O7l0bz1LCcX@U{OM}y3ZMDtuOk2#sy6$jo89{$DO5e1u``KeNonmwB#`iqGukiMU z@!Dzv8!QlUdDC(VbjI@)Y^!%)qR%y5y?R{#x|LI!J4yIIp7ZNYvC3g)qH8Ll^%%*= z;7KZ>Z{s_>yF0=a;|n*(!MaU++Z)sc>i1@64ck>-VT8om!04` zHPaen;CWZ@QFa#mjL0mCFqJi69ahOWdQq%rKWQv9(fq%l6p~+TjDn*+#u$vfAh-+4 z{F(+F2-peAj za$_o)MO0g*kl(qG)*Cs-zRV5dW!bduZP7*gXc*5yF`?&qmpCwjf8IFGLQJ5EJB0W^t>cT6wLGVXb@rZN&bwenu_tsFSY$U8>nycER9yguA?2Kl3Z~+-N?H z_2s9lWB_-iUFcn}lgnNjkEU7rinnA7%$6$H!@m@*i&aR+P0xA3pY6B}bfYL+;B_zY z1ZvP7PT`7UVt`ZUdy@*QsBZdTYF(W+H%F_gUfRo{6QtTb>b-pf-M*A&C@-K??$&s0 z#rDvWs;cGO?5R@8JL?R&zL&$?1h2ar1Grk432ArMjfyc#cEOfrW}WoRnN>&R=@=JB z_sa6klb2VNS^HDrcUXXM-@`f|6YwPRDbqVc1y;Hf)M#-PK4rt4DXYELE6gr<8h8F? zozyCJM*}B#v1tYs{^vy$uQ$tDa$vrT!%TGuOh#>SB2*FZxyIt_a+vRQIDl4Zv=y3y2R4mQ0)@1QvhJmXoOh!dNLZDWd2{zE0 zs%j07%Fc8Vj&=#Ge zXE89$P?a&xOPe0l-WGd~XI|s#dqr@#4JrUE{Y!OnJIn0W#r5sKsnJKc zbgNz2Xh`5PQ{8?H{@Y2I=QmMLuh4G0kp{GbbLa_`Lm71Bgw?tp!*iUj+{I~K6+@Uv zW_BKIqNV;_Jrk23r)WEr*5f2yFUchT<&|$Uw`vXc>|SmIUEp$`#pXELcUQ}a?xxV$ zz%9lMdf0LS%``0=6-_<4@?W9Sp_B5x?$b7YcRzae*JVBsZ_e@yp5%SP^~?!{nFLJQ zirq<@aH~~#Mh!ba4~o{)-am(v_N3Uq-C67@Xa3;Ks;SR<)_khV+_;AO zw^Hw+h5OS1=2<{B`c0LYbwL})afSRt5~HwxI8=^KL*o2<*Q@d{daB9cAwQMDyOxIh{W>8)Gfsw;5TKZ;>;i7-P7J6`D{t&4rEy&xvEQt}o5) z`dBSdJ-85aT7DtZTgFh;sn-K*c4bsZBitE7W*i0b^I1pW0Zzt_B3gbl6Nj`q?}@zl zX1nn%tg6}3<@zwC?)#+xJwU?0(Ik~Pg)6OZF<0t)oV%e{b=uVzh(f2$)SH6oG(l$b z7tS6%DK)RPBWCF^?3HJ>OxM^a+W_PFgVf7516+E#n@OaNGz0}xYY6=b$yYxQKU@kUO69m8QQd!ajdCDeygtKzqk2tz-5YOjky<;G-Wj?`JnGx!+S z^GkHp&U!K>m?gX6$>U?Z=D__{lMIFk28^#vf4Pz?8ISvuq%))5ng5cNe=R1A3&^op zc|lZJv_F;!&y+(wZnd2+3jQj2S*_ZPCZl)I^YOHwAlL5BQ>vQlEOZX`w>}Src@NIM zq3J4Kx0i(ZN6wy`?4v!Ipt$QAvM}%Id6m-(vg-19yV!b6&46Z4fqpu zD&v*D%l7R;S9-60S1apqhxqnTmQiAkF`m(M21l>pq>8y?U zy_7OJDBy#V>O9e9iaXyZ>$f@A^`|E3U9Qi`Db(G5MKv-mFiEMtQus+XS>>^89?*`n zQCrv32sZQA^u6>+SMi{qS{i1D>Rr^0Z_3VW=m|MJgTuDPed5W&v9B(k$0nX;wE1*lbWdCiZfYw-2HOOGdc1iZ73+eJ} z2Cb!D?k-u#&Z1so$SXJky>yAMcgpK?WjznpB(J%Xe-=@9c$)1@-`T3mcudu}0phnV zAjjBlm7ycuXq?uYKwlD6Ebe@FPTRAv!1CZCE!0jQ>BbMiUuxmwQeXEc2Zqkjk9ROw zxYef}sZjJZvRLvU)$>_-OI51TSs3lNXL^`1_dDm@9=Z;Qxc<@?n1vmk^>fTM9bwnJ zkAK-UJ0R< z$I_=$r(ZH0x>nT+-D}^J!RP9_tL?C{WL1!xDj2uTF+5g7b%yJRRbX0FQ#GGbUyR|F zxX`t}CfDYSLt|4Q&h?AOC*5T?_4LJ8nHAAeH~g3twNT8v&G?rv)H2lcFb z@dIp)5nRCQf16B}8=oq@7q-uQ{O!OCUZVr`t=#xknaFvVpEGpl&y&H?HY7c)=s)1k zJ_ifV)rak*4{D~d{^R`=O|ScDJ@lDGA&zJj!LuLU^05qIyX>U|dVZmEbv~{5lU83t zSGJSF_K7ICH4di4dL~NZ9-03#FJ-b!<$6A^1&fDQpos@={v0u98zw>PGWDiY^8(@8o;NE#jm{qIio0?b?)S=5ca^4&V zz3a~8;b>X{kgnoZ3xF<8eAAH#_^RL<5W#jm94%nkfhy}HJB0`vg#vpRE++NiTw-_duj zzzoRnz3l=KyO^g*_w-ztqAY@QPo*1QqF!8-27jt?0nc-ybkYIs2-D|k0Tn5=AMHcg zLU+h_b%^Z_(InKJlV!nMUGE9`@@M{if=L#`!jwCmm0ltqM+Z|Meg?g(Of!|L9FK60 zJOOK*to|6}H3NrsN8FfO`8<8#CuD0|Xoq)UfBdL>vl*Ad{2}PFDU))Bewn|^F(1}L z_>aC$Hx>Ot{)T_!UEIhj&s7=hRz=N{-@ZV@UK0wPqe|PY=j@KD&O5=#V(P4XNWwWR z*QK7sGxDl8oOR1pYE^ZK%#t!Q^*(3pw>p8JQmC1(u8(HwQPLH5*PoJnx>K_tOhj~d zpRv0pT36Qv9=FvyPPV-dLG-Egm@Mlz8UdB71 zllNGON^6f!b1@I$=5nbo+~a`oOi>}$v_nee!d^WaQY5-OW~E z#+mNR)6T4IJdUf2^|vXm!jM-^tS9()8NIxVAHBYN?)b+M7aI9{j-E|5m~~g{ZpmonDu9Zs4g|FB*hB@uX=JcZoZt zST?n3c%S7WyE6?Qa&<+=#X1mA^VrQ%&n*zT^5+dG|H28 z15T#LvPzGeX?J$1J~C?J{gi5Z?DNVd6%2v~wZ!thKukH1)d1wcXjOXV5d4jk_jC+_ zTdac7MFB(jk@o0_uJUwMcbQ1iEmj5VjXv*{JK`b#P0e{u$LO>jTS<-7**mQ%6Z;Vh z{VRX|BOsk{mEqn?SJzqKT^CY{9pN)ey=CfZ3suQb8Nq1EgFVo$uGIH)io#4_6Ed7Y z6MX;q&i#WSBTg2oTgT!%mT_pSCa1{LU#gPa%{4AVg@t=`!n|Gp*tCN|q1J~T899&nkUC`f)0{}SgeU$>|?9>pCxx66wzgy^iN zrOnm5sN^XHM%=QX-xlL1S{u{xBZ98X{7`HEls?QRcu7;a)I8joW4^mS1!NmKbMt`Q z`%Ohnvz}9<b$>>D@Osx zKQ!_`C>N-CbeJ&V4`e6q97?wGSUx1fuH;vb>>R0~cE$TX`UI~RK6W*A*ThX)^m2TM zP-X2)hB)ERhy1`*U@8yU-8`>wTdq-QIh%B9n_1B{b&)bSRp+dWS1D3@bIrd2SG8+$ zftc}3)JP`%tS;FkwciIa;{us&E+xi=*8T;~;3IZkbF1ZU^-+ECcqi;`pdFXT^|}?N zTt}$G5YfrWm25QgcctD}m4Hl(@?SVHZiMvBfpXU@3a_{j&UhfbK{vPpdRSA2;HS#T z>WEBTtu@~Vwc3d()H&egPOmWI^X}x|K>^@&$=4%w23pUz%239gK?w+I%JE#j<6MHo1+`76;lb~v03Pr!TI+pswGRm)c zlFv{jZ&eHQ;Ve*~N4eBWJZSdaP?N(xi{X_{s@*h0pJMqgg0S_&!n!WGSe>yE{&+&( zeHG8b;XL|YmDzXY9(u7(!B=5=3wOTp`m>R2^EW(Wma2f~)BCqj@ih@uo({YP2vk?E zbPBKI1-Z?3@ph-!_mnecJ)TGt$V~x9-DCd#2|eg_RPmdgcAx7s;U?&F1()4s;ltYUZ)EikjIwQRK;L~ccT)aSXFW(Am#2URRr``&53w!j*} zFyh@-h{g9?x%2X%9TFFsdro7lq&1v(r^=k3SC{1B(XE#^R-|`w8h;Qr+RF);ivyMC z_x_pR*k!xShdd=;;mwqthgU!m@l=>l-~_7RuyU{3dZf&Dthqn?Qg3y~i4VSn^AHlkQ5 zm$@+x-Lb~{45#G2&&#Ffh(%|yb=t{dj$oZ$Y=X#_)RX;n9Phx6Ibi~1U-Q|^lZCS5 zifB^kd(n?fHy@%f)L!0OtuRS0x)0O)J{kE2(S27Y^lv%@{k;GAApYVfm^=?#JAoxN z!8O&7Z`6H!$u7)O-HyhX%az4U5CzMvs|wYidfW592aBbyiqPyg`NE1Q`0cfad_2v) zV!!wI2a#-h+)0(h85)0!_bfhU0SC@J>bDEMN-r4R)y}ErJQa#DT&}l{!^hxk4`qyc zu$@k0p8Dh_j-BIG>oUj;_jH(Bv)~b`zuhQCWsY;%()LNjK{6zbX|qn(DgWB%c@i{n`4_UK_4elGOU zJ@YDkIRwGdXjYG5MPWJMguE;^;OsdACtJ(ca?&`9~+WdcotFbx_&`!D$3C=W7A~@d}Nx-DE?e z*M4W9X)Uq&Nl9SF$(lv<>%7MnIx^Gw&n_(rI*a+HUu?sx-ETJZ2|xE{Xn8yNU_BUN zU+$z89N)UCUCJ@FS3AcGow}^zB_uo6s8cu7CUWU^&Me);x8ieXXS={)XT+~ z2z(~eY1!$kTV&_Q^oCcM>~u3c=YD#3{>M519n%`_&StCw%0thQ&%NleOwE34$k5FY zD26TnJL@EECPLIM%WTEjiFSl(dHj)9^9H#SN414WQKECx+o@%BMVlZ_N1)TA;5gI$ zpFB?8voLaZ$hFk(CK#6CAW-I*-&7KXu6hmZlv&sd?XAWWRLobxR2N&<&HSpK?cXod zeXOhxc#1~G90x8eTLbgPMDq?bQ030Q7r5%QfsS<)+m=${ZOw#9bRU~B&=#}Q%sQvT zKYi#2`@Pg;t^?UqSMQT2Piu;-&s4ULKzMh`wI|!TGEUQf&WU2@du~VK+g!_?!R>=q ze4!e;YH-lD!$PF0MoTmRu2xrecmU_}FmLkfAgK9H$rWPexiDGNUGAWc2(Caox!d-4 zcF(kvekD7Z9Xv_kkZVP5>}6j!U(*S5n-ww}KHnNYGgPNxI?pb7@uGkghrG?**y8!n z%fdse;etM8r_`ZyCQDSA-k+9>=s&|4=O+;#G{0`Mb$K;i$Y6J+l)@~;^X;yGMR2SN zE5|o(*Llhl1HTS4HvP<*Jb8z@zH&dY3-xHVXpD+XKiBTu4qqQEYFuuukD)$TX$|Dd zM#|MuLusga-B4rDF8h1FI=P-){9o$815P+hN_%5BY^1#C3cY^L!Rg|tx6e+BV7mfV zMnAgWdkGFI&$;*Zr_Abrwb@I3m%!3Wl8T^glNHlss?!!{yMxOTQ)aWda0$hG0~O^9 zdQN81`|N$IY6xm2tnDfnLRM#If}j3HrdSljcV*`y zUli12a7mb{XxD&3rU}>B%f*Va;QWXwaszzY*`2-`^v|4CnozNyQptU3|8Uo|r@(@5l3oD~s5MW?j=6ECd5~oye*KMzp%c( z@cA0Qe-Jt;u<|tET?kHpT--3h zMbW8Ca~+48$}r#Ke*N!9iJ#9?g1aPNLU~Vd_M_x~J{GDokLuoq-w*p+_&xmXkk9sD z(O({ZPwV;0B6E4dzwXIi;T!QupO@jzWv@4U?kgqG#2pZ^zsKvsJj!@}{H=EpKHC@k zTTEH+DK~qGWA^^|JHPg4|F%&@UGCS9*^hsR>vW~L?pC!j6)1hs1!j^6h#swK@%j&k)#bj5-de9hD*{kL$Uwz91-?p;Uskbf2d4 zBUa(z{BRzU8O2q+CG{-A2>HnI(JaQZvrQ&ax~!@_b8gFMVfd!l-|lT~w=}dvW{Kx%)6D zl+VAn>?;E1pCNd0O^_6fn}%zoITrDPEFwW=co7-0!ifT}V6%Zm4C6DZcA4>5Wu#8{ z*<#(1J`0AnCxor@d^6~ko2Lau98j!4`;th)Z6z24$0S>ntc_+Q$-)h$DVTeXmSDCB zT9H_i5?O|)XiAWf!0`oq1=|u!85*r5IwQk$F`|SpT`DTQ^A!{(6eIXqnbE{W7RHQx zgTyjyv)7(Vv!Q8G4mRi{)-8I@b4lB@caLs{yymaZzkIGUUo!;7P_o46F$*W9c$(lj zfsiOdreqceLUU_qna)?lvM9LNOjx*KE@{nSv?SapPvs&xA>R@tFG!p$P;AP^UP1SI zFqd$5A$my&2AE0&7fweXghLkCYLL=<**y1pSkR@a_6QP@@KuwdXqm#4g-nP{h)nlT;pP)a zfT0L9wvp!RN+;=@WeB1G79YjYG(wOEC{`vkSOxGRtR9}y5rtG(sRH6DNmt-EA7Mh{ zJQingbrKJe18YX;lt$ngoD{-TajayQjx2|`e-ve20AHjumn#`wDQ#+%yVSOswnHt{ zb@621{M67N7kjPY++8d@a=P&MZi5MG^}hJ;{kh;Dj8ylSA}LOm2))duC9$O<9&@N( zB3!*t2-4wLk~LwNEC+X;)+|XQRK`Xme2)?cSx^LyV=8&~;3_znb6S*S%#obRF*2hW46UXCgF-nq=u}Ee zQ>3(nMy#Q;UVA7adx+EHbh#gn4%l7RdeZ)W`0kHq=(pt1lSsu82DAs!!14^LLsWEC zMo|<2DV46Vn5Mjx)KkHuL%&(sj5qPQSZ?VwnF(S?G@L$4Tr?Dm$cyrZcoB~u7WTnBYX4_3~U!-k< z;mLp>L|LQAg`qFv%PhEjg>Ba)c4tbR0f)_@N{1h$uJgCa0u@ zWC_yLZ=g=0mg+@O`$|L|BdXh$H1! z2g@K%wcn}p1@kd;;*ZryWn`D*XX+%KycwPDDi3wO`f&3wz1-2K{~Oj344;S0tBSNB zD!NXh(4th{$Os5b$94@x_G39MmCln~w@h1NY!7SGy$^?|IcFJfH}9Tlrj#yta}ndf+5hlq{M5~HD zEfyaf;92CT(k+P4Qh;4^t^%-JFlzi%Jg~N~f2v zM|yW6MY=4wQKmTyQ#_e{%=e~;9f4Jw-e#pdUA$O*KG?jtDNg_SnwKJP+t@M!vNb2{ z)^%G%nAPkC!E%nK(|BN!w9E<$kvozlyHeLAv(ONvWbE}0Bt`e1G@m$ERZ>;sk(G3o ziBV#;2)l)px0qeoJxrqAnKdtU!{D2&92j=XH7BgyDH){4PGRM8oCSlfe;8kpEDHsj z18B-)hA$t#qMG*nQ4gUSk7NFsdZz%5u!c80mu$i+<=<_}su!H6Zk z95Uc#$Vgdu17idJhq$+U)h_t-}~SxiNb2amEkq53Owxubk6|jYLzP zD%|;UdULbi9bcKTB)0dc{$uNt!^Ri8;blYr@$3KBOINRc-umjZ;KNsH!~Ww34>XC9 zeM=uJo~DFSj0?k1RV?>AMricctA?!~o~U`ee2~BF>76^wF3*O8x2x4aMGR++14Gn3 zObS}Sn2iANi;_Em>N}P~iHAxtij@Y*%_KJ+n##TvxBDxbxDY>I1nX(WIpVfgv`O1- zx5$&tc-)oGjvF1UjwZ(lb=L3prDd1zlP~s9U3U7r(0pk%{;`*QIeaL@2rJkn11x!6 z29fPh)_6%K%_475#j(HpeXb|6Cz|qL&z$5|kNc7`Rd+X3`OCMDRcOm3q!2Yo zM=sKT4W^gvXa7uA5 zg&Dzas=I|UKH144LmA}XKpR1F09l|iD}pF83@jNHK&y-? zDHH%Ch>A?pmJ!nS*wJKZ?B^RN=5y>TiYy6fc20+dd#dt%1_n9DOg6?DzYvgKhb+)StE|!C}k~A1cAJTD}RxmwUtYk7#8(cf;q8ru*6V&9k4n z|NOtXAJ6Hxgws9~O`Of?0SoAlO z&70t|^L@5jU3#v?=+M3h<}mpT2>`krb!B49HVxIyD4`WLhgOq*D%)|RBR3p%)P;YI zbkS}f3c41tOi z)q^8+#l^n0KS&};mD*wwSd1FxduMV_8+CJM7!@Zo*Q%~p!$t3goh06K`&ZmuC(mAw zriH%xUAFlB*{{%*`}0Q!P9eqCIjVjc=DOLU`v<}7=IkQ7eeFq!skmIXq#H-wkH3q* z74%`oLIgh;3?JM!j(wx#&``1SW|3IUDt*)In+K)ei5@%G@_x77?E1N>kG2HB9^TaK zGP&RqJ8AE^Xlz<6h5N;#(7I(-w&fnxd-jt4szHzMo*#a3@M!q7DSz|*&mI{FmkuNW z%_AAU-!;5cGVOw7heYbrN!j^0FN&>iH(TA1>S#(sGp5(w>E3BvS>@w)k75c>e!h{K zKVDYYocf8awi_zleKklJ_x&8E@ZT`*^XhI`HSqXefP!-0_=PV-Tu+GP& zgC1Yn2Pges&^!XRh%qCm?1s$Js;4(Xif&`6!_Tq8D3I;6m3UaL=gVXHOmB0N$AoQz z>#wToEDR;7qh{>PUQgOh?I@m~9Be!5pG6Dx@E6~;SN+D{n@!D-8-mjimYd$B@F(3y z3E*jb(0RGd*V|2dy)0MqqwP!2P)?{6&uazaryb)Yi7~Itm27tg<$=QJ0l>eDh-eILWkyt*MLd#l_BP|17D(6Bm1C(AabQ zm-g8qKJNYQ^FO^gs)nQH{?+i~=H1;!51l`%i^HSSi{&O|n(KC~w4Ty4T}JC`*w0*rj zd~ZUU zuH1h-{QpiKd4(5ldV3@DvOV{-y_?e>-RhtE)sYZSj%2nq-Fu3=M~i<;koUO%^S6^% zM>or(-pAsm{qnh3Caq%Z&D>x7H1hgS^jD5-b@*xbYW3Tv!R0r*^8fl%hjVUs z(Wo`sE;VgykJX#~D9MM0n15bnqw2OZ7!{41`0V;!bvSb$AHD7mWbO7-#cFlLP1#SM zjHc%+Znysm2Os!J&(W*q?#>vGizc^;{MZca71IWW@{0g05PNh z6lST2@9N{#|J{G?ujiqNznr5U3H-#|kB+>j-{D*^P!82x<@;A5?*b9TP}E<(K{G&( z0#RKb=l(zpToeay2!IX6`|FZOAUR<$@Ies>PqO}m%+y1HKR}fN-%kU0fVUYINU{3c zEad>l3v4f7HV_3^mZNA%sVPB%V-?mGMPL@;gOmo$8b|VU-FK2Q9LXRQo?>0fC4Ced z@OgnJc^1ZsMNXi2o)Z;L)dfP7O;r`)5S&Rv!4h>rr=Uj!zK(%`v&iIaOcSik2|#js zK$j3XrX@k51QZ|*CLGRBh?uoxjuI7C04`diWdp?u%>p4Gu$)3u3>O8K7-_7}vwTwu zIF@4_RttPChguA>0O^aYZn#V#@&N1uAoVGkVM9BJC9mx;O`S@8RpX7wWMt4lREaj_ zDbuHka6wKfJ6i|R3=4`vj~a|A1l%&p>7)%-Fa4cVCyT&F#v*mU`)AEeP^`P^K>98k}G z|7j@s`1gnJ6UQPsV=<@cJ(eafAdO zsrLa+NTBJ2DrheI5Z(fG*037Ldqx~?2qVcX!2!K02_mnABozWVLyerJD4EObezB0i z*8}xZv7`ILL(~v|iR||QPuD+D^>7#pftEt>?}xxbrExM8uH=Zr}@hoYzt?}%;`O_KE zJv^-k!q=Bhag;T(c^3FhS9>V2n&Rw~g<3+UFYbss zgk{cF|4({?)eb{~i6CLU$)4Hz%Mz#Rci{M3onm1AfU<|%8i2f05P!L!j07jAfoEiZ z#IkJchXK!`w#cqq;~ZA(&DZtkKzsn&TkN6PU(y^^=QGGGwulLcaMw7xe+{mKe1HGo zEB7P2m!9DD``icWSN-i;i27>;!Utg@U;y=cg@M#;XDunfT;HcAQmPL|-=7bdPJmPh zE`>!jQy&x(r7FPBjLU<%QR^I_Gsw`{LT0U3F;jz0V51QYuh*l2%)ro~>%bot6YyHX zD;%K!g@;)94i6!PIEA2i87Ogv6j=%85pg0f8U*Fr5bFdf#;VAPAP%rTHaUaSATcw( zpgRzNvPh{QOeiv`=Y-CPjENx}C7q-k*5-AFR&0Xy;1z?>ZKfbyNOJ-i!xY3KayeN` z{Q$e3QpT##Q9~Oc(<3+zK`V}2fyz?~+H>8EMII#6lA~D=?tH>ZB-(a8T?jeG;1o$I z3noN0I66v@oRY#`KL5@LhiD_~yN;Zcw zDps9E8ni^I&5Y2F(aGulL0_45B55u%!jcrTwyRJMk-I@RQWf@MX3ZjgWO{Zp!xWy{ zh&M)E-kp=9hUV_05isiP=82ajC&Mke3A=53JnZL+GqO&pf^HQEc~1pzEIfVC;fAN1 z+r4Ek-S^XD;`2uS^5U!MRA1O@x&}y^kJYYp^h@|t1$#=EezILFZ>315C{$kaCjByI1&kU6l)N$ zl*rJSVMS1@X-NcXAL^8dXbGD%f;%lk9ss$NGho3nybSTxVTAyq7Gy?F5w$uE3Z9}_ zHt(1mR72VAu%ahOIaARDj$k-4$Lp4BOhlVDsLHXRR@%ITVh*}cS|PZq(_|#X^?Aed zy+9QRgB8sfJDDlzCPO8NH9J|=XK*5wNbs7nByJN)QL2KZS0mnF3v9}a;jef)FN{%J zi~5YqVO{T7JkqmV<{Wa#C`CFwWsuz9CrzDITWphQQ)C+J8g-oIITX;aXd#ZiQhR-e zV}PS6GI8HEesz96%T!Ou#iw@j=yZ1158aG!820ngD~}@G+Xuw6UoG?6T>uDzP>>xK zYu@9T;rpzo)OX1ck|^I2#9v?)mVk^NiQ@l3bobzirr*B{NCh-jlGGueP@)9yMZl$l zuBRq~gGnlq*RWhI7WKUIcVD8q=rJ{Pg8-tr_XmTU`d%5V%MlN6LqetNNhgI3`NHpc?< zR@uNlI)bTKN}AiE!~>HSv%Z{^$PMT~85rRJg+M2&q6qaOVVUXBWj)+h(IB>r_!Zlr z9lpVdV#n~xwx+0=MnOe=ctOpK?&K=;+vuaiMKI&Tl8 z$!MzbGq+75w(49KD}LK`ca5ZFUWYzPd&f;{rM(nK7nFT9!Y|LtJ@VJ*F+u(nnAf!m zRwf5v8*r{fqFfbZ)m#~-fqTLyUytv@1gafWQ@{}vh-`$wP@rkTEdqcD^#o9)O_;Gu zmJ%gu2`pyGS6PJvfJ=q0=~_ZiI5K1Us+2l0n+xq(Mv8K7?hD%8iQbbrOJ}H)Y%~7- z!LCFn4`Em$SPj&`Gew0r398V|`(UnHgBlTVx^a(6p!&fJXYEL>(l-2wq76a?HT}ZA#Mqo3z+F56;t<0@J%=Dx00}WFz%330R2hkowj?Neq9X|=0r6mfU%_!fixx{V z2PI6Bb=88ekYEMid|19CR1y`aMD?f{ls9^(gLJ~dP2LbEJHG`DOsiz2TETrhMojB zB0r%To;ddWI4@elfNiCR(oz0vk(^BDon$|RD9qj68;`NUU*cV`{lV(lPrEeuWt4hNQ-gXQsXcdXl#3NboHDdLq^wTMAU3WWi-8Hg}hQmi{WFt0EQ^l}_vNo>)s81ORR)FQ1? z$);fs^J=cIt&0b$GxbhaduzQld0B%~zOJdzBi6k~y#VNX;51JeG^nHuoSVK+#&w1O zwZ9hS;Yt+%)S^|JzPS1j_ZJCmp$rF-MT|ICmFlI0NL_%w7}10X>N1G;5{*Tkty?A$ zG7qn?m=QQx1rV)>t}ZJA%Yt@Sw`Q;gD=64H0{)OrO&RVM>q3$RwNx-vLD5@)VZ}Pc zz}u8&IzyWL1hS5Ra#;HqY#xp!2pV$0Ar}jj$8#j3ib~=JOgCECg6`R+R0;}cCl;Vk zJH!Rimjfq&!ZFZz6A8Kq@=Bt(x?m5K(9bPjFCN++OSQ~`PP>)mlt@FIMgV4uG&>wI zbgQ*ylM?h+7>&<+F5k5?I$CO%kw;1@M15=EoKmJvgt|c6pk8I_X3H7`1fGV*g0Su? zSU6jm-IGmsrW$d|7|ogeOvr}xMY@I3glbF3R2@2IFdjVA^pMNE-aN9)4%4&FZRX_H z#?0LWQT%d!w!oX|P_)9d`0xjHu;osY_@niJIXpbd$FxPapE>S9WBi&l&!0Un-+c8( zmwKde{I~Yo|Je)2@;P)Nkm2qdP>!ssJvmZ`A1S2T-8`zE?d?p_neAH6q+ z(FwYzaV*!~;;Jq&ELEe%+Avn*$l5*zg_o_dCsDUNQ2!yrLbCzgD}++ruhH*uDwK9y zf0McQ9KxX>Az2Jlr)Lh%047MJg02tDN{vHU0a7lkJ4&I&ntWyzIDkk(qyhGYz$GA1 zL<}4~*0q|~!4d`NN#jWj!OQUxf?(vaz(M|oBhw-eotI9l5MnH8nhZ1?kX@vf=<~dx z(xS}+K*b78JAnyP!z%#9M1yKU1=S>#pddecQ~(iCik$B|Vh%jAH^*6&Vif2Q{L01t#KTk^nQXczIt#3Xfd?vnZm%2qYf{i(b$m zxY7prr8}h9qrlMungTPH*i7Rh(JTO%fb!H=c-n{SgcbauIz^?l6p)Yzn(GctrwT1sm*~UX?vZ9!wOPUxuQZ>CS)O()eroRyMV}ow zqD;tr!!>NM8gR4wHNR%F))!mbfo^qZWssJz>}&I}WmD>~_T$f}|2eiTKSFdbx z8~8F|bv)9Tp0Fk9n3Kkgov)NcEuYOglXY1-Sf$Go>(y2GXiW!`+rAY)nkI>$2N(32 zQU=t4XmxKsebhLay2EdN`gL*9KV%}}<#+k>7hBxV$*=uL$}W=M0)CW>qkCo%>~!FB zq@gOoO_QKNNGjX2q|oVbVa13IQ_KTO#Cw<(EG=NwA}TQ;Y=P19!M+kNx&F2c*@w3l zz!3%fkF1Q5NQcUATFyc}H(u19xI)Rcd8?DRkW-gtJkB?A?y z|8xzj88`}6H6aIpQ^vN7_UBL_Nm$kj^qWwfp#NyF9FAi#5>KQ@%fAq4KwY4FdI+qF zHD5yT5)F<`sP?ePYc>sT2)M{VoPu0YyJiXS<$}&mgQ*voJeVd{;=p|dJ}zL}fZgIW ziU*p4WkWE(B3yIBkbEG0aa5{8ieM}_CfFTu!a<8`LJuYL2v|j62}GNNg^OswlL#YI z0G@@eyHyrY9(Zw}yI~ns1_H^~05v19CZ-gJc#qN)Nd}HqWmTjgU|ygkw*heTkfJ6U6RpF5wv3oT&2PoCs>p; z$yQ@Wa%DSN`vH%21sjT$Sp=OaB3XbZmIuQF3(3urLqTU{SLAI!gh~}BJRxSFb8EUg z5_3pdj>hVAqd&m5Eay?vWe!Qj_MXMQBU=oP15g!y2| zt6tO18>61z(xnJny}(tAxg!N2^!w4mx8}O0jI)+|>aAvG*mqA3b!nbl49o|@X*gEq zVQ7y$&pt3N5CW_MX0*6Fe*P#OkE6Sh*w6^NIuo*a%Yl&H&;`NeO7~6@N%$(1F(*A6(MA00U&~dqQ;<gIN;ye{ z$~qiX4bZ-?8{7kS9X)Lan@HUs1W@1s9*DKE3L6F)h(7k|gWoxto76E7%*qUzx z?gT(l4UB-Xg|1UjL{0~at)|p4gMiF}kHN3Nsu`%(s(`A6M8dNGg6g>eeh7+==0dt* zAbkn6TtjUd85-<30kD!e9Vi>X)^f^AES4k$WN`-733d($VE=|D z8Kiy4I)J=H5jcC=a%iY1u)s(`R|U8uwYy%?I9MO70SNo(*mu*F$qk+Yf+NwgnbVKEnz4^pzx%?_X)WIe>PTN_ealMESg+Cg$)%7Sev^8D2$BfxegW3gy$G?M#)xI z@&{^_Xr<0ZnbK|Wfmv%v5bZet>IUn?_90^+LKvvdByVGX}8xJ?Oora`*U zMCIcq?{$E18A!3J-my#V9n)(E`B}5eEYaw$-I-KH5dmN#pWT-+8(NI^PNA&gkKOcws(Iv{5n(o<2G!i}LttQf~6TSWL&0sPS0EyJ6KhYCT<4 zxpw~eYRn}UMdiQj^%|$UW|B?%)d!vC!K+u}&t4dBewdv;UjKT;-@zIlckJf&*UtLl zwET3?x%rP#`|ytasM@2qdpEOHFaBV$Rnudn%HQr4M+N?3eOg>BmY@y2Is4o0AK$#E z#GwHP#>EQ^-ofzTpLibmV4-$+-ZjhrXR_I}{T5%HzFWWSR8lithO zj-OPIBs#&E7J;?c{3d?f$ycN3@YH0L>iH%IH&XY)Dw6dXXHmZi=tlc@k{o&Z{%(-$ z=cA&z_`CD%+vcQgX%A=pY$k3FZWjxr7C3N|SJ6)S=xJy51=UxJqqo`UzkQ^*m)W`T z7Me~D!~>@@hk}4>CV*rV#tRJCAO}kpS|1m$#mJk^dIYp(7`Pzl7h!vrtWuDu8QD;E zCI$;7*rt`j#~~0=K{Mh5V$imFygg=dv{9#$GVS0?%YQQZ_*x&l@_u_9)?t7^83i#P z4EGd$Z!V}AQdW|I4&el_24LXOK_k^T5t1{iv6{rzJBBDlw|EuTJOJ?79+p*`QhY)4 zfli18o^a^e0BPk41d^C)v|2C`?+_tdkfE-HoG{*6Vl`O%pW5F<=Xg4rDqAIDrDf zFj#!WgfnHOKtUN;YN`T!)Xa$_8N4gduIjpo2Q=6gWDRIISEg~zdkTvl=X zuuV~#2mtuR(2+V)b5Y{7DZ~~*lgANR9Hcae6dX_y}D#_{O6u~CH!AqiINqxQkA*B>g_-{0>f zZ@g^$7zB=8qJuLrEfm0^q2OIIE|gsrBsI1^_)vEtP=Y3Eo#EX(B6lKF5JK zK(Cog*shrJpt8G^1vrMFVFlxGq|i3Bp^0@YLHpm?W5(_7c08GpTpmt_1W;{OEf%wg z1Y%3dq5aG}2E4+E9g(KkBsJXz2Z$32T*UN1)+<|${!iM?rfr34bfX^{mV`Ml{ojk{Q;f9woa@1AQr#l4MSnJ zbfPA`-Kd!7{$+l-Ten6JuR5-9eDUWhD9*iyH3&*sOU{*;_;%d2mWt5mFa^8pid zPyN?AJP`b_#le6^%!=A6r79)2=o-DDFqUtW)x%ZWd1VdGVJ6QQ8_&b28?+bT=Rqv; zVAfLC7BZ$M(-8>Z>3Io=&)0jeUES+yjcq=A^SVY!&_W5d=Ne=X4WuXNq2PW%_XRKo zJ`Q^tHJH)}G)^2@3|O&kANp@W=Y&LzA}nDb4;34B^BB5H7PcpL=pI9b&@sh~7?7!* zWh17uz?7E*-`Q%Vb}Ac*AoW6F&9_)ja$+&qRZtxd-WcFr1Yl{|O{@P zr-n7DD-jpmz?_({g%ixzBzLKP6)~`15%%?h{>MnLuL`zjiWZ+@HUvJ|gx#^C#MWK` zwE{bjjwK!D0J>0CGxZD^s;OFv1E~+$fe(9AjSdhY3%^k^I7gtavpqG4!NQ;$PMFJ> zC}@C?NNzb+q}aa0+m#XrL?(CVKqcI$db?EB)uc7xysg>*_Wx9|$9Y-dJ=rU*(K3rR zD*^V~74~VvF!r)pOEvRe)Tj*1;={1$nm6*ETrNk&R?~)a@XLBmHa$E#I0%&HarD{6 z19$4ZX_kAPPM4k>&wB0bZP;7(iI=_U?CtB<`zPuCrgI5Sq02omqt7I)& z4h=9^1QM@WqNFhL7y^%`Qjd?Z=}A&-WC>?=VMDB9$m2E=V?uQVgL34u(-DAiv-BWo z#ahb%CRuDOxWJ^xz5dS^hl$WL%%g5O@s>0BdAq@mZTH9NWPALsxobRpsO`Q9FQ&(# zul=k)KfIMsPCbIqYrYn^705@>u^2RE0+UMFy6Zs__EfT4;a2FI4IR?GnfHgsC!Bm# zrfGlk5QlwLD~sV$NUoPDdtRG(psB!+YnUk__h#3d_SteAm5*f*jrlQg`7|zl8>6Dvtrq`X%4n`Grl9t2t1`9do z`@Y)D6W;6&RqRIdb9_8I5LDOzxX7 zI^8xpyPRD8aPbCinv5MvfUSE#hdZ8F0NGB!#8Q!)Hhh!9uEDcjBJDRcc~pUbVV4t! zv;DcV&0?_dhGPL+1=NosOm2FF7hSHOUvWjBr2WYzN~9zT(#-$3o8_y+=&Q*8?dScA zzqdm0Ww^ZK)is4od7yt8p{I;LQ$*3)m z(gHt;wWJ6+Kpvpu#o)5nT?w&)R2hK@y_J`O=&DB<(e#84mZPmaM5(N}L0&YZK{$QZ z<->`m8?8WVpL-Q}!Nn;&K44}F38OVSPU31>mT`H@p7`B?)1Itd1&fJ|htACR<^4)) z%0}#Ek_lU#nwI;?FT7}PW9er>dcExKd;N8xasIHF`CB>BRtB@Nj)&bt&QF$y8|Q%Z z_kOfD&b2$ryp=2kcsw2u-95{EW=LnrcH5`#jJ(l!H9axihIKI*i3ihRx>_H|Zn1E( z8K~NR+PZF-+Sk)$@7TKN$WO=y7kn{47YFEv(4gH2Xr|6m+phh;e!i(^u}RFSG)3JL{J(4k1$iFzLtbaZ%$8o$Kw`M5FjfclNqe* zIz?h*FFqo;(D+(`TF4hSR+9E$=^3Cq_KFrWsoG)Xp+$ok0;0&IK%?~4!aLZEv=*>rgk zaxyP6K>5N(+e+eMhvIag*sTWQBo1~v+O_G~1x`~^Rlv8hMGy%Ns6K4f16zw|Lh2Lf zsczLyODxMG4=S&3z+RySqo{@=`i4MMAQ4E&NG!4-(Gja+br3gc1#AEXQaYgME#piBq0CFVuG^sG4Lou)QeJ$RmVzt?50ILV)=PI~b0vtNF;ttL+#f@+ZP_<`rYsZEx( z_YzEnU}E8wT1#LQP|fQHG=PvOgNYm1c!IPjps|4I(rbnvzzdIT5kZFuWe$ObO@J-a zCK!|8N2+#uV2}&pu?H50Hk1Pf!NUgQg4qd-SFR^x#PGVnN&tmWqSQ*cg5gOM z{Xl>gKLOV^Y{=Dekn=$d6a?8}1R0n(A8c+uc2$lnLC!I;2vRrfA_DddgQrb|ut@~F zt@vQQ5v69F!lN*387yO%MJty`7_9J$1M%JjZC=bW;0SV+0Z^uzu-(W88J|XGP{q2V z;GDy)D25fNLDOL=APrgyM_PO*Zeod#yfIbzW zl@`vu0=a3X`5@F+M>q@|sQ(&{VgWReVW2GISj9|X=6p6zv708o4d9mT93JWVn1bwI z@roYo>k<>$iI)oLfbCdP?l!Z5Z8+W}5tFJY$<7i((Q1fYXMJFHn0?C6IxIWy`Yqq) zj$6}XmugEbrwJg{TIy{#-}wVYWOJb767RF9vj`djHV)y@8*a7VYRnSFgpDQkLnZgh z*0|Beq1Rl2)AXwqyRU^!ne(Jg5(BtshjCLbEKbhPRqsJ_egfiyGl8dyOj^>xbmfza zyc|Ct+W{5~=O91XOMPRYWJa=J*l2D?YR0sjCxb5euroI%-nx+*U`o45M!~*t@}>h^ zj@Z~-<)``!oE>|cv*m#A9=?9FDgvDTLCrPTFf@EvZNUi9QlqBPdR~k#j8@tO`+`y6 z$MKw>vh96s_tGxhtFGnRUPk*&9U&H z`Q@9$O3F8zo>AD3tmp^(BT<4%kHzDT`8qr+Zd>x|7oPB+%bUOZP=Nt~u)JDtzksculBWecxB{&pR7G(7ai#*K1PEoY9Z<#%c%VoDG*}>79W4wB z%(Xloh~!+g9rgc8s128^a6cHW9J@0K>{lN=Vu~N*$U671e*V1bJ^9&mYwYc>FO|~Wn>K>{uyd7Z zdT=#9I%p)eIi6{^Bmb_|zCGbDqy6>%!Q!-9t-tLLu7}LG*XpCYwlU`CN9kzs&>2ju zlQDmHa`kk53Qu6zyL>CR*WdLw%fs1ae?5M+bU$1DX3+ZM(X-X_N8&T|ZS(x&;+GFQ z<vO!dlfL4e!6^v;)a#9 z7uY+tahneH55)9NGjW6SOnqp2ZGWM7wg}J>j5-J(NZsjnRK*fJbI6Oa@xtXS`*C3O zJlO6yKF5`>00$2Go8>6oIb2oYVHj9GB2`)d?j~-qeoV@+TJAhhL01s|(WUj4&{mL^H0M5FiYOr&cQowuZg*m7e ziisrH{ElWg5Esk6YAmW|TdS1t*qj*=JdQ*=9x8lMut8DKg$)Fq0}pZN-U-c8E$6J_ z&e~<_!E+f7O~-NRI0s^4z}Z73(s0(Em6(X7Uelc)xFb)oh-}l{Urd{4Ao`jOy31gq zwZGQ&YPXcpcJ}ptWJZxAw9fr_@3PIEADEZ_e+lQcG_jw;) zFITcWNKo8o8c}Pz>6)z{4LfMQ*=)!}^>E{i#qQ7~CyCCScBIaBMs%0e7=6^R+{{+Z zRsA8fhO~zEs-SQ*B;q?2LUZ$b! z-f?q~62jjq%E-_gqtmREr*Ht20)~O=;k;HN07}G(7_cy`K_a;5#CYLxJkf|p6gF(K zLOWhD1d^nBWr7-acNy%#kzW*+IB)QmoC4h~9~ zif1+P>nBeJZ%bNqyqI3|Up#s7>2BTp>i*ejuT5S3`>*fzL-F-|uM#jJLsZnZmPnP? z$zx^5LcH^2_be1(Qjl3AES(S`eZeJY)6hhf4EdB9Hw+@g*##rr$g^~`TOg){ULy0p z$>Nw@Oxl>0UP#GG$z?rQS>!Ug##l;c%e=&-oA^4IEU@&jJ|PpC#c{r?$Q{%tI~jj* zwBE1viNphD&o6RzlV6_ht6S4UH$Q(-tE;cxG#f>A#wl zNaoJ?cE|n4?$)qT*n8O+f0yP(HTG3)ICdVxyUZQfG6{k=bY0?~@kV&jArv)`*=VP5@J+n?3 zu^99zPa?yxNbKcBq*`@?%wUj%7ivv!pY^NM`38Rhcx-~kcn)W?pnEZ6Je>(BQc4Np zHXWhpQhlaRvLmeo5f@X5`nhCPQb(%7y^@)E-s>TZP(<@7KW)w(%p4U0 z&26v8|K0u?`};tgd_H(~8??6{zy25t+Fjt*aR<^z_-Qewq=Cgr4#HptJb^-FlbT9f z3AK*<5haF)&4d~T1yHP_mUNY8bHLVQ;6O_dJgTxrVyLmiKt_PRPoYLjD^|dFMO1~7 zI!3gi#MDea=w|`5^DTk59lr4zEr z1ze#TO}CYdS$N7v;A2&mtyQ6JnhUeGtIGwlYG9Nz;I&ANi`kJ1cZfT}^MhtKWieI7 zvy8G;E0{ZLvAiJgvUkFD7>ox0%ag}uBzP>)(4Te5y28P4$RS{a7# zb&Tma?Trud<6gDWL1#}lf}>T(*e=z1cJ|mP{JeWpF1@dop2e4+7HIaHPUG3{TaIyU znd6f1Q6Elb#hbTZhChEi_SfN>GU#k+e(hplJsC7>AGl?&dEtJ%E++nC9sW191BBt9 z_bF3EK#J&hQ%84TdOAY0A{RlZQ81K;y+@LZa$+D`wO69y(q_!`2w0i(JL8FjXNfV> z?JXU#A=@m<`+3Hloe*x8E?r&oPm^h<(a$cbS?&FY8W>)97*tJB7h=iVxc5#Ok+tV%H;4cv^N3>KpbcaHjr5!!yv+S2mvzz zM3Hj2yT<_*Op;VI?8=hWQ*t6-!ZBPfBmydmv}$of)kU$*Z+WMa@M*F&4#XNEEZGd@ z2qTE+iJZ;pSh9|f3B0c5rO&QneM_(gG1gY8!uh=v128B97Kk*{f%1Gr5ae{Hj8&_v zyj06M%BHz33+Kw(--xtLBILX zeD?!&b9CKbAAe_rBQCJ7dZ|?wLU_C%4rQz#zKsKkD3Y1FphmJ$HEHKl5`9T9J!coo zj#?0*7L16-Qead9K!-S|4DtzK@qECoKU1t1CEhFa-Pg0j{PDGrsf?%CvN_`XKaPLd zAqI0yswi%IkZlrrSg9QCEe3VNetgv);cHr}N!_VSPhYOT4(yae6=-dnYP-0QT*#H zUK*@_%yk;lw_h~xscj-2hf0}}^-CALY>{1ShF1ntFTlZjMK+2cSrTekYQ%bYXsH^w zRYrPfl5liqdl?&b0x6}{&I$I=t#3P2Mqbx~vfVnzD7IwpW@q}&HL8xv&mPQ(aS<_6 zZgN&-R9oKL9(wx<*}e*{LM?URyci!$&JKO?wC8w_9^Kv$(Nm=L3^L-+3?Z%V_%e|#*=`1iLA(m0fUy-`<>7|OIvYCnil!U&J; z6cE0YS|$bZtN}~Z{(G{|mhZbiY%>&w#9=9tiJSFQ7$?OH7#ho<+R1g5G+V?+j>mbk zX~nIpQFbz%MD~{@cx8rghtl%>aYf?uRp z2-)AgT+W5I(>7njBfhg*>UN$VpQy5|FlRd#uAwjzKkt{TJHq~`E^Q{|cJHB>l+8tn z4;1sm*gh1%cHWQZanqbHkXiLnsjaecb7l=C!d4DCzvbH}%(hfk>&VHBx!68!)_RqT zp`xy4+nw$-EB#??eDRI{;@Y27MvsHc*FM`QzqF=wg@Ybo>TwR{u!`z;(?U~cc8mSc)`+xc6+d;j5tqljI z)sxiB)FHDCJ90QLlLPE`j7mO;d+0lG0)})w)@`8V)#UY*5%g4?DFy-=JRcDb#VXsZR6_cvsdtaZN}0q{Xd<2<;Cjp%hmY%Rk!k|7YBdZ zd2_O})!%+8zdNx%u6bC%-;-ay|B&D#78eEcpkn&Xlg-!~v|G(%d#h?*4%W~7+{MXk z>*o|5I>g5)wxXDUesG}el9%T|jd+N5tlYrD#KDqiN*(5TGQ!duagMHq8KEYFHd8}K; zXf|(j+P(7e?LPnG$Jb9D=s&JQ<|mS$3U#}N95bC%DKoH_>rgv|+;Y3NRH{`^uMM?E zZF$@mmuD`2m0R}8MjHJ&I4J%vRaBMxo|0QhUvADe^k4S>8t}W!CjMda5Z*>1yA%%j zJ`x9BSI%1LSW$!O5G<@ifl8)!XG_*Yrb|J84k8zvR>3bV#c&WGrZZVIpWs7*qvHVN z&W4hOrfLNUG4P-8B-R6oB=BM3T7V;ifmAx+U;!lz`#>C81W*J>+90VO1p`?`zKpG^ zLZI1v6;K>B3xqp4PFn%!%*J4yfxvWrbQ*xTG7LUVltKI9s9}$j0UQL*n&U@drxYl| z#RN_a4C4bLcru_(paS0%2}=xgO-Vr$xQNYYLEFfy=NG|vJ;bt+$aOFh{(l5J{!95T BYB&G@ literal 0 HcmV?d00001 diff --git a/src/LibRed/LibRed.Core/Resources/Cjk/ChineseTradBopomofoLegacy.bin b/src/LibRed/LibRed.Core/Resources/Cjk/ChineseTradBopomofoLegacy.bin new file mode 100644 index 0000000000000000000000000000000000000000..7cedaa32988f99f823558f978cc0f0c84e56f4c0 GIT binary patch literal 40399 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 %E>v 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

_E1cT8!<)4q-d6ec5(u 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 literal 0 HcmV?d00001 diff --git a/src/LibRed/LibRed.Core/Resources/Cjk/Japanese.bin b/src/LibRed/LibRed.Core/Resources/Cjk/Japanese.bin new file mode 100644 index 0000000000000000000000000000000000000000..7a0399f7de1b90166e529f33be98f347a2b76525 GIT binary patch literal 15824 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 a0K=f9{zcElE}#Ga00000fR_P>^*2Ga{&-jb literal 0 HcmV?d00001 diff --git a/src/LibRed/LibRed.Core/Resources/Cjk/JapaneseLegacy.bin b/src/LibRed/LibRed.Core/Resources/Cjk/JapaneseLegacy.bin new file mode 100644 index 0000000000000000000000000000000000000000..3d8b7188449216c6023d57ea11080fe80ea07312 GIT binary patch literal 15820 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 W{zcElE}#Ga00000fR_P>^*2EPEoi;~ literal 0 HcmV?d00001 diff --git a/src/LibRed/LibRed.Core/Resources/Cjk/JapaneseRadicalStrokeCount.bin b/src/LibRed/LibRed.Core/Resources/Cjk/JapaneseRadicalStrokeCount.bin new file mode 100644 index 0000000000000000000000000000000000000000..cc01d1ea7e64b98379232d8da3878c3123e42ad6 GIT binary patch literal 15904 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 C(VT$* literal 0 HcmV?d00001 diff --git a/src/LibRed/LibRed.Core/Resources/Cjk/Korean.bin b/src/LibRed/LibRed.Core/Resources/Cjk/Korean.bin new file mode 100644 index 0000000000000000000000000000000000000000..25cca70a9f0df3f1933c3bfce4c11e16a5f9f0a0 GIT binary patch literal 21763 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 literal 0 HcmV?d00001 diff --git a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs index 0a7195dc..d5768c79 100644 --- a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs @@ -23,7 +23,8 @@ public static class DatabaseCreator /// /// Format version byte (e.g. 0x02 = ACE 12 / Access 2007). /// true for the ACCDB identifier, false for the MDB (Jet) identifier. - /// ANSI code page (1252 for en-US). + /// ANSI code page of the collation's language (1252 for en-US, 0 for a language + /// with none) — see . /// The database's default collation — its LCID and sort-order version /// (1033 / version 0 is General Legacy en-US). /// Creation timestamp as an OLE-automation date (days since 1899-12-30), passed @@ -249,9 +250,9 @@ private static readonly (string Name, ColumnSpec[] Columns)[] MSysComplexTypeTab /// 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 — 405 configurations - /// (399 at version 0, and the six orders that have a version-1 table), the - /// two General orders and every locale in JetLocaleTailoring, each verified by having ACE build an + /// Any order accepts can be created — 417 configurations + /// (405 at version 0, and the twelve orders that have a version-1 table), the + /// two General orders, every locale in JetLocaleTailoring and the CJK orders, 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 @@ -274,6 +275,10 @@ public static void CreateEmpty(string path, byte version = 0x02, Collation? coll JetFormatBase format = JetFormatBase.FromVersionByte(version); + // Page 0 carries the ANSI code page of the order's language, as ACE writes it — not a fixed 1252. + int codePage = JetCodePages.For(sortOrder) ?? throw new NotSupportedException( + $"Cannot create a database in {sortOrder}: the code page ACE writes for it has not been measured."); + // Jet 4 (0x01, the Access 2000 / 2002-2003 `.mdb`) and the ACCDB versions can both be created; the // identifier follows the version byte in BuildDefinitionPage, so the pair is always consistent and the // file reopens. Jet 3 is rejected by FromVersionByte already — DAO cannot create one either @@ -304,7 +309,7 @@ public static void CreateEmpty(string path, byte version = 0x02, Collation? coll const int seedPages = 10; // page 0, page 1, 4 core TDEFs (2..5), 4 usage maps (6..9) byte[][] seed = [ - BuildDefinitionPage(version, format.IsAccdb, 1252, sortOrder, + BuildDefinitionPage(version, format.IsAccdb, codePage, sortOrder, BitConverter.Int64BitsToDouble(SeedCreationDateBits)), BuildFreeMapPage(format, seedPages), // page 1: global free-pages map objTdef, acesTdef, queriesTdef, relTdef, // pages 2..5: core TDEFs diff --git a/src/LibRed/LibRed.Core/Storage/JetCjkSortOrders.cs b/src/LibRed/LibRed.Core/Storage/JetCjkSortOrders.cs new file mode 100644 index 00000000..eaa051aa --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/JetCjkSortOrders.cs @@ -0,0 +1,120 @@ +using LibRed.Catalog; + +namespace LibRed.Storage; + +///

+/// The Chinese, Japanese and Korean sort orders: each is General of the same version plus a table of +/// per-character weights, measured from ACE and embedded one resource per order. +/// +/// +/// +/// Measured character by character over the whole BMP against Access-authored fixtures, every character these +/// orders weigh differently from General gets a single weight in its place — a replacement primary, and in +/// version 1 a secondary with it. The ideographs are what move: into pronunciation, stroke-count, Bopomofo or +/// radical order, 7,000 to 28,000 characters an order, with a handful of symbols besides (the Japanese orders +/// weigh \ as ¥ and give ― the unweighted FF FF). An iteration mark copies the +/// tailored weight before it. +/// +/// +/// Korean is the one order that is not only a table: it sorts Hangul first, moving the lead byte of every +/// weight General contributes (see ), and its table holds the hanja — +/// weighed by their Hangul reading, the primary a syllable's and the secondary telling hanja apart — with the +/// \ weighed as ₩. +/// +/// +/// Not every order Access offers is here. "Japanese - Unicode" (0x00010411) and "Korean - Unicode" +/// (0x00010412) are created by Access but refused by ACE on open, because Windows no longer supports +/// those alternate sorts — so there is no engine to measure them against, and they stay refused. +/// +/// +internal static class JetCjkSortOrders +{ + /// + /// Suppresses the tables, leaving only the Korean lead-byte move, so the encoder is General of each order's + /// version. Only the generator sets this, for the reason + /// gives: it records where the encoder disagrees with ACE, and must not measure an encoder that already + /// agrees. + /// + internal static bool Suppressed { get; set; } + + /// Each order's collation and the name of its resource, which is the name of the fixture it was + /// measured from. + internal static readonly (Collation Collation, string Name)[] Orders = + [ + (new(CollatingOrder.ChineseSimplified, 1), "ChinesePronunciation"), + (new(CollatingOrder.ChineseSimplified, 0), "ChinesePronunciationLegacy"), + (new(CollatingOrder.ChineseSimplified, 1, SortId: 2), "ChineseStrokeCount"), + (new(CollatingOrder.ChineseSimplified, 0, SortId: 2), "ChineseStrokeCountLegacy"), + (new(CollatingOrder.ChineseTraditional, 1, SortId: 3), "ChineseTradBopomofo"), + (new(CollatingOrder.ChineseTraditional, 0, SortId: 3), "ChineseTradBopomofoLegacy"), + (new(CollatingOrder.ChineseTraditional, 1), "ChineseTradStrokeCount"), + (new(CollatingOrder.ChineseTraditional, 0), "ChineseTradStrokeCountLegacy"), + (new(CollatingOrder.Japanese, 1), "Japanese"), + (new(CollatingOrder.Japanese, 0), "JapaneseLegacy"), + (new(CollatingOrder.Japanese, 1, SortId: 4), "JapaneseRadicalStrokeCount"), + (new(CollatingOrder.Korean, 0), "Korean"), + ]; + + private static readonly Dictionary> Tables = + Orders.ToDictionary(o => o.Collation, o => new Lazy(() => Load(o.Collation, o.Name))); + + private static readonly byte[] KoreanLeads = KoreanLeadBytes(); + + private static readonly Lazy Suppressable = new(() => new LocaleTailoring( + new Dictionary(), leadBytes: KoreanLeads)); + + /// The order's tailoring, or null when the collation is not a CJK order. + public static LocaleTailoring? For(Collation collation) + { + if (!Tables.TryGetValue(collation, out Lazy? table)) return null; + if (!Suppressed) return table.Value; + return collation.Order == CollatingOrder.Korean + ? Suppressable.Value + : new LocaleTailoring(new Dictionary()); + } + + /// + /// Korean's move of General's lead bytes: 81–F2 down 0x37 and 4A–80 + /// up 0x72, everything else in place. Measured over every weight General gives the BMP, with no + /// exception in either band. + /// + private static byte[] KoreanLeadBytes() + { + var leads = new byte[256]; + for (int b = 0; b < 256; b++) + leads[b] = (byte)(b switch + { + >= 0x4A and <= 0x80 => b + 0x72, + >= 0x81 and <= 0xF2 => b - 0x37, + _ => b, + }); + return leads; + } + + private static LocaleTailoring Load(Collation collation, string name) + { + using Stream stream = typeof(JetCjkSortOrders).Assembly + .GetManifestResourceStream($"LibRed.Resources.Cjk.{name}.bin") + ?? throw new InvalidOperationException($"The {name} sort-order resource is missing from the assembly."); + + var reader = new BinaryReader(stream); + int count = reader.ReadInt32(); + byte[] deltas = JetTextCollationV1Overrides.ReadStream(reader); + byte[] primaryLengths = JetTextCollationV1Overrides.ReadStream(reader); + byte[] primaryBytes = JetTextCollationV1Overrides.ReadStream(reader); + byte[] secondaries = JetTextCollationV1Overrides.ReadStream(reader); + + var entries = new Dictionary(count); + int codePoint = 0, cursor = 0, primary = 0; + for (int i = 0; i < count; i++) + { + codePoint += JetTextCollationV1Overrides.ReadVarInt(deltas, ref cursor); + entries[((char)codePoint).ToString()] = + new TailoredWeight(primaryBytes[primary..(primary + primaryLengths[i])], secondaries[i]); + primary += primaryLengths[i]; + } + return new LocaleTailoring(entries, + leadBytes: collation.Order == CollatingOrder.Korean ? KoreanLeads : null, + weighsDecompositions: false); + } +} diff --git a/src/LibRed/LibRed.Core/Storage/JetCodePages.cs b/src/LibRed/LibRed.Core/Storage/JetCodePages.cs new file mode 100644 index 00000000..37d43ba9 --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/JetCodePages.cs @@ -0,0 +1,96 @@ +using System.Collections.Frozen; +using LibRed.Catalog; + +namespace LibRed.Storage; + +/// +/// The ANSI code page ACE writes at page 0 0x3C for a database created in a given collating order. +/// +/// +/// +/// ACE stamps the code page of the order's language: 1250 for Czech, 936 for Chinese Simplified, and +/// 0 for a language Windows gives no ANSI code page (Georgian, Hindi, Armenian …). It depends on the +/// LANGID alone — every sort id of a language gets the same one (Chinese Pronunciation and Stroke Count are +/// both 936, Georgian and Georgian Modern both 0), and so does every version. DAO takes a CP= in its +/// locale string and ignores it: CP=932 on an en-US database still writes 1252. +/// +/// +/// Measured, not derived: DAO created a database in every LCID LibRed can create in, and the value read back +/// is what is listed here (CodePageProbeTest); the four orders DAO refuses — they exist only at +/// version 1 — come from Access-authored files. The runtime's own TextInfo.ANSICodePage happened to +/// agree on every one, but it comes from ICU on Linux, and a table cannot drift. +/// +/// +internal static class JetCodePages +{ + private static readonly FrozenDictionary ByLangId = Build( + [ + (0, + [ + 0x002B, 0x0030, 0x0031, 0x0033, 0x0037, 0x0039, 0x003A, 0x003D, 0x003F, 0x0045, 0x0046, 0x0047, + 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0051, 0x0053, 0x0054, 0x0055, + 0x0057, 0x0058, 0x005A, 0x005B, 0x005C, 0x005E, 0x0060, 0x0061, 0x0063, 0x0065, 0x0072, 0x0073, + 0x0077, 0x0078, 0x0081, 0x042B, 0x0430, 0x0431, 0x0433, 0x0437, 0x0439, 0x043A, 0x043D, 0x043F, + 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, 0x0451, + 0x0453, 0x0454, 0x0455, 0x0457, 0x0459, 0x045A, 0x045B, 0x045C, 0x045D, 0x045E, 0x0460, 0x0461, + 0x0463, 0x0465, 0x0472, 0x0473, 0x0477, 0x0478, 0x0481, 0x0845, 0x0849, 0x0850, 0x0860, 0x0861, + 0x0873, 0x0C50, 0x0C51, 0x105F, 0x785F, 0x7C50, + ]), + (874, [0x001E, 0x041E]), + (932, [0x0411]), + (936, [0x0804]), + (949, [0x0412]), + (950, [0x0404]), + (1250, + [ + 0x0005, 0x000E, 0x0015, 0x0018, 0x001A, 0x001B, 0x001C, 0x0024, 0x0042, 0x0405, 0x040E, 0x0415, + 0x0418, 0x041A, 0x041B, 0x041C, 0x0424, 0x0442, 0x0818, 0x081A, 0x101A, 0x141A, 0x181A, 0x241A, + 0x2C1A, 0x681A, 0x701A, 0x781A, 0x7C1A, + ]), + (1251, + [ + 0x0002, 0x0019, 0x0022, 0x0023, 0x0028, 0x002F, 0x0040, 0x0044, 0x0050, 0x006D, 0x0085, 0x0402, + 0x0419, 0x0422, 0x0423, 0x0428, 0x042F, 0x0440, 0x0444, 0x0450, 0x046D, 0x0485, 0x0819, 0x082C, + 0x0843, 0x0C1A, 0x1C1A, 0x201A, 0x281A, 0x301A, 0x641A, 0x6C1A, 0x742C, 0x7843, + ]), + (1252, + [ + 0x0003, 0x0006, 0x0007, 0x0009, 0x000A, 0x000B, 0x000C, 0x000F, 0x0010, 0x0013, 0x0014, 0x0016, + 0x0017, 0x001D, 0x0021, 0x002D, 0x002E, 0x0032, 0x0034, 0x0035, 0x0036, 0x0038, 0x003B, 0x003C, + 0x003E, 0x0041, 0x0052, 0x0056, 0x005D, 0x005F, 0x0062, 0x0064, 0x0066, 0x0067, 0x0068, 0x0069, + 0x006A, 0x006C, 0x006E, 0x006F, 0x0070, 0x0071, 0x0074, 0x0075, 0x0076, 0x0079, 0x007A, 0x007C, + 0x007E, 0x0082, 0x0083, 0x0084, 0x0086, 0x0087, 0x0088, 0x0091, 0x0403, 0x0406, 0x0407, 0x0409, + 0x040A, 0x040B, 0x040C, 0x040F, 0x0410, 0x0413, 0x0414, 0x0416, 0x0417, 0x041D, 0x0421, 0x042D, + 0x042E, 0x0432, 0x0434, 0x0435, 0x0436, 0x0438, 0x043B, 0x043E, 0x0441, 0x0452, 0x0456, 0x0462, + 0x0464, 0x0466, 0x0468, 0x0469, 0x046A, 0x046B, 0x046C, 0x046E, 0x046F, 0x0470, 0x0474, 0x0475, + 0x0479, 0x047A, 0x047C, 0x047E, 0x0482, 0x0483, 0x0484, 0x0486, 0x0487, 0x0488, 0x0491, 0x0807, + 0x0809, 0x080A, 0x080C, 0x0810, 0x0813, 0x0814, 0x0816, 0x081D, 0x082E, 0x0832, 0x083B, 0x083C, + 0x083E, 0x085D, 0x0867, 0x0C07, 0x0C09, 0x0C0A, 0x0C0C, 0x0C3B, 0x1007, 0x1009, 0x100A, 0x100C, + 0x103B, 0x1407, 0x1409, 0x140A, 0x140C, 0x143B, 0x1809, 0x180A, 0x180C, 0x183B, 0x1C09, 0x1C0A, + 0x1C0C, 0x1C3B, 0x2009, 0x200A, 0x200C, 0x203B, 0x2409, 0x240A, 0x240C, 0x243B, 0x2809, 0x280A, + 0x280C, 0x2C09, 0x2C0A, 0x2C0C, 0x3009, 0x300A, 0x300C, 0x3409, 0x340A, 0x340C, 0x3809, 0x380A, + 0x380C, 0x3C09, 0x3C0A, 0x3C0C, 0x4009, 0x400A, 0x4409, 0x440A, 0x4809, 0x480A, 0x4C0A, 0x500A, + 0x540A, 0x580A, 0x5C0A, 0x703B, 0x743B, 0x7814, 0x783B, 0x7C14, 0x7C2E, 0x7C3B, 0x7C5D, 0x7C67, + ]), + (1253, [0x0008, 0x0408]), + (1254, [0x001F, 0x002C, 0x0043, 0x041F, 0x042C, 0x0443, 0x782C, 0x7C43]), + (1255, [0x000D, 0x040D]), + (1256, + [ + 0x0001, 0x0020, 0x0029, 0x0059, 0x0080, 0x0401, 0x0420, 0x0429, 0x045F, 0x0480, 0x048C, 0x0492, + 0x0801, 0x0820, 0x0846, 0x0859, 0x0C01, 0x1001, 0x1401, 0x1801, 0x1C01, 0x2001, 0x2401, 0x2801, + 0x2C01, 0x3001, 0x3401, 0x3801, 0x3C01, 0x4001, 0x7C46, 0x7C59, + ]), + (1257, [0x0025, 0x0026, 0x0027, 0x0425, 0x0426, 0x0427]), + (1258, [0x002A, 0x042A]), + ]); + + /// The code page ACE writes for a database in , or null when it has + /// not been measured for that order's language. + public static int? For(Collation collation) => + ByLangId.TryGetValue(collation.Lcid & 0xFFFF, out int codePage) ? codePage : null; + + private static FrozenDictionary Build((int CodePage, int[] LangIds)[] groups) => + groups.SelectMany(g => g.LangIds.Select(langId => (langId, g.CodePage))) + .ToFrozenDictionary(e => e.langId, e => e.CodePage); +} diff --git a/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs b/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs index f961dfa0..386909a2 100644 --- a/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs +++ b/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs @@ -16,46 +16,89 @@ 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. + /// Closes the kana section, after the FF that introduces the mark codes. /// 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]; + /// The mark code of a kana that is a letter in its own right. + public const byte Letter = 0b01; + + /// The mark code of a prolonged sound mark ー lengthening the kana before it. + public const byte Prolonged = 0b11; + + /// The mark code of an iteration mark (ゝ, ヽ, 々 …) repeating the kana before + /// it — かゝ closes FF 98, where かー closes FF 9C and かあ a bare FF. + public const byte Repeat = 0b10; + + /// + /// Whether is an iteration mark — [MS-UCODEREF]'s PW_REPEAT — and the + /// secondary it adds to the weight it repeats. + /// + /// + /// An iteration mark weighs as a copy of the weight the character before it contributed, with its OWN + /// secondary: 人々 is 9FD4 9FD4 with secondaries 02 05, and がゝ is が twice + /// with secondaries 03 02 — the mark does not inherit the voicing, ゞ adds its own. The long + /// vowel mark is one too wherever it has no kana to lengthen: 人ー and aー double what came + /// before. Measured against ACE under both versions, which agree except that 〻 and ꀕ are + /// repeat marks only in version 1 — version 0 ignores both. + /// + /// The character. + /// Whether the order is version 1. + /// The secondary the mark adds. + public static bool TryGetIterationMark(char character, bool version1, out byte secondary) + { + secondary = character switch + { + (char)0x3005 => 0x05, // 々 + (char)0x309D or (char)0x30FD or (char)0x3031 or (char)0x30FC or (char)0xFF70 => 0x02, // ゝ ヽ 〱 ー ー + (char)0x309E or (char)0x30FE or (char)0x3032 => 0x03, // ゞ ヾ 〲 — voiced + (char)0x303B when version1 => 0x05, // 〻 + (char)0xA015 when version1 => 0x07, // ꀕ + _ => 0, + }; + return secondary != 0; + } + /// - /// Appends 01 01, the packed small/normal flags, the prolonged-mark flags and the closing constant. + /// Appends 01 01, the packed small/normal flags, the mark codes 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) + /// The key being built. + /// Per kana, whether it is a small form. + /// Per kana, , or . + public static void Append(List output, List small, List marks) { output.Add(0x01); output.Add(0x01); - AddFlags(output, small, marked: 0b10, unmarked: 0b11); + AddCodes(output, small.Count, i => small[i] ? (byte)0b10 : (byte)0b11, unmarked: 0b11); output.Add(0xFF); - AddFlags(output, prolonged, marked: 0b11, unmarked: 0b01); + AddCodes(output, marks.Count, i => marks[i], unmarked: Letter); 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. + /// Packs one two-bit code per kana, three to a byte, most significant first, under a 10 marker + /// in the top two bits, 00 padding the last byte. The small flags are 11 normal and 10 + /// small, 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. The mark + /// codes pack the same way. /// - /// Nothing is emitted at all when no flag is set, which is why a lone normal kana closes straight into - /// the tail. + /// Codes are written only up to the last one that is not , so nothing at all is + /// emitted when every kana is unmarked, which is why a lone normal kana closes straight into the tail. /// /// - private static void AddFlags(List output, List flags, int marked, int unmarked) + private static void AddCodes(List output, int count, Func code, byte unmarked) { - int last = flags.LastIndexOf(true); + int last = count - 1; + while (last >= 0 && code(last) == unmarked) last--; 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); + packed |= (index > last ? 0b00 : code(index)) << (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 index ce74dc9c..b3426416 100644 --- a/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs +++ b/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs @@ -30,14 +30,47 @@ internal sealed class LocaleTailoring public LocaleTailoring( IReadOnlyDictionary entries, bool doublesDigraphs = false, - bool reverseDiacritics = false) + bool reverseDiacritics = false, + byte[]? leadBytes = null, + bool weighsDecompositions = true) { Entries = entries; DoublesDigraphs = doublesDigraphs; ReverseDiacritics = reverseDiacritics; + LeadBytes = leadBytes; + WeighsDecompositions = weighsDecompositions; MaxLength = entries.Count == 0 ? 0 : entries.Keys.Max(k => k.Length); } + /// + /// Whether the characters a General decomposition produces take this tailoring's weights. + /// + /// + /// A Latin locale's letters reach into decompositions: Croatian's DŽ expands to D + Ž, + /// and it is Croatian's Ž that ACE stores. A CJK table does not. Version 1's base table + /// expands the CJK radicals and compatibility ideographs to the unified ideograph they stand for — + /// ⼀ U+2F00 to 一, U+FA30 to U+4FAE — and ACE weighs them as General does, + /// never with the order's weight for that ideograph: 315 to 421 characters in each version-1 CJK order, + /// measured against ACE. The tables weigh the characters they list and nothing reached through a + /// decomposition. + /// + public bool WeighsDecompositions { get; } + + /// + /// A replacement for the lead byte of every weight the General table contributes, indexed by that byte — + /// or null for an order that keeps General's lead bytes, which is every order but one. + /// + /// + /// Korean reorders whole scripts rather than letters: Hangul sorts first, so General's lead bytes + /// 81–F2 move down 0x37 and 4A–80 (Latin, Greek, Cyrillic, the other + /// scripts, kana, Bopomofo) move up 0x72 to make room — A is BC, 가 is + /// 4A 03. It applies to each WEIGHT, not to each byte: the second byte of a two-byte weight keeps its + /// value (ᄀ 81 02 becomes 4A 02), and an expansion has every weight moved (IJ + /// 59 5B becomes CB CD). A tailored entry already states its final bytes and is not moved, + /// nor is a copy an iteration mark makes of a weight already moved. + /// + public byte[]? LeadBytes { get; } + public IReadOnlyDictionary Entries { get; } public bool DoublesDigraphs { get; } @@ -145,6 +178,7 @@ internal static class JetLocaleTailoring /// as null: it records that the order was measured to be indistinguishable from General. public static LocaleTailoring? For(Collation collation) => Tailorings.GetValueOrDefault(collation) + ?? JetCjkSortOrders.For(collation) ?? (collation is { Version: 0, SortId: 0 } && GeneralV0.Contains(collation.Order) ? None : null); /// + public static TheoryData LocaleFixtures() + { + var data = new TheoryData(); + foreach (string path in Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, "Data"), "*.accdb").Order()) + { + try + { + using var db = JetDatabase.Open(path); + if (db.DefinitionPage.DatabaseKey == 0 && db.Collation.IsIndexKeyEncodable) + data.Add(Path.GetFileName(path)); + } + catch (InvalidOperationException) { } // password-encrypted + } + return data; + } + + // The same, in every order: page 0 carries the code page of the order's language (1250 Czech, 936 Chinese, + // 0 for Georgian), so creating in an order has to stamp the one Access stamps — from LibRed's own table, + // not from the file being compared against. + [Theory] + [MemberData(nameof(LocaleFixtures))] + public void Synthesized_page0_header_matches_an_access_file_in_its_order(string fixture) + { + string path = TestDatabases.Data(fixture); + byte[] real = File.ReadAllBytes(path); + using var db = JetDatabase.Open(path); + var dp = db.DefinitionPage; + + byte[] synth = DatabaseCreator.BuildDefinitionPage( + dp.JetVersion, isAccdb: true, JetCodePages.For(dp.Collation)!.Value, dp.Collation, + (dp.DatabaseCreationDate - new DateTime(1899, 12, 30)).TotalDays); + + Assert.Equal(Convert.ToHexString(real[0x00..0xA0]), Convert.ToHexString(synth[0x00..0xA0])); + } + + // Creation refuses an order without a measured code page, so every order LibRed can encode must have one. + [Fact] + public void Every_creatable_order_has_a_measured_code_page() + { + var missing = new List(); + foreach (CollatingOrder order in Enum.GetValues()) + foreach (byte version in (byte[])[0, 1]) + foreach (byte sortId in (byte[])[0, 1, 2, 3, 4]) + { + var collation = new Collation(order, version, sortId); + if (collation.IsIndexKeyEncodable && JetCodePages.For(collation) is null) + missing.Add($"0x{collation.Lcid:X5} v{version}"); + } + Assert.Empty(missing); + } + // Every column of the system tables a new database carries is described byte-for-byte as Access describes it: // the catalog flags 0x10/0x20 on exactly the catalog tables' columns (and not on the complex templates, which // once carried them), the attachment template's extended flag 0x10, and the zero 0x09 of an engine table. diff --git a/test/LibRed.Engine.AccessTests/LikeCollationProbeTests.cs b/test/LibRed.Engine.AccessTests/LikeCollationProbeTests.cs index 9347ce27..8b180c1c 100644 --- a/test/LibRed.Engine.AccessTests/LikeCollationProbeTests.cs +++ b/test/LibRed.Engine.AccessTests/LikeCollationProbeTests.cs @@ -58,17 +58,20 @@ public void Report() } /// Every candidate pair asked of ACE in every collation LibRed can create, one query per database. - /// Opt-in via LIBRED_SCREEN_LIKE=1: it creates 405 databases and takes the better part of an hour. + /// Opt-in via LIBRED_SCREEN_LIKE=1: it creates 417 databases and takes the better part of an hour. [Fact] public void Screen_every_collation() { Assert.SkipUnless(Environment.GetEnvironmentVariable("LIBRED_SCREEN_LIKE") == "1", - "set LIBRED_SCREEN_LIKE=1 — this asks ACE 26,000 pairs in each of 405 collations"); + "set LIBRED_SCREEN_LIKE=1 — this asks ACE 26,000 pairs in each of 417 collations"); var pairs = LikeCandidates.Pairs; List collations = [.. Enum.GetValues().Distinct() .Where(o => o != CollatingOrder.Undefined) - .SelectMany(o => (Collation[])[new(o, 0), new(o, Collation.GeneralVersion), new(o, 0, 1)]) + .SelectMany(o => (Collation[])[new(o, 0), new(o, Collation.GeneralVersion), new(o, 0, 1), + // the CJK orders reach sort id 4, at both versions + new(o, 0, 2), new(o, Collation.GeneralVersion, 2), new(o, 0, 3), new(o, Collation.GeneralVersion, 3), + new(o, Collation.GeneralVersion, 4)]) .Where(c => c.IsIndexKeyEncodable) .Distinct()]; From 329434f95fe6e6d69f8909f09de8e81ef1afe42e Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 00:55:25 +0800 Subject: [PATCH 50/88] Build text keys without per-character garbage The CJK orders' iteration-mark and lead-byte handling made text keys about 10% slower to build, on top of costs that were already the larger part: version 0 allocated an array per character to remember the weight a shadda or iteration mark might copy, version 1 built its five working lists afresh for every key, and tailoring lookups made a string or two per question. The copied weight is now CopiedPrimary, shared by both encoders and holding a one- or two-byte weight inline; version 1 reuses per-thread lists as version 0 does; and LocaleTailoring looks its entries up by span. Per key, version 0 390 to 353 ns and 457 to 112 bytes, version 1 745 to 644 ns and 536 to 56 bytes. agg.group_text_key 24.2 to 22.7 ms and predicate.or_two_indexes 7.3 to 7.0 ms, both now below where they were before the CJK orders. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Storage/CopiedPrimary.cs | 56 +++++++++++++++++ .../LibRed.Core/Storage/JetLocaleTailoring.cs | 29 +++++++-- .../LibRed.Core/Storage/JetTextCollation.cs | 18 +++--- .../LibRed.Core/Storage/JetTextCollationV1.cs | 62 ++++++++++--------- 4 files changed, 121 insertions(+), 44 deletions(-) create mode 100644 src/LibRed/LibRed.Core/Storage/CopiedPrimary.cs diff --git a/src/LibRed/LibRed.Core/Storage/CopiedPrimary.cs b/src/LibRed/LibRed.Core/Storage/CopiedPrimary.cs new file mode 100644 index 00000000..fc986313 --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/CopiedPrimary.cs @@ -0,0 +1,56 @@ +namespace LibRed.Storage; + +/// +/// The primary weight a character contributed, kept in case the next character copies it — a shadda doubling +/// it, or an iteration mark repeating it. Both encoders record one for every character, and almost every weight is +/// one or two bytes, so those are held inline and recording allocates nothing; a longer weight keeps an array. +/// +internal readonly struct CopiedPrimary +{ + private readonly byte[]? _bytes; + private readonly byte _first, _second; + + /// A weight's bytes, which the caller does not change afterwards. + public CopiedPrimary(byte[] bytes) + { + _bytes = bytes; + Length = bytes.Length; + } + + public CopiedPrimary(byte only) + { + _first = only; + Length = 1; + } + + public CopiedPrimary(byte first, byte second) + { + (_first, _second) = (first, second); + Length = 2; + } + + /// A weight as it stands, copied only when it is longer than two bytes. + public static CopiedPrimary Of(ReadOnlySpan weight) => weight.Length switch + { + 1 => new CopiedPrimary(weight[0]), + 2 => new CopiedPrimary(weight[0], weight[1]), + _ => new CopiedPrimary(weight.ToArray()), + }; + + /// How many bytes the weight has; zero when nothing was contributed. + public int Length { get; } + + public void AppendTo(List primaries) + { + if (_bytes is not null) + { + primaries.AddRange(_bytes); + return; + } + if (Length > 0) primaries.Add(_first); + if (Length > 1) primaries.Add(_second); + } + + /// The bytes as an array — for the copying itself, which is rare, rather than the recording. + public byte[] ToArray() => _bytes ?? (Length switch { 0 => [], 1 => [_first], _ => [_first, _second] }); +} diff --git a/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs b/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs index b3426416..a6c621f8 100644 --- a/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs +++ b/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs @@ -40,8 +40,20 @@ public LocaleTailoring( LeadBytes = leadBytes; WeighsDecompositions = weighsDecompositions; MaxLength = entries.Count == 0 ? 0 : entries.Keys.Max(k => k.Length); + + // Looked up by span, never by a string made for the purpose: every character of every key asks, and a + // string or two per question was most of what encoding a key allocated. Every table is an ordinal + // Dictionary, which takes the lookup as it is; anything else is copied into one. + Dictionary table = + entries is Dictionary dictionary + && dictionary.TryGetAlternateLookup>(out _) + ? dictionary + : new Dictionary(entries, StringComparer.Ordinal); + _bySpan = table.GetAlternateLookup>(); } + private readonly Dictionary.AlternateLookup> _bySpan; + /// /// Whether the characters a General decomposition produces take this tailoring's weights. /// @@ -136,16 +148,13 @@ public bool TryMatch( /// text never contained. /// public bool TryMatchSingle(char character, out TailoredWeight weight) => - Entries.TryGetValue(character.ToString(), out weight) || - Entries.TryGetValue(character.ToString().ToUpperInvariant(), out weight); + TryGet(new ReadOnlySpan(in character), 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)) + if (TryGet(text.Slice(start, length), out weight)) { consumed = length; return true; @@ -155,6 +164,16 @@ private bool TryLongest(ReadOnlySpan text, int start, out TailoredWeight w consumed = 0; return false; } + + /// The entry for as written, else for its invariant uppercase — the order + /// the class remarks give, which lets a locale disagree with invariant casing. + private bool TryGet(ReadOnlySpan key, out TailoredWeight weight) + { + if (_bySpan.TryGetValue(key, out weight)) return true; + Span upper = stackalloc char[key.Length]; + key.ToUpperInvariant(upper); + return _bySpan.TryGetValue(upper, out weight); + } } /// diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs index 6238c78c..1775bf84 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs @@ -348,7 +348,7 @@ public static bool TryEncode( // trailing record carries, since that is this character's whole contribution. Empty at the start of // the string and after a weight that folded into its predecessor — in both cases there is nothing to // double. - byte[] lastWeight = []; + CopiedPrimary lastWeight = default; bool lastWeightIsKana = false; // What an iteration mark copies: the one weight — or the one inline record — the character before it @@ -358,7 +358,7 @@ public static bool TryEncode( // letter before it, or an astral character, which version 0 ignores: e + U+0301 + 々 and a𐀀々 both copy // the letter. Recomputed from the counts at the start of each character, except after a mark or a // combining voicing, which leave it as it was — かゝゝ repeats か twice, and かーゝ repeats か, not the vowel. - byte[]? repeatable = null; + CopiedPrimary? repeatable = null; bool repeatableIsKana = false; bool keepRepeatable = true; int weightsAtCharacter = 0, recordsAtCharacter = 0; @@ -409,7 +409,7 @@ public static bool TryEncode( tailoring?.TryMatchSingle(c, out _) != true) { inline.Add((secondaries.Count, code)); - lastWeight = [InlineMid, code]; + lastWeight = new CopiedPrimary(InlineMid, code); lastWeightIsKana = false; continue; } @@ -475,7 +475,7 @@ public static bool TryEncode( primaries.AddRange((byte[])[0xFF, 0xFF]); continue; } - AddWeight(repeatable, markSecondary, final: true); + AddWeight(repeatable.Value.ToArray(), markSecondary, final: true); if (repeatableIsKana) { kana.Add(kanaSmall); @@ -503,7 +503,7 @@ public static bool TryEncode( if (c == Shadda) { if (lastWeight.Length > 0) - AddWeight(lastWeight, DefaultSecondary, final: true); + AddWeight(lastWeight.ToArray(), DefaultSecondary, final: true); else primaries.AddRange((byte[])[0xFF, 0xFF]); continue; @@ -601,9 +601,7 @@ 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))) + if (tailoring is not null && tailoring.TryMatchSingle(character, out TailoredWeight tailoredOne)) AddWeight(tailoredOne.Primaries, tailoredOne.Secondary, final: true); else if (ExtraLetters.TryGetValue(character, out TailoredWeight own) || ExtraLetters.TryGetValue(upper, out own)) @@ -651,7 +649,7 @@ void Add(byte primary, byte secondary = DefaultSecondary) if (tailoring?.LeadBytes is { } leads) primary = leads[primary]; primaries.Add(primary); secondaries.Add(secondary); - lastWeight = [primary]; + lastWeight = new CopiedPrimary(primary); lastWeightIsKana = false; } @@ -688,7 +686,7 @@ void AddWeight(ReadOnlySpan weight, byte secondary, bool final = false) foreach (byte b in weight) primaries.Add(b); secondaries.Add(secondary); - lastWeight = weight.ToArray(); + lastWeight = CopiedPrimary.Of(weight); lastWeightIsKana = false; } } diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs index ba8aced8..4e0ebe9f 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs @@ -91,20 +91,26 @@ public static bool TryEncode( WeightTable table = Table.Value; ReadOnlySpan text = value.AsSpan().TrimEnd(' '); - var primaries = new List(); - var secondaries = new List(); + // The working lists are this thread's, cleared rather than allocated, as version 0's are: a comparison + // encodes both sides every time, and five new lists per string were most of what a key allocated. Safe + // because nothing here re-enters TryEncode. + EncodeScratch scratch = t_scratch ??= new EncodeScratch(); + scratch.Clear(); + + List primaries = scratch.Primaries; + List secondaries = scratch.Secondaries; // 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). // A Han character is the exception: its FD FF marker counts as a weight of its own, so 人- puts the // hyphen at 0x0F and 人人- at 0x17, though each Han character takes one secondary slot. A one-byte // primary still counts once (a Lao vowel then a hyphen is 0x0B), so this is not a byte count halved. - var inline = new List<(int Position, byte ScriptMember, byte AlphabeticWeight)>(); + List<(int Position, byte ScriptMember, byte AlphabeticWeight)> inline = scratch.Inline; int hanMarkers = 0; // The kana small/normal flags and mark codes, and the running state the prolonged mark needs. - var kana = new List(); - var marks = new List(); + List kana = scratch.Kana; + List marks = scratch.KanaMarks; int kanaWeight = -1; byte kanaVowel = 0; bool kanaSmall = false; @@ -475,6 +481,28 @@ bool Append(char character) return true; } + [ThreadStatic] private static EncodeScratch? t_scratch; + + /// The lists builds a key in, + /// kept per thread and reused. + private sealed class EncodeScratch + { + public List Primaries { get; } = []; + public List Secondaries { get; } = []; + public List<(int Position, byte ScriptMember, byte AlphabeticWeight)> Inline { get; } = []; + public List Kana { get; } = []; + public List KanaMarks { get; } = []; + + public void Clear() + { + Primaries.Clear(); + Secondaries.Clear(); + Inline.Clear(); + Kana.Clear(); + KanaMarks.Clear(); + } + } + private static WeightTable Load() { using Stream stream = typeof(JetTextCollationV1).Assembly @@ -547,30 +575,6 @@ private static int ReadVarInt(byte[] data, ref int offset) } } - /// The primary an iteration mark would copy, recorded for every character in case one follows. Almost - /// always a two-byte pair, held inline so the recording allocates nothing; a tailored or measured weight keeps - /// the array it already has. - private readonly struct CopiedPrimary - { - private readonly byte[]? bytes; - private readonly byte first, second; - - public CopiedPrimary(byte[] bytes) => this.bytes = bytes; - - public CopiedPrimary(byte first, byte second) => (this.first, this.second) = (first, second); - - public void AppendTo(List primaries) - { - if (bytes is not null) - { - primaries.AddRange(bytes); - return; - } - primaries.Add(first); - primaries.Add(second); - } - } - /// Weights in parallel arrays, reached through a slot per BMP code point — the entry's index plus /// one, zero for none. Arrays rather than a dictionary keep the table near 400 KB resident instead of several /// MB; the slots, 128 KB of it, replaced a binary search that every character of every comparison paid. From bbd2652485f5182729a417db9fc9db9a5c27cca0 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 02:12:29 +0800 Subject: [PATCH 51/88] Weigh version-1 text without binary searches, and group without re-keying Two binary searches per character - the version-1 override table and its ignorable ranges - were a seventh of building a key, for an answer that is almost always no. Each set is now a bit per code point, 8 KB apiece; an override's weights are still searched for, but only when it has some. Version 0 handed its local Add to two helpers as a delegate, which made the encoder's captured state a heap object for every key; the helpers now answer instead. A GROUP BY built a scope per group, so every group resolved its column references afresh, and it keyed its grouping text twice: once to find the group, again to order the output. One scope is now rebound to each group with its aggregates, and the key a group was found by is the one that orders it. Per key: version 1 644 to about 325 ns, version 0 112 to 48 bytes. agg.group_text_key 22.7 to 13.7 ms and 13.5 to 6.8 MB; agg.group_high_card 8.4 to 5.8 ms; agg.having 4.3 to 2.8 ms. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Storage/JetTextCollation.cs | 41 ++++----- .../Storage/JetTextCollationV1Overrides.cs | 33 ++++---- .../LibRed.Engine/Execution/EvalScope.cs | 14 +++- .../Execution/ExpressionEvaluator.cs | 7 ++ .../LibRed.Engine/Execution/QueryExecutor.cs | 84 +++++++++++-------- 5 files changed, 105 insertions(+), 74 deletions(-) diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs index 1775bf84..36f8627a 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs @@ -619,10 +619,14 @@ bool WeighCharacter(char character) // 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)) + // Each expanded letter (ß=SS, Þ=TH, Æ=AE) is its own weight; an atomic accent (Ø, Ð) is its base + // letter with a secondary. + else if (Expansions.TryGetValue(upper, out string? expansion)) { - // handled by the expansion / atomic-accent tables + foreach (char letter in expansion) Add(Letters[letter - 'A']); } + else if (AtomicAccents.TryGetValue(upper, out (char Base, byte Secondary) atomic)) + Add(Letters[atomic.Base - 'A'], atomic.Secondary); // 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 @@ -639,7 +643,9 @@ bool WeighCharacter(char character) { if (block is { } weight) AddWeight(weight.Primaries, weight.Secondary); } - else if (!TryAddAccented(upper, Add)) + else if (TryDecomposeAccented(upper, out char baseLetter, out byte accent)) + Add(Letters[baseLetter - 'A'], accent); + else return false; return true; } @@ -691,29 +697,14 @@ void AddWeight(ReadOnlySpan weight, byte secondary, bool final = false) } } - /// 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)) - { - foreach (char letter in expansion) add(Letters[letter - 'A'], DefaultSecondary); - return true; - } - if (AtomicAccents.TryGetValue(u, out (char Base, byte Secondary) atomic)) - { - 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) + /// It answers rather than adding, as the explicit tables above it do inline: handing the encoder's + /// own Add in as a delegate made its captured state a heap object, allocated for every key encoded. + private static bool TryDecomposeAccented(char u, out char letter, out byte weight) { + letter = default; + weight = 0; // 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. @@ -722,9 +713,9 @@ private static bool TryAddAccented(char u, Action add) // Canonical decomposition: a base A–Z letter followed by one combining diacritic we know. string nfd = u.ToString().Normalize(System.Text.NormalizationForm.FormD); - if (nfd.Length == 2 && nfd[0] is >= 'A' and <= 'Z' && DiacriticWeights.TryGetValue(nfd[1], out byte weight)) + if (nfd.Length == 2 && nfd[0] is >= 'A' and <= 'Z' && DiacriticWeights.TryGetValue(nfd[1], out weight)) { - add(Letters[nfd[0] - 'A'], weight); + letter = nfd[0]; return true; } return false; diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs index 019ca578..d1db5280 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs @@ -44,6 +44,7 @@ public static bool TryGet(char c, out ReadOnlySpan primaries, out ReadOnly primaries = secondaries = default; if (Suppressed) return false; Table table = Loaded.Value; + if (!IsSet(table.OverrideBits, c)) return false; int index = Array.BinarySearch(table.CodePoints, c); if (index < 0) return false; primaries = table.PrimaryBytes.AsSpan(table.PrimaryOffsets[index], table.PrimaryLengths[index]); @@ -59,21 +60,21 @@ public static bool TryGet(char c, out ReadOnlySpan primaries, out ReadOnly /// 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]; - } + public static bool IsIgnorable(char c) => !Suppressed && IsSet(Loaded.Value.IgnorableBits, c); + /// Both questions are asked of every character a version-1 key is built from, and the answer is + /// almost always no: 501 overrides and about 5,000 ignorables in 65,536 code points. A bit per code point — + /// 8 KB a set — answers in one read, where a binary search over each was a seventh of building a key. The + /// override's weights are still found by searching, but only for a character that has some. private sealed record Table( char[] CodePoints, byte[] PrimaryLengths, int[] PrimaryOffsets, byte[] PrimaryBytes, byte[] SecondaryLengths, int[] SecondaryOffsets, byte[] SecondaryBytes, - int[] IgnorableStarts, int[] IgnorableLengths); + ulong[] OverrideBits, ulong[] IgnorableBits); + + private static bool IsSet(ulong[] bits, char c) => (bits[c >> 6] & (1UL << (c & 63))) != 0; + + private static void Set(ulong[] bits, int c) => bits[c >> 6] |= 1UL << (c & 63); private static readonly LazyShared by every order in : no entries, no reversal, nothing to do. diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs index 66dcae76..6238c78c 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs @@ -290,7 +290,7 @@ private sealed class EncodeScratch public List Secondaries { get; } = []; public List<(int Position, byte Code)> Inline { get; } = []; public List Kana { get; } = []; - public List Prolonged { get; } = []; + public List KanaMarks { get; } = []; public void Clear() { @@ -298,7 +298,7 @@ public void Clear() Secondaries.Clear(); Inline.Clear(); Kana.Clear(); - Prolonged.Clear(); + KanaMarks.Clear(); } } @@ -335,9 +335,9 @@ public static bool TryEncode( List<(int Position, byte Code)> inline = scratch.Inline; // One entry per kana in the string: true for a small form. Emitted as a section of its own. List kana = scratch.Kana; - // 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. - List prolonged = scratch.Prolonged; + // What each kana is — a letter, a prolonged sound mark or an iteration mark. Packed like the small + // flags but with its own codes, into a section of its own. + List marks = scratch.KanaMarks; // 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; @@ -349,11 +349,35 @@ public static bool TryEncode( // the string and after a weight that folded into its predecessor — in both cases there is nothing to // double. byte[] lastWeight = []; + bool lastWeightIsKana = false; + + // What an iteration mark copies: the one weight — or the one inline record — the character before it + // contributed, and whether that was a kana. A character contributing two weights (ß, æ, a ligature) or a + // weight and an inline record leaves nothing to copy, and so does a mark that found nothing: ß々 and + // 々々 keep FF FF. A character contributing NOTHING is transparent — a combining accent folding into the + // letter before it, or an astral character, which version 0 ignores: e + U+0301 + 々 and a𐀀々 both copy + // the letter. Recomputed from the counts at the start of each character, except after a mark or a + // combining voicing, which leave it as it was — かゝゝ repeats か twice, and かーゝ repeats か, not the vowel. + byte[]? repeatable = null; + bool repeatableIsKana = false; + bool keepRepeatable = true; + int weightsAtCharacter = 0, recordsAtCharacter = 0; // 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++) { + int weights = secondaries.Count - weightsAtCharacter, records = inline.Count - recordsAtCharacter; + if (!keepRepeatable && (weights > 0 || records > 0)) + { + bool single = weights == 1 && records == 0 || weights == 0 && records == 1; + repeatable = single ? lastWeight : null; + repeatableIsKana = single && lastWeightIsKana; + } + keepRepeatable = false; + weightsAtCharacter = secondaries.Count; + recordsAtCharacter = inline.Count; + char c = s[position]; char u = char.ToUpperInvariant(c); @@ -377,12 +401,16 @@ public static bool TryEncode( // The 20 hand-verified ignorables first, then the 40 measured ones — 60 across the BMP, every dash // and quotation form, the Arabic harakat, and the CJK and fullwidth punctuation — then the 60 // remaining controls, which are word-sort ignorables too. - if (Ignorables.TryGetValue(c, out byte code) || - JetTextCollationTableV0.TryGetInlineCode(c, out code) || - TryGetControlInlineCode(c, out code)) + // A locale that weighs one as a character of its own wins: the Japanese orders give U+2015 the + // unweighted FF FF where General records it here. + if ((Ignorables.TryGetValue(c, out byte code) || + JetTextCollationTableV0.TryGetInlineCode(c, out code) || + TryGetControlInlineCode(c, out code)) && + tailoring?.TryMatchSingle(c, out _) != true) { inline.Add((secondaries.Count, code)); lastWeight = [InlineMid, code]; + lastWeightIsKana = false; continue; } @@ -397,8 +425,9 @@ public static bool TryEncode( { AddWeight([JetKanaSection.KanaPage, kanaVowel], DefaultSecondary); kana.Add(kanaSmall); - prolonged.Add(true); + marks.Add(JetKanaSection.Prolonged); kanaWeight = secondaries.Count - 1; + keepRepeatable = true; continue; } @@ -406,8 +435,9 @@ public static bool TryEncode( out byte vowel)) { AddWeight([JetKanaSection.KanaPage, sound], voicing); + lastWeightIsKana = true; kana.Add(small); - prolonged.Add(false); + marks.Add(JetKanaSection.Letter); kanaWeight = secondaries.Count - 1; kanaVowel = vowel; kanaSmall = small; @@ -423,6 +453,35 @@ public static bool TryEncode( if (c is (char)0xFF9E or (char)0xFF9F && kanaWeight >= 0 && kanaWeight == secondaries.Count - 1) { secondaries[kanaWeight] = c == (char)0xFF9E ? (byte)0x03 : (byte)0x04; + keepRepeatable = true; + continue; + } + + // An iteration mark weighs as a copy of what the character before it contributed, with its own + // secondary (see JetKanaSection.TryGetIterationMark). Copying a kana makes it a kana too, recorded + // in the kana section as a repeat and inheriting the small flag — ゃゝ is small twice — and it + // stands where a halfwidth voicing mark or a long vowel mark can reach it: カヾ voices the copy, + // かゝー lengthens it. With nothing to copy it is the unweighted FF FF — and, like a shadda with nothing + // to double, it takes NO secondary slot: 々が is secondaries 03, not 02 03, which the Korean hanja + // (whose secondaries are never the default) are what showed. It leaves nothing to copy after it. + // A locale that weighs the mark as a character of its own wins, as its tailoring wins everywhere: + // Japanese radical/stroke order gives 々 a radical weight, and 人々 is then two different weights. + if (JetKanaSection.TryGetIterationMark(c, version1: false, out byte markSecondary) && + tailoring?.TryMatchSingle(c, out _) != true) + { + keepRepeatable = true; + if (repeatable is null) + { + primaries.AddRange((byte[])[0xFF, 0xFF]); + continue; + } + AddWeight(repeatable, markSecondary, final: true); + if (repeatableIsKana) + { + kana.Add(kanaSmall); + marks.Add(JetKanaSection.Repeat); + kanaWeight = secondaries.Count - 1; + } continue; } @@ -444,7 +503,7 @@ public static bool TryEncode( if (c == Shadda) { if (lastWeight.Length > 0) - AddWeight(lastWeight, DefaultSecondary); + AddWeight(lastWeight, DefaultSecondary, final: true); else primaries.AddRange((byte[])[0xFF, 0xFF]); continue; @@ -455,7 +514,7 @@ public static bool TryEncode( 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); + AddWeight(tailored.Primaries, tailored.Secondary, final: true); position += consumed - 1; } // A ligature character weighs as its decomposition, one component at a time — ACE stores DŽ @@ -500,7 +559,7 @@ public static bool TryEncode( hasWordSortRecord = inline.Count > 0; - if (kana.Count > 0) JetKanaSection.Append(output, kana, prolonged); + if (kana.Count > 0) JetKanaSection.Append(output, kana, marks); // 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 "-あ". @@ -545,7 +604,7 @@ bool WeighCharacter(char character) 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); + AddWeight(tailoredOne.Primaries, tailoredOne.Secondary, final: true); else if (ExtraLetters.TryGetValue(character, out TailoredWeight own) || ExtraLetters.TryGetValue(upper, out own)) AddWeight(own.Primaries, own.Secondary); @@ -589,17 +648,29 @@ bool WeighCharacter(char character) void Add(byte primary, byte secondary = DefaultSecondary) { + if (tailoring?.LeadBytes is { } leads) primary = leads[primary]; primaries.Add(primary); secondaries.Add(secondary); lastWeight = [primary]; + lastWeightIsKana = false; } // 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) + // + // `final` marks bytes that are already what the key holds — a tailored entry, or a copy of a weight + // already added — which an order moving General's lead bytes (Korean) must not move a second time. + void AddWeight(ReadOnlySpan weight, byte secondary, bool final = false) { + if (!final && !weight.IsEmpty && tailoring?.LeadBytes is { } leads && leads[weight[0]] != weight[0]) + { + byte[] moved = weight.ToArray(); + moved[0] = leads[moved[0]]; + weight = moved; + } + // 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 @@ -618,6 +689,7 @@ void AddWeight(ReadOnlySpan weight, byte secondary) foreach (byte b in weight) primaries.Add(b); secondaries.Add(secondary); lastWeight = weight.ToArray(); + lastWeightIsKana = false; } } diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs index 2f68fe07..ba8aced8 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs @@ -96,19 +96,48 @@ public static bool TryEncode( // 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). + // A Han character is the exception: its FD FF marker counts as a weight of its own, so 人- puts the + // hyphen at 0x0F and 人人- at 0x17, though each Han character takes one secondary slot. A one-byte + // primary still counts once (a Lao vowel then a hyphen is 0x0B), so this is not a byte count halved. var inline = new List<(int Position, byte ScriptMember, byte AlphabeticWeight)>(); + int hanMarkers = 0; - // The kana small/normal and prolonged-mark flags, and the running state the prolonged mark needs. + // The kana small/normal flags and mark codes, and the running state the prolonged mark needs. var kana = new List(); - var prolonged = new List(); + var marks = new List(); int kanaWeight = -1; byte kanaVowel = 0; bool kanaSmall = false; + // What an iteration mark copies — the rule version 0 follows (see JetTextCollation), except in WHAT is + // copied: the table's (script member, alphabetic weight) pair, not the bytes the weight was encoded as. + // The two differ exactly where the encoding rearranges the table: a Han character encodes as FD FF AW DW + // and is copied as 05 AW (人々 is FDFF3D26 053D), a jamo encodes as AW DW and is copied as 04 AW (ᄀ々 is + // C002 04C0). Everywhere else the pair IS the primary, so a kana, a Hangul syllable, a measured + // override or a tailored letter is copied as it was written. + CopiedPrimary lastUnit = new([]); + bool lastUnitIsKana = false; + CopiedPrimary? repeatable = null; + bool repeatableIsKana = false; + bool keepRepeatable = true; + int weightsAtCharacter = 0, recordsAtCharacter = 0; + // 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++) { + // A character contributing nothing is transparent, as in version 0: e + U+0301 + 々 copies the e. + int weights = secondaries.Count - weightsAtCharacter, records = inline.Count - recordsAtCharacter; + if (!keepRepeatable && (weights > 0 || records > 0)) + { + bool single = weights == 1 && records == 0 || weights == 0 && records == 1; + repeatable = single ? lastUnit : null; + repeatableIsKana = single && lastUnitIsKana; + } + keepRepeatable = false; + weightsAtCharacter = secondaries.Count; + recordsAtCharacter = inline.Count; + char character = text[position]; // A locale tailoring overrides everything below it. The mechanism is v0's exactly — the entries @@ -123,6 +152,8 @@ public static bool TryEncode( primaries.AddRange(tailored.Primaries); secondaries.Add(tailored.Secondary); } + lastUnit = new(tailored.Primaries); + lastUnitIsKana = false; position += consumed - 1; continue; } @@ -155,8 +186,9 @@ public static bool TryEncode( primaries.Add(kanaVowel); secondaries.Add(DefaultSecondary); kana.Add(kanaSmall); - prolonged.Add(true); + marks.Add(JetKanaSection.Prolonged); kanaWeight = secondaries.Count - 1; + keepRepeatable = true; continue; } @@ -169,9 +201,32 @@ public static bool TryEncode( kanaWeight == secondaries.Count - 1) { secondaries[kanaWeight] = character == (char)0xFF9E ? (byte)0x03 : (byte)0x04; + keepRepeatable = true; continue; } + // An iteration mark: a copy of what came before, with the mark's own secondary — the same rule as + // version 0, with 〻 and ꀕ added (JetKanaSection.TryGetIterationMark). A tailoring that weighs the + // mark as a character of its own has already taken it above. With nothing to copy it is weighed + // alone below, as the FF FF the measured overrides hold — with no secondary slot, which is what the + // overrides carry — and leaves nothing to copy after it. + if (JetKanaSection.TryGetIterationMark(character, version1: true, out byte markSecondary)) + { + keepRepeatable = true; + if (repeatable is { } copy) + { + copy.AppendTo(primaries); + secondaries.Add(markSecondary); + if (repeatableIsKana) + { + kana.Add(kanaSmall); + marks.Add(JetKanaSection.Repeat); + kanaWeight = secondaries.Count - 1; + } + continue; + } + } + if (JetTextCollationTableV0.TryGetKana( character, out byte sound, out byte voicing, out bool small, out byte vowel)) { @@ -188,8 +243,10 @@ public static bool TryEncode( primaries.Add(JetKanaSection.KanaPage); primaries.Add(sound); secondaries.Add(voicing); + lastUnit = new(JetKanaSection.KanaPage, sound); + lastUnitIsKana = true; kana.Add(small); - prolonged.Add(false); + marks.Add(JetKanaSection.Letter); kanaWeight = secondaries.Count - 1; kanaVowel = vowel; kanaSmall = small; @@ -216,6 +273,8 @@ public static bool TryEncode( { foreach (byte b in measuredPrimaries) primaries.Add(b); foreach (byte b in measuredSecondaries) secondaries.Add(b); + lastUnit = new(measuredPrimaries.ToArray()); + lastUnitIsKana = false; continue; } @@ -241,8 +300,10 @@ public static bool TryEncode( primaries.Add(JetKanaSection.KanaPage); primaries.Add(tableSound); secondaries.Add(tableVoicing); + lastUnit = new(JetKanaSection.KanaPage, tableSound); + lastUnitIsKana = true; kana.Add(isSmall); - prolonged.Add(false); + marks.Add(JetKanaSection.Letter); kanaWeight = secondaries.Count - 1; kanaVowel = 0; kanaSmall = isSmall; @@ -260,11 +321,13 @@ public static bool TryEncode( // expanding a ligature could trip a digraph entry that the original text never contained. foreach (char expanded in sequence) { - if (tailoring is not null && + if (tailoring is { WeighsDecompositions: true } && tailoring.TryMatchSingle(expanded, out TailoredWeight component)) { primaries.AddRange(component.Primaries); secondaries.Add(component.Secondary); + lastUnit = new(component.Primaries); + lastUnitIsKana = false; continue; } if (!Append(expanded)) return false; @@ -285,12 +348,18 @@ bool Append(char character) // instead records inline as 0x83 — a real difference between the two weight tables). if (scriptMember == 0 && alphabetic == 0) return true; + // Whatever this character contributes, the pair is what an iteration mark after it would copy. + if (alphabetic != 0) + { + lastUnit = new(scriptMember, alphabetic); + lastUnitIsKana = false; + } + 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)); + // secondaries.Count is the weight count — every weight contributes exactly one secondary slot — + // plus one for each Han marker (see the declaration of `inline`). + inline.Add((secondaries.Count + hanMarkers, scriptMember, alphabetic)); return true; } @@ -305,6 +374,7 @@ bool Append(char character) primaries.Add(alphabetic); primaries.Add(diacritic); secondaries.Add(DefaultSecondary); + hanMarkers++; return true; } @@ -365,7 +435,7 @@ bool Append(char character) hasWordSortRecord = inline.Count > 0; - if (kana.Count > 0) JetKanaSection.Append(output, kana, prolonged); + if (kana.Count > 0) JetKanaSection.Append(output, kana, marks); // 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 @@ -477,6 +547,30 @@ private static int ReadVarInt(byte[] data, ref int offset) } } + /// The primary an iteration mark would copy, recorded for every character in case one follows. Almost + /// always a two-byte pair, held inline so the recording allocates nothing; a tailored or measured weight keeps + /// the array it already has. + private readonly struct CopiedPrimary + { + private readonly byte[]? bytes; + private readonly byte first, second; + + public CopiedPrimary(byte[] bytes) => this.bytes = bytes; + + public CopiedPrimary(byte first, byte second) => (this.first, this.second) = (first, second); + + public void AppendTo(List primaries) + { + if (bytes is not null) + { + primaries.AddRange(bytes); + return; + } + primaries.Add(first); + primaries.Add(second); + } + } + /// Weights in parallel arrays, reached through a slot per BMP code point — the entry's index plus /// one, zero for none. Arrays rather than a dictionary keep the table near 400 KB resident instead of several /// MB; the slots, 128 KB of it, replaced a binary search that every character of every comparison paid. diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs index 6e055e46..019ca578 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1Overrides.cs @@ -124,7 +124,9 @@ private static Table Load() starts, lengths); } - private static byte[] ReadStream(BinaryReader reader) + /// Reads one length-prefixed zlib stream — the unit every generated collation resource is built + /// from. + internal static byte[] ReadStream(BinaryReader reader) { byte[] compressed = reader.ReadBytes(reader.ReadInt32()); var output = new MemoryStream(); @@ -133,7 +135,9 @@ private static byte[] ReadStream(BinaryReader reader) return output.ToArray(); } - private static int ReadVarInt(byte[] source, ref int offset) + /// Reads a little-endian base-128 integer, seven bits a byte, the high bit set on all but the last. + /// + internal static int ReadVarInt(byte[] source, ref int offset) { int value = 0, shift = 0; while (true) diff --git a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs index 295c181f..db0e5c9b 100644 --- a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs +++ b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs @@ -330,7 +330,9 @@ private static bool TryNiladicFunction(ColumnReference c, out object? value) // More VBA/Access built-ins (verified vs ACE via the function-whitelist sweep). All NULL-propagating // via Convert1 unless noted; positions are 1-based. // Asc and Chr work in the system ANSI code page (Chr takes 0-255; Chr(128) is '€', Asc('Ā') is 65 by - // best fit); AscW and ChrW in UTF-16 code units, AscW signed and ChrW taking -32768 to 65535. + // best fit); AscW and ChrW in UTF-16 code units, AscW signed and ChrW taking -32768 to 65535. The + // machine's code page, NOT the database's: in a 1251, 1253 or 932 database ACE on a 1252 machine still + // gives Chr(192) = 'À', and Asc of a Cyrillic or Greek letter is 63. "ASC" => Convert1(f, v => (int)Ansi.GetBytes(FirstCharacter(v))[0]), "CHR" => Convert1(f, v => AnsiCharacter(InRange(AsLong(v), 0, 255)).ToString()), "SPACE" => Convert1(f, v => new string(' ', Count(v))), diff --git a/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs b/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs index 8e2056c3..d29031a5 100644 --- a/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs +++ b/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs @@ -27,8 +27,8 @@ namespace LibRed.Engine.Execution; /// pattern, in a bracket list and in the value, so 'aßb' LIKE 'a[s]sb' is True; _ still takes the whole /// character. The table is measured (LikeFoldTableGeneratorTest) and embedded, so LIKE answers the same on every /// platform. -/// None of it depends on the database's collation: the same pairs match in every one of the 405 orders LibRed -/// can create, Turkish included (verified vs ACE). Width, kana, superscripts, hyphens and apostrophes, which the +/// None of it depends on the database's collation: the same pairs match in every one of the 405 non-CJK orders +/// LibRed can create, Turkish included (verified vs ACE; the CJK orders are not yet screened). Width, kana, superscripts, hyphens and apostrophes, which the /// collation folds or ignores, all count here, and so do trailing spaces: 'a ' LIKE 'a' is False where /// 'a ' = 'a' is True. /// A bracket that is never closed, or a range written backwards, is an invalid pattern. It is only reported diff --git a/src/LibRed/README.md b/src/LibRed/README.md index 6ea6fe9b..03a8107a 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -147,10 +147,15 @@ Treat the number as of its date — an EF Core version bump moves it. is retailored at all and a word needs two accents before the order diverges. Covered: French, German (incl. Phone Book), Spanish Traditional and Modern, Czech, Slovak, Polish, Hungarian (incl. Technical), Croatian, Bosnian, Serbian, Slovenian, Romanian, Turkish, Estonian, Latvian, Lithuanian, Icelandic, - Norwegian/Danish, Swedish/Finnish, Ukrainian, Macedonian, Vietnamese, Georgian Modern, Indic and Thai. + Norwegian/Danish, Swedish/Finnish, Ukrainian, Macedonian, Vietnamese, Georgian Modern, Indic and Thai — + and the **Chinese, Japanese and Korean** orders (Pronunciation, Stroke Count, Bopomofo, Radical/Stroke, + in both versions where Access offers both): General plus a measured table of ideograph weights, and for + Korean a reordering that sorts Hangul first. The two "Unicode" CJK orders stay refused — Access creates + them but ACE will not open them. Iteration marks (`々`, `ゝ`, …) repeat the weight before them, as ACE's do. The collation itself is **one 32-bit LCID** — LANGID plus a sort id that distinguishes an alternate order for the same language (Hungarian Technical from Hungarian, German Phone Book from German) — plus the - version byte. Five orders DAO still offers (Arabic, Greek, Hebrew, Dutch, Cyrillic) are **inert**: ACE + version byte. A database created in an order carries that language's ANSI code page on page 0, as + Access's do. Five orders DAO still offers (Arabic, Greek, Hebrew, Dutch, Cyrillic) are **inert**: ACE records them faithfully and then encodes General keys anyway, verified over 82 samples. Also handled: the 510-byte index-entry limit with ACE's own truncation checksum, characters above the BMP, and the 16-bit inline word-sort position. See `docs/format/page-03-04-index-btree.md` and diff --git a/src/LibRed/docs/format/page-00-database.md b/src/LibRed/docs/format/page-00-database.md index ee82b2eb..6e09fd42 100644 --- a/src/LibRed/docs/format/page-00-database.md +++ b/src/LibRed/docs/format/page-00-database.md @@ -18,7 +18,7 @@ | `0x1C` | 4 | **Global released-pages map pointer** — `[row:1][page:3]`; `0x00000101` = page 1 row 1 ([page-05 §9.1](page-05-usage-maps.md)) | | `0x20`–`0x2C` | 4×4 | **System-catalog bootstrap pointers**: TDEF pages of `MSysObjects` / `MSysACEs` / `MSysQueries` / `MSysRelationships` = `2, 3, 4, 5`. `0x20` is the **catalog root** (how the engine finds `MSysObjects`). | | `0x30`–`0x3B` | 12 | Zero in every file seen, but **not merely reserved**: ACE range-checks `0x30` and `0x34` as `[row:1][page:3]` pointers, against the largest possible page only (see below) | -| `0x3C` | 2 | **ANSI code page** — LE (`0x04E4` = 1252, `0x04E2` = 1250) | +| `0x3C` | 2 | **ANSI code page** of the collation's language — LE (`0x04E4` = 1252, `0x04E2` = 1250, `0` = none); see below | | `0x3E` | 4 | **Database (encryption) key** — 0 when there is no password | | `0x42` | 40 | **Password** (Jet 4; Jet 3 = 20 bytes) — additionally masked by a creation-date-derived value, so an empty password does not read as zeroes | | `0x6A` | 4 | **Creating engine's build number** — `0x000011A6` (4518) on everything ACE writes, but **not a constant**: Jet-4-authored files carry the build of the `msjet40.dll` that created them (see below) | @@ -205,14 +205,22 @@ CF 65 ED FF 07 C7 46 A1 78 16 0C ED E9 2D 62 D4 ; 0x88 ``` **Verified** against real files: at the code page (`mask[0x3C]=7B,42`) the mask yields the canonical -Windows code pages `0x04E4`/`0x04E2`; at the collation LCID (`mask[0x6E]=01,1B`) it agrees with each +Windows code page of the collation's language; at the collation LCID (`mask[0x6E]=01,1B`) it agrees with each column descriptor's own locale at `0x0B`–`0x0C`; at the creation date (`mask[0x72]=12 4F 4A 94 6C 3E 60 26`) it matches `MSysObjects.DateCreate` to the second. Applied whole, it also yields a zero database key on no-password files and an empty password that unmasks to the creation-date-derived pattern (below). **Decoded fields** (all little-endian; `DatabaseDefinitionPage` → `JetDatabase`): -- **Code page (`0x3C`, 2 bytes)** → `CodePage` (1252 / 1250). +- **Code page (`0x3C`, 2 bytes)** → `CodePage`. The Windows ANSI code page of the **language** of the + database's collation (`0x6E`): 1252 for English and Western Europe, 1250 Central Europe, 1251 Cyrillic, + 1253 Greek, 1254 Turkish and Azerbaijani, 1255 Hebrew, 1256 Arabic, 1257 Baltic, 1258 Vietnamese, 874 Thai, + 932 Japanese, 936 Chinese Simplified, 949 Korean, 950 Chinese Traditional — and **`0`** for a language + with no ANSI code page (Georgian, Armenian, Hindi and the other Indic languages, Khmer, …). It follows the + LANGID alone: every sort id of a language stamps the same value (Chinese Pronunciation and Stroke Count + are both 936, Georgian and Georgian Modern both 0), and so do both collation versions. A creator does not + choose it — DAO's `CreateDatabase` takes a `CP=` in its locale string and ignores it, stamping the + language's code page whatever is passed. - **Database key (`0x3E`, 4 bytes)** → `DatabaseKey`. **Zero ⇒ the data pages are unencrypted; nonzero ⇒ the file is ACE-encrypted** (Access 2007+ "Set Database Password" encrypts the whole database). Verified: an unprotected file reads `0`; a password-protected `.accdb` reads a nonzero diff --git a/src/LibRed/docs/format/page-03-04-index-btree.md b/src/LibRed/docs/format/page-03-04-index-btree.md index b0479a67..7c6246ce 100644 --- a/src/LibRed/docs/format/page-03-04-index-btree.md +++ b/src/LibRed/docs/format/page-03-04-index-btree.md @@ -376,6 +376,11 @@ Then the value, transformed: > **Weights, not bytes.** The two agree for everything Latin, so only a two-byte weight tells them apart: > `£-` puts the hyphen at `0x0B` (`0x07 + 4×1`) although `£` is `34 A7`, and `©`, `½`, `Ω`, `б` all behave > the same, while `£A-` is `0x0F`. So both the secondary section and this one index by weight. + > + > **Except a version-1 Han character, which counts twice here.** Its four-byte `FD FF AW DW` primary takes + > two positions — `人-` puts the hyphen at `0x0F`, `人人-` at `0x17` — though it takes a single secondary + > slot. It is not a byte count halved: a one-byte version-1 primary (a Lao vowel) still counts once, and a + > Hangul syllable, a jamo and a kana once each. Version 0's Han weights are two bytes and count once. > **Why those two characters specifically:** this is Windows' documented **word sort**, the default for > the NLS sorting functions — *"all punctuation marks and other nonalphanumeric characters, except for the @@ -498,8 +503,35 @@ Then the value, transformed: > あいー [01,01,11] 10|01 01 11 = 97 あああー [01,01,01,11] = 95 B0 > ``` > - > So the section is `01 01 FF 02 80 FF 80`. With no kana before it, `ー` is - > nothing special and keeps the ordinary `FF FF` primary the table holds for it. + > So the section is `01 01 FF 02 80 FF 80`. With no kana before it, `ー` is an + > iteration mark instead (below). + > + > **Iteration marks repeat the weight before them.** `々`, `ゝ`/`ヽ`/`〱` and their voiced forms + > `ゞ`/`ヾ`/`〲` — and, in version 1 only, `〻` and the Yi `ꀕ`, which version 0 ignores — weigh as a + > **copy of the one weight the character before them contributed**, carrying the mark's *own* secondary: + > `々` `05`, `ゝ` `02`, `ゞ` `03`, `〻` `05`, `ꀕ` `07`. The copy does not inherit the secondary it repeats: + > `がゝ` is `が` twice with secondaries `03 02`, `é々` is `e` twice with `0E 05`. `ー` is one wherever it has + > no kana to lengthen — `人ー` and `aー` double the character before. The rules, all measured against ACE + > under both versions: + > + > - **After a kana the copy is a kana**: it joins the kana section with mark code `10` (`かゝ` closes + > `FF 98`, where `かー` closes `FF 9C`), inherits the small flag (`ゃゝ` packs small+small), and a + > following halfwidth voicing mark or long vowel mark reaches it (`カヾ` voices the copy, `かゝー` + > lengthens it). `々` after a kana is a kana repeat too, with its own `05`. + > - **A mark leaves what it copies unchanged**, so a chain repeats the same weight (`人々々`, `かゝゝ`), and + > a kana repeat after `ー` copies the last kana *letter*, not the vowel (`かーゝ` = `か`,`あ`,`か`). + > - **Only a character that contributed exactly one weight can be copied** — or one inline record, whose + > `06 xx` pair then moves into the primaries (`'々`). After an expansion (`ß`, `æ`, a ligature), at the + > start of the string, or after a mark that found nothing, the mark weighs alone as its table's `FF FF` + > and leaves nothing to copy after it: `ß々`, `々々` and `ー々` keep `FF FF`. A character contributing + > nothing — a combining accent folding into the letter before, or an astral character in version 0 — is + > transparent: `e`+U+0301+`々` and `a𐀀々` copy the letter. + > - **Version 1 copies the table's (script member, alphabetic weight) pair**, which is the primary itself + > except where the encoding rearranges it: a Han character encodes as `FD FF AW DW` and is copied as + > `05 AW` (`人々` = `FDFF3D26 053D`), a jamo encodes as `AW DW` and is copied as `04 AW` (`ᄀ々` = + > `C002 04C0`). + > - **A locale that weighs the mark as a character of its own wins**: Japanese radical/stroke order gives + > `々` a radical weight, and `人々` is then two different weights. > > **Version 1 builds the kana section identically** — same sound weights, same framing, byte for byte: > ACE encodes `U+304C` as `7F 7F0A 01 03 0101 FF 02 80 FF 80 00` under both orders. The two versions @@ -514,7 +546,7 @@ Then the value, transformed: > sound and voicing. The one fact it does not supply is the small flag, and that cannot be inferred from > reaching that path: script member 3 also collects the circled forms, which are *not* small, and the > iteration marks, the lone prolonged mark and the double hyphen, which are not kana letters at all and - > which ACE gives the unweighted `FF FF` primary and no kana section. + > which ACE gives the unweighted `FF FF` primary and no kana section when they stand alone. > > What `02 80 FF 80` denotes is still not established; it never varies, so it is emitted as a literal. > @@ -588,8 +620,9 @@ Then the value, transformed: the fixed-type keys. **Locale-specific orders.** A database can be created with a sort order other than General; Access exposes - them as the "New Database Sort Order" list. Verified against ACE for **every non-CJK entry in that list**, - each compared against General v0 (plus orders only DAO can name): + them as the "New Database Sort Order" list. Verified against ACE for **every entry in that list** but the + two Unicode CJK orders ACE will not open, each compared against General of its version (plus orders only + DAO can name): - The stored value is a **true LCID**, not a small enum — Spanish Traditional is `1034` (`0x040A`) and Spanish **Modern** is `3082` (`0x0C0A`). DAO's `CollatingOrderEnum` lists only `dbSortSpanish = 1034`; @@ -822,20 +855,72 @@ Then the value, transformed: > Georgian is the live counterexample on the second axis — `1079` at sort id 0 is in the set, `1079` at > sort id 1 is Georgian Modern with a tailoring of its own. + **The Chinese, Japanese and Korean orders.** Access offers fourteen, told apart by LANGID, sort id and + version: + + | order | LCID | versions | + |---|---|---| + | Chinese Pronunciation | `0x00000804` | 1, 0 (`- Legacy`) | + | Chinese Stroke Count | `0x00020804` | 1, 0 | + | Chinese (Taiwan) Bopomofo | `0x00030404` | 1, 0 | + | Chinese (Taiwan) Stroke Count | `0x00000404` | 1, 0 | + | Japanese | `0x00000411` | 1, 0 | + | Japanese Radical/Stroke Count | `0x00040411` | 1 | + | Japanese Unicode | `0x00010411` | 0 | + | Korean | `0x00000412` | 0 | + | Korean Unicode | `0x00010412` | 0 | + + Measured character by character over the **whole BMP** against ACE, every character the twelve ACE opens + weigh differently from General of their own version gets **one weight** in place of General's: + + - The **ideographs** move, into the order's own sequence — pronunciation, stroke count, Bopomofo, + radical. The Chinese orders move 16,679 to 20,935 characters at version 0 and 24,144 to 27,752 at + version 1; Japanese 7,072 at either version, and Japanese Radical/Stroke Count 13,094. A version-0 entry + is a two-byte primary; a version-1 entry a two-byte primary **and a secondary** (`一` in Chinese Stroke + Count is `C0 02` with `10`), replacing General v1's `FD FF` Han weight. Japanese v0 and v1 depart at the + same code points, the v0 primary being the v1 one with a compacted lead byte (`一` is `81 56` against + `C4 56`). + - A few **symbols** move with them. The Japanese orders weigh `\` as `¥` (fullwidth `\` keeps General's + weight), and give `―` (U+2015) the unweighted `FF FF` where General records it as a word-sort + ignorable; Japanese also moves the kanbun marks (U+3192–319F), the parenthesised and circled ideographs + (`㈠` is `C4 59`, beside `一`'s `C4 56`) and the era squares (U+337B–337F). Japanese Radical/Stroke + Count gives `〃々〆〇` weights of their own ahead of `一` (`々` is `C0 04` with `0D`), so there `人々` is + two different weights and not an iteration. + - **A decomposition does not reach the table.** Version 1's base table expands the CJK radicals and the + compatibility ideographs to the unified ideograph they stand for (`⼀` U+2F00 to `一`), and ACE weighs + them as General does — never with the order's weight for that ideograph. A Latin locale is the opposite: + Croatian's `DŽ` = `D` + *Croatian's* `Ž`. + - An **iteration mark** copies the tailored weight of the ideograph before it. + + **Korean** is General v0 plus a table, but also a **reordering of scripts**: Hangul sorts first, so the + lead byte of every weight General contributes moves — `81`–`F2` down `0x37`, `4A`–`80` (Latin, Greek, + Cyrillic, the other scripts, kana, Bopomofo) up `0x72`, everything below `4A` in place. `A` is `BC`, `가` + is `4A 03`. It moves each **weight**, not each byte: `ᄀ` `81 02` is `4A 02`, and `IJ`'s two weights `59 5B` + are `CB CD`. Every weight General gives the BMP moves by that rule, with no exception in either band. The + table then holds the **hanja**, weighed by their Hangul reading — the syllable's primary, and a secondary + telling apart the hanja that share it (`一` is `66 57` with `41`, `㈠` `66 57` with `47`) — and gives `\` + the weight of `₩`. A tailored weight and an iteration mark's copy are already final and do not move + again. + + **"Japanese Unicode" and "Korean Unicode" cannot be measured.** Access 365 creates a database in either — + creating a file needs no collation — but ACE refuses to open one: *"Selected collating sequence not + supported by the operating system"*, because Windows no longer supports those alternate sorts. With no + engine to measure against, they stay refused. + What remains: **Irish 1084**, unmeasured — Jet accepts it, but not in a process that has loaded the ACE - OLE DB provider, so its ACE keys are unknown; and the **CJK** orders, deliberately out of scope. Three + OLE DB provider, so its ACE keys are unknown; and the two **Unicode** CJK orders above. Three orders — Serbian Latin 2074, Bosnian Latin 5146 and Hindi 1081 — are unreachable at version 0 by construction: Jet refuses them with *"Incorrect collating sequence."*, and they are exactly the three already implemented at version 1. > **Everything above is verified by creation too.** For every combination `IsIndexKeyEncodable` accepts — - > every accepted order at version 0 plus the six orders with a version-1 table — a database LibRed + > every accepted order at version 0 plus every order with a version-1 table — a database LibRed > synthesises in that order, indexed by ACE, holds keys matching LibRed's byte for byte. A wrong LCID > cannot pass quietly: ACE indexes with whatever order that LCID really names. - *Not yet handled:* **Irish 1084** and the **CJK** orders (above). **Six** version-1 collations are - implemented — General v1, Indic v1, Romanian v1, and Croatian / Bosnian / Serbian v1 sharing one table — - and those six are the only non-CJK orders that differ from General v1, so the rest are covered by falling - back to it. DAO writes version 0 for every LANGID it accepts, so a v1 database needs another authoring + *Not yet handled:* **Irish 1084** and the two **Unicode** CJK orders (above). **Six** version-1 collations + besides the CJK ones are implemented — General v1, Indic v1, Romanian v1, and Croatian / Bosnian / Serbian + v1 sharing one table — and those six are the only non-CJK orders that differ from General v1, so the rest + are covered by falling back to it. DAO writes version 0 for every LANGID it accepts, so a v1 database needs another authoring route. - **GUID:** the start flag `0x7F`, then the 16 GUID bytes in **canonical string order** (i.e. `guid.ToString("N")` bytes — **not** the mixed-endian `.ToByteArray()` storage layout), split into two diff --git a/test/LibRed.Core.AccessTests/CjkSortOrderGeneratorTest.cs b/test/LibRed.Core.AccessTests/CjkSortOrderGeneratorTest.cs new file mode 100644 index 00000000..c2b9618a --- /dev/null +++ b/test/LibRed.Core.AccessTests/CjkSortOrderGeneratorTest.cs @@ -0,0 +1,248 @@ +using System.IO.Compression; +using LibRed; +using LibRed.Catalog; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Core.Tests; + +// GENERATOR: the Chinese, Japanese and Korean sort-order resources, one per order. +// +// For each order, ACE encodes every BMP character in an indexed column of the Access-authored fixture the order +// is named after, and each key is compared with what LibRed's General encoder of the same version gives — with +// the CJK tables suppressed, but Korean's lead-byte move left on, since that is a rule rather than data. Every +// character where they differ goes into the order's table as the one weight ACE stored for it: its primary +// bytes, and its secondary. +// +// That every departure IS one weight is a measured claim, and the generator enforces it rather than assuming +// it: a key with two weights, a kana section or an inline record is a mechanism the encoder would have to +// implement, not data a table can carry, so it is reported and the run fails. +// +// Opt-in via LIBRED_GENERATE_CJK=1: each order inserts ~63,000 rows through ACE and rewrites a checked-in binary. +[Collection(AceCollection.Name)] +public class CjkSortOrderGeneratorTest(ITestOutputHelper output) +{ + private const string ResourceDirectory = "src/LibRed/LibRed.Core/Resources/Cjk"; + + public static TheoryData Orders => [.. JetCjkSortOrders.Orders.Select(o => o.Name)]; + + [Theory] + [MemberData(nameof(Orders))] + public void Generate_the_sort_order_resource(string name) + { + Assert.SkipUnless(Environment.GetEnvironmentVariable("LIBRED_GENERATE_CJK") == "1", + "set LIBRED_GENERATE_CJK=1 — this measures ~63,000 characters through ACE and rewrites a resource"); + + // Suppressed before anything can touch a CJK collation — even formatting one reaches the tailoring, via + // IsIndexKeyEncodable — so the generator never needs the resource it is about to write, and bootstraps + // from a stale or absent one. + JetCjkSortOrders.Suppressed = true; + Collation collation = JetCjkSortOrders.Orders.Single(o => o.Name == name).Collation; + string fixture = TestDatabases.Data($"{name}.accdb"); + var entries = new SortedDictionary(); + var leftover = new SortedDictionary(); + int agreed = 0, refused = 0; + try + { + using (var db = JetDatabase.Open(fixture)) + Assert.True(db.Collation == collation, $"{name}.accdb carries {db.Collation}, not {collation}."); + var column = new ColumnDef { Name = "K", Type = JetDataType.Text, Index = 0, Collation = collation }; + + for (int chunk = 0x0000; chunk <= 0xF000; chunk += 0x1000) + { + string[] characters = Range(chunk, chunk + 0x0FFF); + Dictionary ace = AceKeys(fixture, characters); + foreach (string text in characters) + { + if (!ace.TryGetValue(text, out string? key)) { refused++; continue; } + string ours; + try { ours = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [text])); } + catch (NotSupportedException) { ours = "(refused)"; } + if (ours == key) { agreed++; continue; } + if (TryReadOneWeight(key, out byte[] primary, out byte secondary)) + entries[text[0]] = (primary, secondary); + else + leftover[text[0]] = (key, ours); + } + } + } + finally { JetCjkSortOrders.Suppressed = false; } + + output.WriteLine($"{name} (0x{collation.Lcid:X8} v{collation.Version}): {agreed} agree with General, " + + $"{entries.Count} tabulated, " + + $"{leftover.Count} left over, {refused} ACE would not store"); + foreach ((int codePoint, (string ace, string general)) in leftover.Take(40)) + output.WriteLine($" U+{codePoint:X4} ACE {ace,-40} General {general}"); + + byte[] blob = Build(entries); + string directory = Path.Combine(RepositoryRoot(), ResourceDirectory); + Directory.CreateDirectory(directory); + string path = Path.Combine(directory, $"{name}.bin"); + File.WriteAllBytes(path, blob); + output.WriteLine($"wrote {path} ({blob.Length:N0} bytes)"); + + SortedDictionary reloaded = Parse(blob); + Assert.Equal(entries.Keys, reloaded.Keys); + foreach ((int codePoint, (byte[] primary, byte secondary)) in entries) + { + Assert.Equal(primary, reloaded[codePoint].Primary); + Assert.Equal(secondary, reloaded[codePoint].Secondary); + } + Assert.Empty(leftover); + } + + /// + /// Reads a key holding exactly one weight — 7F primary 01 00, or 7F primary 01 secondary 00 + /// — into its primary bytes and its secondary, the default 02 when the section is empty. + /// + private static bool TryReadOneWeight(string hex, out byte[] primary, out byte secondary) + { + byte[] key = Convert.FromHexString(hex); + primary = []; + secondary = 0x02; + if (key.Length < 4 || key[0] != 0x7F || key[^1] != 0x00) return false; + if (key[^2] == 0x01) + { + primary = key[1..^2]; + } + else if (key.Length >= 5 && key[^3] == 0x01) + { + primary = key[1..^3]; + secondary = key[^2]; + } + else return false; + return primary.Length > 0; + } + + /// A count, then four zlib streams: code-point deltas as base-128 integers, primary lengths, + /// primary bytes, and one secondary per entry — what JetCjkSortOrders reads. + private static byte[] Build(SortedDictionary entries) + { + var deltas = new List(); + int previous = 0; + foreach (int codePoint in entries.Keys) + { + WriteVarInt(deltas, codePoint - previous); + previous = codePoint; + } + var stream = new MemoryStream(); + var writer = new BinaryWriter(stream); + writer.Write(entries.Count); + WriteStream(writer, [.. deltas]); + WriteStream(writer, [.. entries.Values.Select(e => (byte)e.Primary.Length)]); + WriteStream(writer, [.. entries.Values.SelectMany(e => e.Primary)]); + WriteStream(writer, [.. entries.Values.Select(e => e.Secondary)]); + writer.Flush(); + return stream.ToArray(); + } + + private static SortedDictionary Parse(byte[] blob) + { + var reader = new BinaryReader(new MemoryStream(blob)); + int count = reader.ReadInt32(); + byte[] deltas = ReadStream(reader), lengths = ReadStream(reader), primaries = ReadStream(reader), + secondaries = ReadStream(reader); + var entries = new SortedDictionary(Comparer.Default); + int codePoint = 0, cursor = 0, offset = 0; + for (int i = 0; i < count; i++) + { + codePoint += ReadVarInt(deltas, ref cursor); + entries[codePoint] = (primaries[offset..(offset + lengths[i])], secondaries[i]); + offset += lengths[i]; + } + return entries; + } + + private static void WriteStream(BinaryWriter writer, byte[] data) + { + var compressed = new MemoryStream(); + using (var deflate = new ZLibStream(compressed, CompressionLevel.SmallestSize, leaveOpen: true)) + deflate.Write(data, 0, data.Length); + writer.Write((int)compressed.Length); + writer.Write(compressed.ToArray()); + } + + 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 void WriteVarInt(List target, int value) + { + while (value >= 0x80) { target.Add((byte)((value & 0x7F) | 0x80)); value >>= 7; } + target.Add((byte)value); + } + + 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; + } + } + + private static string[] Range(int first, int last) + { + var characters = new List(); + for (int c = first; c <= last; c++) + if (!char.IsControl((char)c) && !char.IsSurrogate((char)c)) + characters.Add(((char)c).ToString()); + return [.. characters]; + } + + private static string RepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "EFCore.Jet.sln"))) + directory = directory.Parent; + return directory?.FullName ?? throw new InvalidOperationException("EFCore.Jet.sln not found above the test output."); + } + + private static Dictionary AceKeys(string source, string[] samples) + { + string path = TemporaryDatabase.CopyPath(source, "cjkgen-"); + try + { + using (var connection = AceTestDatabase.Open(path)) + { + Exec(connection, "CREATE TABLE Gen (K TEXT(50), V LONG)"); + Exec(connection, "CREATE INDEX IX_Gen ON Gen (K)"); + for (int i = 0; i < samples.Length; i++) + { + using var insert = connection.CreateCommand(); + insert.CommandText = "INSERT INTO Gen (K, V) VALUES (?, ?)"; + insert.Parameters.AddWithValue("k", samples[i]); + insert.Parameters.AddWithValue("v", i); + try { insert.ExecuteNonQuery(); } catch (Exception) { /* ACE refused this value */ } + } + } + + using var db = JetDatabase.Open(path); + var table = db.OpenTable("Gen"); + IndexDef index = table.Definition.Indexes.Single(i => i.Name == "IX_Gen"); + ColumnDef keyColumn = table.Definition.FindColumn("K")!; + var rows = table.Rows().WithIds().ToDictionary(r => r.Id, r => r.Values); + var keys = new Dictionary(); + foreach ((byte[] stored, RowId rowId) in new IndexCursor(table.Channel, index.RootPage).RawEntries()) + if (rows.TryGetValue(rowId, out object?[]? values) && values[keyColumn.Index] is string text) + keys[text] = Convert.ToHexString(stored); + return keys; + } + finally { TemporaryDatabase.Delete(path); } + } + + private static void Exec(System.Data.OleDb.OleDbConnection connection, string sql) + { + using var command = connection.CreateCommand(); + command.CommandText = sql; + command.ExecuteNonQuery(); + } +} diff --git a/test/LibRed.Core.AccessTests/CodePageProbeTest.cs b/test/LibRed.Core.AccessTests/CodePageProbeTest.cs new file mode 100644 index 00000000..69586489 --- /dev/null +++ b/test/LibRed.Core.AccessTests/CodePageProbeTest.cs @@ -0,0 +1,103 @@ +using System.Globalization; +using System.Reflection; +using LibRed; +using LibRed.Catalog; +using Xunit; + +namespace LibRed.Core.Tests; + +// SURVEY: which ANSI code page ACE writes at page 0x3C for each collation — the measurement behind +// JetCodePages. Access-authored fixtures carry the code page of the order's language (936 for Chinese +// Simplified, 1250 for Czech, 0 for Georgian Modern). DAO takes a CP= in its locale string and ignores it, +// deriving the code page from the LANGID exactly as Access does (Probe_cp_parameter; the fixtures agree), which +// is what makes DAO a fair oracle for the orders no fixture covers. +[Collection(AceCollection.Name)] +public class CodePageProbeTest(ITestOutputHelper output) +{ + private const int UseJet = 2; + private const int Ace12 = 128; + + [Fact] + public void Probe_cp_parameter() + { + object workspace = Workspace(); + foreach (int lcid in (int[])[0x0409, 0x0405, 0x0411, 0x20804, 0x10437, 0x0439]) + foreach (string suffix in (string[])[";CP=1252;COUNTRY=0", ";CP=932;COUNTRY=0", ";COUNTRY=0", ""]) + output.WriteLine($"0x{lcid:X5} {suffix,-20} -> {Create(workspace, $";LANGID=0x{lcid:X4}{suffix}")}"); + } + + [Fact] + public void Dao_against_every_access_fixture() + { + object workspace = Workspace(); + int agree = 0, differ = 0; + foreach (string path in Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, "Data"), "*.accdb").Order()) + { + int lcid, codePage; + try + { + using var db = JetDatabase.Open(path, readOnly: true); + (lcid, codePage) = (db.Collation.Lcid, db.CodePage); + } + catch (InvalidOperationException) { continue; } // the encrypted one + + string dao = Create(workspace, $";LANGID=0x{lcid:X4};COUNTRY=0"); + bool same = dao.StartsWith($"cp {codePage} ", StringComparison.Ordinal); + if (same) agree++; else differ++; + output.WriteLine($"{Path.GetFileNameWithoutExtension(path),-30} 0x{lcid:X5} access cp {codePage,-5} dao {dao}" + + (same ? "" : " <-- differs")); + } + output.WriteLine($"{agree} agree, {differ} differ"); + } + + [Fact] + public void Sweep_every_creatable_lcid() + { + object workspace = Workspace(); + var lcids = new SortedSet(); + foreach (CollatingOrder order in Enum.GetValues()) + foreach (byte version in (byte[])[0, 1]) + foreach (byte sortId in (byte[])[0, 1, 2, 3, 4]) + { + var collation = new Collation(order, version, sortId); + if (collation.IsIndexKeyEncodable) lcids.Add(collation.Lcid); + } + + output.WriteLine($"{lcids.Count} distinct LCIDs"); + foreach (int lcid in lcids) + { + string icu; + try { icu = CultureInfo.GetCultureInfo(lcid & 0xFFFF).TextInfo.ANSICodePage.ToString(CultureInfo.InvariantCulture); } + catch (CultureNotFoundException) { icu = "-"; } + output.WriteLine($"0x{lcid:X5}\t{Create(workspace, $";LANGID=0x{lcid:X4};COUNTRY=0")}\truntime {icu}"); + } + } + + private string Create(object workspace, string locale) + { + string path = TemporaryDatabase.CreatePath("codepage-", ".accdb"); + try + { + object database = Invoke(workspace, "CreateDatabase", path, locale, Ace12)!; + Invoke(database, "Close"); + using var db = JetDatabase.Open(path, readOnly: true); + return $"cp {db.CodePage} lcid 0x{db.Collation.Lcid:X5} v{db.Collation.Version}"; + } + catch (Exception e) + { + return "REJECTED " + ((e as TargetInvocationException)?.InnerException?.Message ?? e.Message).Trim(); + } + finally { TemporaryDatabase.Delete(path); } + } + + private static object Workspace() + { + AceSurveys.RequireOptIn(); + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not available in this process."); + return Invoke(engine!, "CreateWorkspace", "", "admin", "", UseJet)!; + } + + private static object? Invoke(object target, string member, params object?[] args) => + target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); +} diff --git a/test/LibRed.Core.AccessTests/CreatedDatabaseCollationAccessTests.cs b/test/LibRed.Core.AccessTests/CreatedDatabaseCollationAccessTests.cs index e3a4b8bd..6f1c6060 100644 --- a/test/LibRed.Core.AccessTests/CreatedDatabaseCollationAccessTests.cs +++ b/test/LibRed.Core.AccessTests/CreatedDatabaseCollationAccessTests.cs @@ -27,7 +27,7 @@ public static TheoryData EncodableCollations() var data = new TheoryData(); foreach (CollatingOrder order in Enum.GetValues()) foreach (byte version in (byte[])[0, 1]) - foreach (byte sortId in (byte[])[0, 1]) + foreach (byte sortId in (byte[])[0, 1, 2, 3, 4]) // the CJK orders reach sort id 4 if (new Collation(order, version, sortId).IsIndexKeyEncodable) data.Add((int)order, version, sortId); return data; @@ -48,12 +48,15 @@ public void LibRed_creates_a_database_that_ACE_indexes(int order, byte version, Assert.Equal(collation, db.Collation); // Words that exercise the tailorings across the set: accented Latin, the digraphs, a word-sort - // ignorable, and Thai's leading vowel. Any order encodes all of them; what differs is the keys. + // ignorable, Thai's leading vowel, and the CJK tables — ideographs, an iteration mark, hanja beside + // Hangul, and the backslash two of them reweigh. Any order encodes all of them; what differs is the + // keys. string[] samples = [ "apple", "café", "coté", "côte", "Ångström", "co-op", "O'Brien", "ñ", "č", "ž", "lj", "dž", "ch", "ll", "ı", "İ", "å", "ø", "ß", "เก", "ไทย", "Ω", "б", "א", + "一", "漢字", "人々", "カタカナ", "がくせい", "한국", "韓國", @"C:\", ]; Dictionary ace = AceKeys(path, samples); diff --git a/test/LibRed.Core.AccessTests/LocaleCollationAccessTests.cs b/test/LibRed.Core.AccessTests/LocaleCollationAccessTests.cs index 69609074..d599f4fe 100644 --- a/test/LibRed.Core.AccessTests/LocaleCollationAccessTests.cs +++ b/test/LibRed.Core.AccessTests/LocaleCollationAccessTests.cs @@ -36,6 +36,10 @@ public static TheoryData Fixtures() => "Croatian", "Bosnian", "Serbian", // Thai, whose contraction is built as a rule rather than a table of entries. "Thai", + // The Chinese, Japanese and Korean orders — tables of ideograph weights, and Korean's script reordering. + "ChinesePronunciation", "ChinesePronunciationLegacy", "ChineseStrokeCount", "ChineseStrokeCountLegacy", + "ChineseTradBopomofo", "ChineseTradBopomofoLegacy", "ChineseTradStrokeCount", "ChineseTradStrokeCountLegacy", + "Japanese", "JapaneseLegacy", "JapaneseRadicalStrokeCount", "Korean", ]; [Theory] @@ -117,6 +121,23 @@ private static string[] Samples(bool extendedBlocks) if (!char.IsControl((char)c) && !char.IsSurrogate((char)c)) samples.Add(((char)c).ToString()); + // The CJK blocks, where the CJK orders put their weights: the symbols, Bopomofo, jamo and enclosed forms + // whole, and a stride through the large ones — every 7th ideograph, every 11th syllable — which crosses + // every page of their tables while keeping the suite to seconds. The whole BMP is what the generator + // measures; this keeps what it wrote honest. + if (extendedBlocks) + { + (int First, int Last, int Stride)[] cjk = + [ + (0x1100, 0x11FF, 1), (0x3000, 0x303F, 1), (0x3100, 0x312F, 1), (0x3130, 0x318F, 1), + (0x3190, 0x31BF, 1), (0x3200, 0x33FF, 1), (0x3400, 0x4DBF, 7), (0x4E00, 0x9FFF, 7), + (0xAC00, 0xD7A3, 11), (0xF900, 0xFAFF, 1), + ]; + foreach ((int first, int last, int stride) in cjk) + for (int c = first; c <= last; c += stride) + samples.Add(((char)c).ToString()); + } + samples.AddRange([ "apple", "Apple", "APPLE", "cafe", "café", "Ångström", "O'Brien", "Anne-Marie", "co-op", "coop", "Łódź", "Kraków", "İstanbul", "Isparta", "ırmak", "Ğğ", "München", "Grüße", "Bär", "Baer", @@ -140,6 +161,22 @@ private static string[] Samples(bool extendedBlocks) // The prolonged mark: alone, with nothing to lengthen, and after every kind of kana. "ー", "ーあ", "あー", "あいー", "あーい", "ああー", "あああー", "あーー", "あーあー", "ぁー", "がー", "アー", "アー", "コーヒー", "コーヒー", "サーバー", + // Iteration marks: a copy of what came before with the mark's own secondary — after a kana (joining + // the kana section as a repeat), a voiced or small one, an ideograph, a Latin letter, an accented one, + // an inline record, a combining accent; chained, after the long vowel mark, before it, and with + // nothing to copy (at the start, after an expansion, after a mark that found nothing). + "かゝ", "かゞ", "がゝ", "がゞ", "ぱゞ", "ゃゝ", "カヽ", "カヾ", "かヽ", "かゝゝ", "かーゝ", "かゝー", + "カヾ", "ガゝ", "か〱", "か〲", "か々", "か々ゝ", "かー々", "ヴゞ", "㋐ゝ", + "人々", "人々々", "人々ー", "人ゝ", "人ー", "人ー", "人〱", "人〻", "a々", "é々", "é々", "aー", + "'々", "人-々", "½々", "\t々", "ß々", "æ々", "々", "々々", "ゝゝ", "ー々", "々人", "ꀀꀕ", "ꀀꀕꀕ", + "々が", "ーが", "ゝが", "ゞé", "〱é", "ーが", "ß々é", "々々é", "ー々é", + // A Han character counts twice in version 1's inline section, a one-byte primary once. + "人-", "人人-", "人-a", "人'", "ະ-", "aະ-", "가-", "ᄀ-", + // Words through the CJK tables, beside Latin and kana: a tailored weight next to General's, and the + // context rules running over tailored weights (Korean moves the kana a long vowel mark copies). + "中文", "中国", "中國", "漢字", "日本語", "時々", "한글", "대한민국", "韓國", "ㄅㄆㄇ", "\\", + "―", "a中", "中a", "a한", "한a", "カー", "カタカナ", "中-文", "한-글", "가", + "東京タワー", "ソウル", "北京", "chico", "llama", "coche", "calle", "chata", "hodina", "cukr", "ljubav", "njegov", "džem", "meggy", "asszony", "nagy", "cukor", "csak", ]); diff --git a/test/LibRed.Core.AccessTests/LocaleFixtureCollationProbeTest.cs b/test/LibRed.Core.AccessTests/LocaleFixtureCollationProbeTest.cs index 92b8e422..f76f209d 100644 --- a/test/LibRed.Core.AccessTests/LocaleFixtureCollationProbeTest.cs +++ b/test/LibRed.Core.AccessTests/LocaleFixtureCollationProbeTest.cs @@ -96,7 +96,13 @@ public void Probe_every_sort_order_fixture() using (var db = JetDatabase.Open(path)) baselines[name] = db.DefaultCollationVersion == Collation.GeneralVersion ? "v1" : "v0"; Dictionary baseline = baselines[name] == "v1" ? generalV1 : generalV0; - Dictionary theirs = KeysFor(path, name); + Dictionary theirs; + try { theirs = KeysFor(path, name); } + catch (InvalidOperationException ex) + { + output.WriteLine($" {name}: ACE refused it — {ex.InnerException?.Message ?? ex.Message}"); + continue; + } differences[name] = Samples .Where(s => baseline.GetValueOrDefault(s) != theirs.GetValueOrDefault(s)) .Select(s => $"{Describe(s),-16} General {baseline.GetValueOrDefault(s) ?? "(none)",-26} " + diff --git a/test/LibRed.Core.Tests/CollationTests.cs b/test/LibRed.Core.Tests/CollationTests.cs index 264a600d..51655f87 100644 --- a/test/LibRed.Core.Tests/CollationTests.cs +++ b/test/LibRed.Core.Tests/CollationTests.cs @@ -92,12 +92,13 @@ public void The_written_locale_bytes_are_byte_identical_to_the_old_hardcoded_con // This used to be asserted with Cyrillic (1049), on the reasoning that a non-English locale obviously // could not be encoded with the English table. The collation survey measured 1049 and found its keys // byte-identical to General v0, so the example stopped being an example. The two below are refused for - // reasons that will outlive a survey: CJK is deliberately out of scope, and Irish is the one order that - // could not be measured at all. + // reasons that will outlive a survey — there is no engine to measure them against. Access 365 creates a + // database in "Japanese - Unicode", but ACE refuses to open one, because Windows no longer supports that + // alternate sort; and Jet accepts Irish, but not in a process that has loaded ACE. [Theory] - [InlineData((int)CollatingOrder.Japanese, "a CJK order, deliberately out of scope")] - [InlineData(1084, "Irish - accepted by Jet, but never measured against ACE")] - public void Index_key_encoding_refuses_an_unmeasured_collation(int order, string why) + [InlineData((int)CollatingOrder.Japanese, 1, "Japanese - Unicode: created by Access, refused by ACE on open")] + [InlineData(1084, 0, "Irish - accepted by Jet, but never measured against ACE")] + public void Index_key_encoding_refuses_an_unmeasured_collation(int order, byte sortId, string why) { _ = why; // names the case in the test output @@ -105,7 +106,7 @@ public void Index_key_encoding_refuses_an_unmeasured_collation(int order, string { Name = "C", Type = JetDataType.Text, - Collation = new Collation((CollatingOrder)order, 0), + Collation = new Collation((CollatingOrder)order, 0, sortId), }; var ex = Assert.Throws(() => IndexKeyEncoder.Encode([(column, true)], ["abc"])); diff --git a/test/LibRed.Core.Tests/Data/ChinesePronunciation.accdb b/test/LibRed.Core.Tests/Data/ChinesePronunciation.accdb new file mode 100644 index 0000000000000000000000000000000000000000..dcb5be9ad7abb21300ab221cf9c4c8407470cbc2 GIT binary patch literal 479232 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 P%;VtBKu?1+@k;vtXt)E_ literal 0 HcmV?d00001 diff --git a/test/LibRed.Core.Tests/Data/ChinesePronunciationLegacy.accdb b/test/LibRed.Core.Tests/Data/ChinesePronunciationLegacy.accdb new file mode 100644 index 0000000000000000000000000000000000000000..a8822c27faebeb9505984ba39007640af3e98aae GIT binary patch literal 446464 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 literal 0 HcmV?d00001 diff --git a/test/LibRed.Core.Tests/Data/ChineseStrokeCount.accdb b/test/LibRed.Core.Tests/Data/ChineseStrokeCount.accdb new file mode 100644 index 0000000000000000000000000000000000000000..5d06518eed2f33abf19785b9bbcfd08e38e0c043 GIT binary patch literal 454656 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 S&-y>>|E&MB{(tDz|NkEo=3Y(! literal 0 HcmV?d00001 diff --git a/test/LibRed.Core.Tests/Data/ChineseStrokeCountLegacy.accdb b/test/LibRed.Core.Tests/Data/ChineseStrokeCountLegacy.accdb new file mode 100644 index 0000000000000000000000000000000000000000..5f083c0a5faf1a4be710d8a011989d2acb7c2a6c GIT binary patch literal 450560 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 Zf7bt5|7ZQ5^?%m?S^sDKfB&oh|3CF&3vU1b literal 0 HcmV?d00001 diff --git a/test/LibRed.Core.Tests/Data/ChineseTradBopomofoLegacy.accdb b/test/LibRed.Core.Tests/Data/ChineseTradBopomofoLegacy.accdb new file mode 100644 index 0000000000000000000000000000000000000000..94e6b13465bac91771cfc5d684fb4061c79020d3 GIT binary patch literal 446464 zcmeI52VhiH+J?`anG{lHl8E#&^cqY;M}agV#Dow+k)n`fk`PEE2_*_9;##ol3c9vc zEV#P*uf4&duD!d8y|=YDbk_?1`<**?W-^%oLZs+>CpYuWso#0dx#e_aC{<8jSmi0K z^|;eACb=`yl}#yidzHOtVY>%re*Cwx+SG~a(i8rA?v9f$PJXE2p3jpuz4hc}#jm{o zucy*}I(^gB>3N@xUi;OhKPG%K@WV%Jr!Q=BA3v(s!*AVuRq7!bUAx{q{Ozp`^RkRp zgRgpf)nCGTUh+%jl)ujY;p$UL7p(jK*|mL7E_=A=fXsh?@l46*J0c#*x^iWQn9n~> zEP7!^xKbmAB|#G&BtQZrKmsH{0wh2JBtQZrKmu(+V78HAD1QF11PsIDLKA>KZ0cUu za#vfFDuzJEcjo4*J~t zd6l!;1T0+(;#IVY*Fytjcz}Xa1E(BA%&%s0jD*!r*u zY8gT*Q8C(N8f>c(PoyuDD3zyHXsv7EYa0BDZdC|JLhqoaBQBAvR4IJs;H-icor25@ z)FRl_!eykIp&AfFi8=|T0L_6mW$f6YLjJ{pr4$u8PSpR!4}67mZ5qbIkCX{HOh1w% zlDRsA<+>FsheQ7qJKy2A(?%x0PA$@G@olFZ@ExY(v*EuTXRM0E|8_XzloS75a;g~o zkHzU!QTQK?6J-kjBXFAP_;jE^0wh2JBtQZrKmsH{0wnM|AYe28=9FaQq23|2jv8;8 zbTuR(eFaiAH>>B`3Kd^ND!(|a`oFGSquK=5ftDrq*PNUHjB zoTmTptRmJjCRUm>H2xz25+DH*AOR8}0TLhq5+DH*IPeJAj89`lt620*2Ud1l)c?`n zjt)#Q)>araVCaDZ{v_GNzrWIM_AyJ7f$sAU3o#f#_ezH&;J_AxF!V}$yY`z~M8Ki$ zMECb&TjL708`{F*8w}vL*m)a6sQ&l_v@z7w-jcPK~t zy1RorvZH(*)yZg4u)1bbLbOguM|Le*DWo04+l{=F-e(7x$9R#SsJ{a<4ZXI_x{-Ug zXbWkXGT=s-cARXz@RwyuBtQZrKmsH{0wh2JBtQZr@Vg^mGoH}($wUm*m#U;$A-3xA z{^S-!VpMsguEWcqevBwqhHg0Udl6&Hd!XFvxBzV-D=!3A{~?>ze}66C8m;cF{!0Ua z>BC>k_g4R{pn@!<KiG#8BMOR9@2^0cKqZEgM`1vDEm{a<^nnxq z1MT9_MsS=ST5q{FO#;Uz^wn;rqvl_#YyUEQy%>_GnZMRu1%}nl7nTgaulKf3)M0pg zIa)1tJv`lpywu}Bkm@x$?cxWGH#-?EVTT`UwFti-Ok~)J9|!!z>F}+1tVScB73=Y$nL3jI36KB@ zkN^pg011%5?hw#j6uaYaCvKW{Vn?gJB_Q2=I3m5@=C5D-zfA)9wUN^OACQ3V*WBB5 z?_D4c;%vgH1#OiozYLr@2Dp*$ zCY-tpSbpF%EE65J&j6OGIB{70#pW;m|5XGU&w=yCyzI*C?A+Y^B}+=PXXY=FEQ#;p zx5~+0;x#l(!vatFLetfurolo_UiGnFH=RtQ^8917E9ZEMmdu`0YPz*q7Nwr@O4F!= zX|yD_d`_v?sH4|t`4WVnHR)`cRA%Q`Q7ADn8ViQlGxJij^Kxyk=OIg+a_69O&02?_I?<$4yD9-F@` z*K=(3!Xi`k$QHJh%S-cz*t07uvvX%J%rC0c0mrxEEH6KI&cbXnfv#5E%&eXx>u#uc zXDbfoN99{!CB#9B&SF z4eiRzpl+cpD?Of^dHK2C($>9&Bhz+Ho*BqMzkQ%Lhu?Tnb-quZaKCZO;vW%cxx`al zj_XaQwU=q3%l8;<;uaHIqZHM=9BpDZO_o$IT$o+Hd`?k;M;jkv8dopLFVD*_m;vn{cWM*XLf?fD$}`Ol2m+0!7V(FOfVx9`dl8W1@rR0lxg7BwL_p(Ad`A(m zcR+k65il7fzOx9J2!O@{2xtw8?;!$~NsRB=*kp#|5JAET6G6fW7eT^_5CL;B;v+@q z2%((_ogqYt&=o?o2;CvXh|m*4tO&gzI7R3K!6gEArihOdVE}}95r#lW5FrIZR}n@+ z=qAEg2;D_UgWwh+6GAT$vLN&pVJ3urBFu+i)&n8vrU&7pn?fXAw#IXn9PxIapnvlp z36KB@kN^pg011!)36KB@98?4hqr1ye*zO(wZ~f*s36KB@kN^pg011!)36KB@kN^q% z!3m(7vyG0{I^Tay@U}e<#9<^wp_4P@v8!(X)ZG{wu;*?maSR$G0{Z_4 zjTHgqWzaZFINTDBu!Qj0FleeJrPz4e6I06y+h+|2q9=sqzU&}-fF~A^*L57%! z(9seGh=5@cgKU=2$r1*NfPoT&?3U2k5(bIT6;^uWg$P|NlffcjFvTD}7DEI*EJB1K zV$~CZ9-SeA9w;HgP_e>*i$Qw4h6sA-gb0aZ)dzwe!68Bq%Vd}c7?d$ckLeIW4+#phX{HIh6qVwg@GG`Mq0usOBgKzhH@lp8Y1Yli=b1A!5zsuT_WgIh@f@G zaF1lIp$J+Z5p?Wn5cFsf5%g#g5e|{KG9k3LghMSs7XSiK>{Q|0wh2JBtQZrKmsH{0wh2JB=9>X z!2bW=app2q+ajnv4#Z(> zj>UB4)(>-V047%RI2Z~1+C9R?s}??XzCGCZwzwt_MzYkI1W14cNPq-LfCNZ@1W14c z4lV)?5`;``_Q>iHIfoOr8j?~p5pOzt~ zinY9M&=IQTXPiIaG+)45zu&4;Ik@+wx+uURD6^jkSE)v^m@73-e}<{R&rmbiRAf9h zJ8Z(1ajrHY{u`XU@T#-wnSH-mO3LP=5Z?nDp@^SVK^RLjo{f10o{ zXA@#|+12)BGfmbvOnui_8ZHU*qNvmNnlkSNl8U)>!Ji5Kz{7VMxOoGU>#hQ50xt$j z_FfcnH^elzo#>g1lI211ms_iFPQo1`_%qC}7pBGSB(MS`wN{nmTB_fjz;LPVBrr){ zjV|}Q6&Rl8I|(dH2UMxG8md};2K$p}ARq+hif%fupP8@Lx56SJ)v^*xf{b0>ioAOkA(6R#GJrYQ)t6#A|kbhT2ck%J{#DNyoh9*s8wx%FXh zQSSxNr?aW`+@9ul-~|v^8Vis~@^CH0-K7MP?cxf9bdnNFgXjA&_^K3cOH+`uL*iwO3IciIOMpCA&xu_O;y$XjAaoAfHU+;xh01 z)-G}hd+fGBga!IaD$~Vg7ZG;ds|a(DvMLn#U103ISCJHWUt8-#yg$ktrLRa}59Gao zHxntT1GqAL%S|X9S6e-#qeFgtktVc=)PEtXRxV_@tSj_QN}>#Pr^xiDO6FA?`u*Mt z*!qTrF9P%FB`>1oI#22VO`aTe>VhGmA_#ccJ9aCAeCR88YV)n>f+5Xso#wBAJ2W0f zyPZx%|5c3xN`CA zXs1$9qtVAMUYq5W8Sd~VV+Ct{!?bB<8P)P7{|LOHOHs2R>?B~T8d8M2r#w_8=SQIG z68uT=2PnnL)U+|&dl(Dkw1aEOIN`xgtj2`Ci<6K!GmEWGhp7WN>E zNXx<&WPyNGdz4fEB`Y-^dFm!;Q~&c>9w_~T%&cd2V9^wJy|ue4{W2@>cgyrM&yQT( zbU;P30_OF2wp;aeF+U0K%1H2z9jdiI8xq@IgHY-W{Vjfw4$b@o=04PeUmOm(#h4{SE@U}F*@>|}{Qh)F7>Gx;2k$?U2RkR$&FeBWwp2I4R}I;|f0-~{R3-e5!iKBx#ZHt7KxM;%m3Q&|!q z0TLhq5+DH*AOR8}0TLjAHYdRPf14vcpaPWof2LBb{~u7fLS;yR1W14cNPq-LfCNZ@ z1V~`t6JY&+-!tErF-rYEi}n9~iJWnf011!)36KB@kN^pg011%50ZG7SoG%LrVx3)c zl{ZVtz=iz8Y^oRp7@IMKV011!)36KB@ zkN^pg011!)3G6QdhT+lme~I6w?sYBq{n}p|FgCzf{CJB%L36KB@kN^pg z011!)36Q`ag8=*g{}}nqRFVJ*kN^pg011!)36KB@kN^q%u?g&H|9=nk|Kkj2|NkGm z{4n(-KmsH{0wh2JBtQZrKmsH{0tW{H_WvIo`9v*AfCNZ@1W14cNPq-LfCNZ@1V~_S z2<&P9zZ?DkI3q&$|3`BEzv4jxBtQZrKmsH{0wh2JBtQZrKmrF20rvkNIC()$NPq-L zfCNZ@1W14cNPq-LfCNb3w<55o{r|nu|BurZy8l0p{r|sJhA~_cAOR8}0TLhq5+DH* zAOR8}0TS2)0_^|a18DR@0wh2JBtQZrKmsH{0wh2JBtQZO8G&EX|8FS6pSZs0|Hm2U z@BdGLrOoT09sB?Ns3?&D36KB@kN^pg011!)36KB@kidQ?!2bXJo&{8Z1W14cNPq-L zfCNZ@1W14cNPq->a{_zX|KA_||2U)k{r?V!-PixW-H1}NRH3R+%T$TlvEw&S1|uK= z5+DH*AOR8}0TLhq5+DH*AORBC2?6&1?*tC5NPq-LfCNZ@1W14cNPq-LfCNb3cS68s zTHhyX%Y{QM!r;fWuxpD*{MI7| z%%ubZeQY-(#fKxvhf3&s@k&^%k zkN^pg011!)36KB@kN^pg0131K0mHEA-g^mZn)=-Js_PNgZLTX_XS5q=?~Y23dZOKoc5g(MMt&Lb zdPGUY^oWTOLnAszghy-(e>?oC@O#2HhF=hVVt7>e|H7KWD#PZ6z2|t=vDtCG<3fkq z5$pKT{vUgd{TO?;eYE{P+l{u1ZKvAeY`++v7%v$Mja*}#F~Il-babc)rJ8o^aH$Ko z9X{iwRilnR>g=r>hpV{TZ@6aU`74Iz_Kd1{=+6J#oSb{wj=R@8KYA+hiUkj5{G53A z$@xhu=9K(=ZDiTy>tFA6)V2<94tr$N>073p((mL?K0fh*t-Ti8N{))Y>x;K@N5-Ep z_o}W>Cyse%mfKc?oeT`+@<)5Ypcu;t;}x_f7YuD-w63MAeXOrfIlm@hE%mjG0`#?v z_OZTJv;llEcqEvwWfY*VWwej=wW1Bs7o%W;^|g!w^tFukvA$Nc0s3OMj$nN)qX2y^ zqkXKe6>WgN7?Wh`>n+j|$b*(fLtrNN2g@T|Sw{OZ2?5bsFkysTa0M!d8;fPMFO@^I zfa%(?-H$22Xdi1b02$UEXF!JaYiZP{rBSbzMm<{^b#H0ZwWU$#mPQ?08p$3vL8_vh z1{>j47TPF$pFXru+~fiY($_TF=bE6Ds~PBF*q3Ds0L-$r&!aWV?>>*#yhiqUwB~iV z&!aW3^?e?#c|Y0b(VF+HeIBiOU)<->n)lv)9<6zQ-{;ZFGh{#991&}rbKl>S&1joF z?=4LN-ju+^C0)`1M)Ia)v^3fdhXGYE>&3?M=zXyXawX4bg(2LQEAPXyWsGpuObL0S z1R2TQKFCP!#6d=KmkctJJ6MpB++Bi<6MpDw685xGtl`|_X zM~vM{J=^><-!=c4U-OYr_Uy==o}z9;1~;i5Dostr!22oCCtt50Sc?JoZk4O5l?TJ_ z2jWs$gl`$1?}2$1%xm$>tui20VZeSZzAG@me} z>hLQ?CE+s~QZZb}fPa}dP_ByfGy*X##3}I(L_9LJpb+6noR+y;$1WxnkR;v?NJoiU zjh^`2iLi^oUHA&k8Sqtv^h|(GLa`opwOX@j_^#GdA!=bN zc_h;%L`#`qA-Y?6BieOQvtd?*ScSRhT;+>>7^n%`VzmVE)gp!}$l^=#c&gS$rgW5m ztp~m{RUYD%{4rB3vASVfrFE6uG0Thmmi!dW@-aojttP>LxmU}+T9;C|S%z?>Ow5H_ z$$dB6nmJdY`UjLZHzsaT@PtuTv8iX$Th|c{rKgq9p?j72yr7oic2Z5(fkWr!vH4 zl-H%$MvJW&#(2Gm92+1zA$t(6TNzO<)ybPhQn)2U{kIn+!X5{=XTxn2s!3oci@g z9;dm`q6Q($>w^CgJxj;30<%Jo#3OPFY{%e~M`{foON9v4av=}iD*afK2Wkl(&ei%6 zS`RaM;Fci%88DOCq4KggT|WjpHPcu^$boqwwC)l@Tg$E!9`!<|M8EK@&|#RbDslQz zYrfXV1H2xw%EMPor`B3^x-b#AqHA>t`80ITw$u4|$zze1Dkk{BM{qp32-$f`-f#&}5*QuGg znVUkt%3A{KkS@{P|I#UCTWD8m4V~~W<<)v6m9k}C8R7lK+YPsM+~Iibih9yHQ``j~cn`u{?%)I7Z4vNp>x5fOB;IHL z+bQhH$0}c&yuROk=a2iS@EdV}$m9hLFIsqyl0zcZFK8m*#muyow=+55GF;xylzKE> zWS3ud$eWlX1Llr+zZ{<6cN2bymj9V9fy*sVJ3bVKxW{R^gLa_Ru=SSM=fT2|w>wzK z{RE-gJ3`)pL*!17B~}Tsvq%tZ-9->RdU#z5Q@3T<3x;yY%{2+%O$grqmLG#+AaB@G zhm)J46iBnaS)~hu_%z?A$LY7u6u4M~0wEWd)GDQZBo|^j{MBf`QlqU#Aux-T6o|p_ zX{g@XrxaVMafoZVh@>u>jbHK}-=J*9tXPS016l@TI=wU_NE?JCIq;YofsXeGjZsjl zp>Tpz4ztOkbvbBazu5#K6Z^GAm@Zj*{=OLmEcD_5<}a}@X-Ed?{ErQzHK%~>OhweGu3vKp15QmW;E7M~` z9X3Z~oIAA%LmPsHP_AiqAQEV7(*5#DN(6M?CmAAO2uG4l1PoM3vWtM>JxLA`(9fR~ zCISW>B!!EB5}p(x0!F?hMT&sYGD+=3h=vd)LJWjx5ipn}DMkde9wo(!fLT0AP7$yL zM3PH{cnEPKV9<9`ya<@jlGI6rE)Y74&=o=#5xPO>DguU?B;f}S5+DH*AOR8}0TLhq z5+DH*Ab~w1;4ntUv{wI@njijB2?I8an8d0WlKu)b4^2C^EVn7dh)7|^?KB;3ou%; ze2Ezm6e&ksL+s@qPo>sTw^kU9rRAPd(MOx<_6no1JipXaUTFrd8!U{*CAsBuO1(z9 z#lmP@nqzv>?H0CBalun|W$tVpv~ClLY|NHa!>2Dw-6RnyfkBgb`MIWYy6Gaiag_F| z8$F^M^X8OmUl>l)6cH|SrOc*|n8uuAJvrK?ZWW1_H2Fg6Cb{D?s#~bt>Bf$DUkY^l zNJ8TfJG2;L5A?0uNfIP#?GH^SJV<~9NPq-LfCNZ@1W14cNMI)f9LAfi*8fvckH_K% zyZ+iq__W%;1kCz>Q@|jA<^urqFW)Et{mVBLK>zZM2hhKKg97v~-^c*{%Qrkg|MHCy z(7${G1@tf9*Z}>@H$Xtw@X?ZFj#1Df4SYid^e^8q0bT3%jXBW2e4`6=J=-@7!K^+b zOfBetz7YlbmmXFiloI5hZ~TG&)fN6&t=_){n6Z-p36KB@kN^pg011!)3A7mj!+0~o z>i_Squ5zWi-f(Vk7CZYoKZw04wmQ}w`$o+5F-v0x#(WUHCVG5ycy#y^*E~M$@z}>V zJ+}0*fm7a^eBI=d$^9p{nL-SR1W14cNPq-L;6NcTLll{Y<*4(pcJL&uj5|YSamlbh zHDbm<&-#l$Otrp;Vr}5Qn6}jkI}c(VSIi@uh^5Y3cwd2~a$5^Rw&EG6+&T~|h|L_iS$(;CVhQ?7n+9Z*`|DTYe5&4Y6sV_INs zjL@q!H_w$otX44AH#BV0Vd;MpT0lmC9bnZQ@}n8eb-EWiDX&gREr274LlMNTfCmSf zilL)=C2sglZvh{H$|jb(f^P*na+;|z`UjfeO&>BsPEI+dhKfLnf|;jMD8=c#(Pl7K zxeigfB6JBhy;+<8m@D8*fd~~y3O1S=tjkpwt7Ju>yvmGB*~XyOyY_UU%&gQU>Xq!R z|B@nMeItXKVC6d;ak;p<8nD85CKi5|W123yJc#wJAEcU{1QfFT%X;JKSbV%13L}qnGOd`c z@BX;@dLRJ_h}L@rMrg-LnoKCA-O7ziORoJ$zZb!1U8F@=Y+4pjZ<%fjZpd`Y((qOg zQUt9#fUI3DyIi#l&Pv1)>IgLgm+e4YCcZ?Vmkbyyag~WawL9@1;zk;w$5opJP8bAo zSUlIfiG?huvg&vNRJZmpXkF=$v7!^uyH}TNMX<9phx9JvVW-2JELSgEO#RB((LL$g zY@v0p1N9?Ao;q=U+Ok%?wN06oi2_Y-)>by1H{QI^gD)*xIUGscw#E4OKBSPBmIP&8 zgMsBkcH@%dnx$j`@Mi!*8lIdP*e*nT1?HZm%rrzYS*4=njPa*J9PUmO@_5ce1mZq0 za4Ut`czCC)ak&4bSb-ZxRy)0*KfI)<&t0#&9&t@`jdu-p-Riu;d6u(-)9(B#wjs7O zwjlQPn8#x7h?y8OG^TURhUjymS4D?Ke-ri1;QcmC(*E=ft~D&QDq~r{w2rBg-yd z|9Y>Zwsm-O*dv=x-!kQtekXtO@re&??X}oea#ZwPU%Z_=GGw&#q1?AP1C$e^{jiU? zL@Nsi1?;6Q+lU2`vy6T_Oz|hoy{hZeiDTZG<+jyW8D>Qrpj=jva@Zl*+%l$?D%H`$Pp$6wN`(dyBV5BhA@|+ zXJ|?9wrASlzkjzqonf+xr);ikrht3r`8ZJ^0TLhq5+DH*AOR8}0TLjAKRf}$=q_V} z^kMhz|7ZQ?Hwlmc36KB@kN^pg011!)36KB@kbtam-xf!z4rDLG+T%bRMwrtaI?ysL z*!HibAx#b{0JwB4Wz@MWm?38?W zL&1T$V*b(pJpxuX$-s<&dW`BX(R@?#M#5Hpx zp~PXGhvst%PLee`lz~C}t^L^me6*LeIZr_RU?axXJiy!zKV!Upn)J8ivXxDGMuE(^ z37A_TIj?iyKexc=PYd$TZ>OC~nAX$~o0v$Gk~d6qPC_P@b2$=o6U-H3TIV{;x6s8@ zuwM>Jg{lfU(3;{Q|0wh2JBtQZrKmsH{0)J=%hVi1F|1S_6-tqsR z`Aq)MXqb2sAOR8}0TLhq5+DH*AOR8}f!_fE-EFy->1f(Z|EBjg31yE1aTuFp&2N{DKqf|rfMrc5Mv8zfQ6{z%0Yg6~%Kj)kNPq-LfCNZ@1W14cNPq-LfCLV70<8ZZ z=sC1^`bjMvZCz&lUxX+z!B~B>AwUG(5FmnX2oOOx1c;y;0z}Xa0V3#z08Ae+8v;Z? zs{juYAOR8}0TLhq5+DH*AOR8}fdig^&8X7z|4&EdPG)h-4>^O!M|k)DlZoSSsnj?) z!6}FKZ;C!|dpQ5U)t&v!;6rQ@XxDSu1D6u;ZW3r{T>>q2Jw9$upgA{@M9HJ1xjRznym4Z05e? zZop21!6eM|&)ALG_PMG6t02^=*5>ff0M9^noS*N@9||Nu0wh2JBtQZrKmsH{0wnMo z5#ap)-v}&&B>@s30TLhq5+DH*AOR8}0TMVM2<+v4duAHiW_!HguJ%3Rv!{VLj2mKQ z0r*fy;QaqMcPJM$JJ1Pu$N#5V^Z!$=`Twcb{Qp#I{(q`9|3B55|DS5j|4+5%|EF5> z|5L5`|EYRHL5xzV(GnL1gQVgI4-y~&5+DH*AOR8}0TLhq5+H#Ci~#HZ2Uv!*u{z$_ z{z3JBOyMx={~}n^{>}Qom{`;P&HBHX==#41y8bVMuK$Z*Ehu2t|HXv$|2ED<21f!U zKmsH{0wh2JBtQZrKmu(*z%Yh7vG%(@Z0bdY36KB@kN^pg011!)36KB@ z{N4y)0DQcEt+-vS`c=2fmBU)D3%08;TlzRs)nawAWvWKWifp^`Uk6tn^{9GKZBQ4e zBIU$RXghvgvNBYms#hhd4AIuAa!9*Q=lsXe8_o`Gtb%E`CZ`p{-#6zCjk;50TLhq5+DH*AOR8} z0TMVk2t=T&XLj4C;ajSZ)~;Yu(==;|RyA^U1ZtK3%BXA+CXS_o8q%?B2Oe0Hbq8#x z=2pqRT5Vvgqg09-rUtA2s<*fD+8Ci^aXqVci_OjSZ+wN0H6p;V3}=_$a)eY-LbE~^ z$MRc_kdDQHj>#s9HikC$5QpTj*yPU zfh;yJjsw@u59wGONYAu5wyN_GeKp=@2^0r1j9naC)p>}a9gBk@IK{D5orf6Ou{cP; zDUPk`JjBqB#evmeAk};82$?{ufj~l*(>g*r76(>?fmO8^4ndaxMkQux)M8WmqcA_C zK?3GM0wh2JBtQZrKmsH{0wh2JBtQax90G>X-8bLguI_a$cP0CN|8XcVr6fQCBtQZr zKmsH{0wh2JBtQZrurmUIgBW(^?!dKk7;b0UfvX-hBLNa10TLhq5+DH*AOR8}0TLhq z5@-VgHsgD9Za?M=_=oA6gZ#bY^mQy#!!Y|H%s;DO5}e?aBhZ;8CrDThJz+wOtoa+> zxd3)Ol|j!1&><*=xd8q_{9<8EMexlYkYJko(9;TRTBYU|k(g8IMkQg6;s zg{2kCv9w|pbdqflBGpvbS7I_iq?&;V0X2|h>VZS;bm{?;oa0q_Er_Yzn66-lpIpn2 z{`RL7KmF4ce0Gvl4bpF?e`fpAwE#0?QjoS$md>>5h z!_WbXh23l69SdNtLKx^{yAdftVQE3AHv_I^_(6dls8ERk z2!7YrfCQ({^-MJ#0}uLQAVC2JE7ajv3I11O$H-b$tYyD$GK?X7r^6V+k#m#1=`&YQ z6hGK6vb7}_VIBcLQ@nmo)8Ae{$d1;$MZ!;Zvv>lFSg4^7l7V_$g@5RS_U|t#`02OP z#Vs&j`s%?QE5YrkpmiS$z^PE@Ol<(i487ZCjn21~@NpFUS7CTa35Jb86X5+DH*AOR8}0TLhq5+H$tk$_>;`S$i}=j;F9=qz$}cfK6^XP@1{m^SK6 z0wh2JBtQZrKmsH{0wh2J2NeOUA9Amh)?I3Q6#_g5_U-O%SOB&X1W14cNPq-LfCNZ@1W14cNZ>#v!217z znlo*q7wi9R!;zto011!)36KB@kN^pg011%5!A`(o*jw%YSIT{`C+`ohzpno`dI}2W zq`?!eug+hhqZ%@TG$QNPq-LfCNZ@1W14cNPq2XiVQK(UA`|>GaZ0bRwO)M}m4M$pW&FQb zg*rI@KNOXN%gj6ef7-#NHMJ!H5+DH*AOR8}0TLhq5+DH*Xlnwj|F<>K11mh%{|~JE zpf)5x0wh2JBtQZrKmsH{0wl1%2pGnj_MBO1ITF92zOe5vJs2+ukic(EKn4IEhdn)4 z9(&e>B{ME~tHZ;mesON6qRjNgo`LbFv^Vm*Ot~f_EB&5B&s{(D*oKS`c^bHsae_ajE{;MY@eyA z>aFPirC!!_u8|t!-u%_2>PcJfBR`yc@98e}?1D@CXAgV&@}#o7=wHfiN^%YFwP3&_ zcTTa-jf|iBLB_hjUb$q=;<2}{nB0)mYxCEe!?ySQDdp}HAN}~%gp;1nI4ghkL(`pC z^zmeUmXufe(B`u)yV^MKyn+q0Hg6nw-{!D;s>2ujY4I~Nx@3jjn)1Zl`^!^K7;)#F z-(>VzGvlgT>fbrx)0^(-a(nI9Yrptx?WC&)od3Q3-8VO--R1tI*I}29{qV-?`aX8( z)&G8>OImU4Ykltiza@`!e*KBH-#ztB!rbbu7fjChbVC1gijO+y%U&m*p8iYjs-My) z+46tL$U5f7M~05<`eD-23s!z?d!)ytq1`u}lJa=b%j-{?RI@pw|6x&wA71|c%j0@3 z==$Yhg%hsZ+VJ$kyARuvHgQy^MF}gPJ8xUvnxlSMled1=4=dm4JFLr5OP9{Q>tEf* z|LNotJ}+$dFK1-@`p)m9Qny9Cy7a`-w0XC_y7|HP z6R$3uboi+ywPiDtANlBl!@Aw~_u8TdZ#n`ey?x#QA@@(}T z_iy;lxPIF5$?YAp%iB-6=Bl-qK3BE%#_6}E^(sGd>jh=ibLt}2_q}^g&dp`--#m26 zqBB3YFWY#<`kC#nd34*jKRvqe^5taM_({^+P7J-3Yd z_gC+%OgQy_8&93EdVP4z=9=fWobujF>XrL1UHHI`o0rTTJMWbTUT@5e{NRfIW7nMY zU*|aYTLYGc|9IEQ3tqWr`4!iHpsu*!U)MkTQ|Y(gefGku*#p1b*85+XcZ^#X-}lr% z4em0kZ~Gs%Uo!D($BZ9Gue;%mxFhQidvDys7j~Z0^T;jNz51Uk52;z*J9*PBg_jrK zlD2S7T|`Rtn-6~O?pU|uorhoUwQ^3wQB4@56zo({cXilFdS}}Qr~ZUcT(fJ zvqttfJ!0fBZ!xlV0ZhGatPrX~V_UZ}K@^c@%=ffMzHV#>S*|}?K z-dP&wF1`F8YyXj#wBeEqUq1Zh5gQ-8an_X`z8o-i%-so*FP=B2-L)Uzd_wOL6Sx24 z@cXa0_x1aa|9aJTS=(*Tl}#D))0~UDZ}{(N$u-N*AMnZB%hDa2Rz8xr@`7vBzdvu^ z>81L!V{RWa@1Kde1FoN$aZAMd+4bi~%$@jr#~+>7Zra{E?b)+76+b%gEc=(gA26!yh0qrxvA9nq=r#&^rlJ?@h^i(c<>?Dv2F=M5J`uFAS*(}2Xk zf4Z&gn(MCF7=7|zCfvXL*7#>GU-(Ax4gXqRKOz2?Ze!gq?>PVI2?ORIy{Jp)+-X16 zeqmHSwfW;dvu3^Y-jp-1zi#tn&x@H)O&*!r^WOBoH&onwWyD?UUc09M(^r@Mbln=y zXK5o@%uWp%LHsZ|QxBRf_>$6X|a@F8(FJJYi zGn|R*ZW)lU$ys>jKRV2a8+E=rruPdU%zEX_S2uReN^ZaEy5iHmi~cCP?7Enmwg3M7 z=JU_|Y{SgImu`OTjr<{l-q?EKMK5;AydyIEzZY&E|H8(3=iIxa)b{wzx4zx@OeU=T4k=cEf_zvpy?n|L-TN*T?s6ykk_&+5bxIXghrBmH|g!df0=j z-x+^v zuHuHpeHUiFGQDBc;j1=$Jv+1E>f~N0$4tKJh5>!&pVjBpKc6?h@66<>?LS}L&E9j( zq8?Gv%MZ=I_}s*Ex=mS}w*J0zK8>hapLA^3MH|{5weqx&&p-afmCsjRbjK^p79IPy zipraxKQZRdKc?-t@74Hz^B#R>QOCbWoHsuFv2UktS<<8b>Qh(WH0#M*-W~YDxNc*w z`T4O4b4F~7zy9+4i!b~v@{Xq_9rxl#A1(d&?aS((fBB3zisqJFoIL%W!_sam{OYtj z&Y$+ww^@fZp0IS|@}jFd|L>Q?BTp?Udj5)pYtOu7Snty=?QqGa0V6+*%z5C@s^!8SDm-v+u?UCc=e)&7yh#8%d@989{1eDCmTjTwsF9j?@pSy{;VY% z|9r}Vs`rK*a^+(${r5HZoC&92l)v`y8y-0Hq>oNKE#|6YI=`3vx0?q4lHB*Q)HD9~ z)CVtr|9{R|??iaU$CQQ*9UuMH?XL{Hr2FS(KX1*eA9&V_7tQR_|03-e}CY{$4{-g=+LPz=2h;nz18jXf3#1Vd-{}ZPdxC*5g(oN zz!#UE|Jks&K6&Y?^=BV*&oTG7?k+oR%7jacZ+NrI>+@bXV&TKitL^Wl9{t^ePj^pS zbKMUY6mNay{o9JW zHs@Zy{ky5N)=w|_zrVbB`t-|Qd*S;}R!1Jc^pz)1{OHlz1>Kk4H^0}ig|*k+xarj1 zSsx^RKkB$`18wz^~9IsCPiv*wRKJZF_%azfY%}`NlJ^of&q{)z==|_(;}!Zyz_~g)_Ea zQTm@()@NSz-=gO)`1+%)J}194fB1liisaroBMNR^_!s-g-13{QzUab^_rW$`)>Kuv$IYcQuN`RXWx2s>?1dPbJO?Vbbj@PalgF&;$iE@Kh@!sPYa4( z|Ki#+{(HlbWzMyiq+I)_2S-PbnfvsOq3*-CtY?wW~ z-^R6DZyNn-=hud8EdTKzFOL694_E2%IoEv>v3cFwtLJ>%_o>l$KJ>`He)xFE-$yT5 zd1BVI0S|q=YOAqm)7R^s8}q-YsU5y~{HHtY1rNlXuNq7DE_}9MP_T2oB zx-Bm1c+w$ZW!Igw?wEVW_dRq%^4ni`A8_6qUG6yj^mD(;e>384&$165zW(+noLArV z)!0!dbO_Iz^v2;M*EK!=x3gdQ@r2xqlP}6mdMa_%%@5r(q36}t4O{#|_Du_adUJiz z8y)96m%YF0sAsc}yDIjxKh2uaZ9~PjcUEu&A_-A*&bW-B#iI+Wo>ictIqe>nwdGMyBG^*s0+oANzOB$s~w-)20i}inS;+<_Voo9FS+)+BZs%Y_To8r z)ZM;iM}0y6>6t$c=z3+}%Py;36nn$$6B}M>|5S(93pb4$KV<5So+C;RnVo&b>$P1! z-LdtYwLf3~zd_Dp>^W~-@YABW>t~KVJ9(9}@r&ey|0VqLVfBcnOTT@|_Rr^5zB2Xf zl^;J>aAn6uUqwu~BE8=~t2WhK5q;d-_dXc0qwhUWoVM+-2cGZyWkO-(;W1zLTDG+F zl52C{z3iNBf9p0Na^9gE&p-D+FWz5OFf(oP$-V#n>hRC!{k!hGZ_lY7cG|1!ZaqJ{ zc={W+jJs^Y!uP)X$aedNQcuO=p&9QUbH!_C-ZABqpD*6@`aM6VZ|MBR3#%6YUv1Wg z@6I{t`o7VJ#LT`s>Wo8woSQYnx!LySXQl7`u%hnHInV!?tB(G656@*Uz0-HjRTU?{ za$;%OxTqe(-^u(rZ2U<_y>#mnBO`yxz4(%eiMH*()E}`b?Y_n%w{Q8xHBnt?A2$4} z>a}nGa`I`1E!gzr1zR3kvtvuUZMFX{x$mbNADh^3?e;fo$mhX7(+%5Lge`^1wVf@);?f++}t2qDvx2}RQd=el55+DH*AOR8}0TLhq z5+DH*2u&cenQ=mlN<|-=KJrv4KIN)dm0+p<8EO%}D{&4}^^hyoO5eA1{K=W4CSwJF zOm&3nqMRyRZ8eOWUDo`6L*38%ze$$=Nq_`MfCNZ@1W14cNPq-LfCNb3z#-AE% zZ>=>TxGf+>4OYEX6l(tt)zdI;bPe&Q$WWWlIj|B(ZAgFwNPq-LfCNZ@1W14cNPq-L zfCN;8>Iw=Ti~y*`YW_;eg8d_A4D_tO_`_7|`(8t(+UJ?Dl2l*SO|@5XDh9QG8~isK z#w7vu{~K8UH%ak736KB@kN^pg011!)36KB@kN^qnF9H!5v#-Nh(Q0n3aa_9z)b&mn1<^8hgp-~_XL%s24bavZmN^N{(ntC z{r`5>|NXQmkpKyh011!)36KB@kN^pg011%5{v{BhxCgfA{1tz^}vEPQ&j;*X}MLtT8430wHS}(R=KJgz7_L2oTYH%Ml8j8Gu%3uxm60L zyCtd7kc#1=663!L;i_B}AtVW*7{814rnv(VkC+uA9*NU3cf((mHmT6@l&B6!M~PaF zv$@MI&ElS=?duSi>0idBEysYpLWCwUOAPgp%3v=fCV1qPvwhqFom?8)ij_wFIGw&Q-qHhar`+A#t%5|H=6cP17yszI0*Gl|s=+bXTAnPXC3-uhtCAt&}AXgq0I~gvVkmx+FQ?1KTGT!mYX0 zQUzj_{LI039pb9PxW_c4p$4IgKB9Af%}?^E9(hy~kR$0}C;mMMe+8}p;oQ=lX)5_s z=gsSGK`wIDhqbv!*4qipp}x0Ae=(k^92u0_AQ0#J}rQ4aeGbrj?VxNyRSkej0Owh9AP z{XCZWxV21+nd<)D^4~lRv!rH0(@NyAl&!#&Wg>ndUZ+cG3FHFAZEm4m2tpZ1x#X&p zo$1iF2p=hZGTyXXvl1`)D4T3qA$Id#75)@R&d-9Y668vUfRu^3bv^a==9k<9W@$EZ zpA>ddzAAMbLdCjIGOvfQtV<}w8ozuK_mcUNRh#TpZE5WT(TBF^_oxrs7cylU8 z$07GGu~`OJl0*JGl6fV|y`ffz75YwUmAIpQ8k+f5qOV%H>QfNr-sss0B|+{8a*s9d zTh=|OoA>VSznjZ*N1kAkUtL<*O3Csdy^FAAo0*zGpXR;PdWM)!4$;3y3%6F;ljlW4 z2>Svrant@YL*5?DA=@#^z=&*z9D>LIUWPcvvjpu`6X|dhXt4?L599i z@tlD9|9|EDf5`wIBtQZrKmsH{0wh2JBtQZrKmsJNZwW-G&WKRDHKZx9&b#)%)zBCT zJ6ZM5TJwMLTyyPzv~e`SSRcBQ@0F*vT>qc_|6VryA^{R00TO640_^{{`m%S`cfFl6{r~0|0rvmP2p<`n!2W;s|Fi#JUgBgN!LIuMS^sad*Ifog z0wh2JB;X~$`akRc9RDA>UZ07PxE%k_@&6qE&+-2p|1Tr_IsSkDjQ@YcVa@-?jE`sK zGs$^K_&erQ1is6X011%5!9-w6=J?*nr=^TdOG?QYnb`bSa?+@*#1SL1QWDd~Od6Xw zYE0IsQAs1yho_{ET-`f1YHDF+$@tzGi^{7?>PlwR7JAaFYpN@&ORIBA$_k4c;9_z~ zMNLL^Rej0IdI>oRMzcz4>&mOEGO8@DoTc@jPE^s?C_M7l%ynakdb@j_}=O1 z>0?JH4EnBk9yNMIdd8TM85vpWV@73Vm?vzH%s^<#!-r?3XJwe5 zq~S1*jhZ*Px~|?mX=Qy$m8YcEJ*BF&`k2_L{*w`@9LeL4A3kPyO6I5$Nr@R*W0Mk- zlQXgs$4nYAJTWOLEh%Gca&l7A$h6gm&q|+kxX)KoY}Av57t5Y5JKA9wSvx!i-pAxV z>yYVxHsv-3tNGY`<#;{8K1olsAAxDuSt`+MlGIGf@S6H2HJ#L7(luYWy-Qlvz+f8 zMQ44FM!d;h(@9t^U>K%v=HSyuPuZ=*BvYBFY)-Y8N$$luK64tYIpJ6)vzk-e%?f{= zg-d^T^U)fCk$hSBWFU=cNPi+GMP@-B3n>}DlTj&5)NX{PNh?jeOj5()GD)AKH7%J^ znFJl>+%>IGOaf;}&IcQr+FcJ0f#%7`oB5a;UH~mJ@EHc%9BA1GW|HQeEx#asMMpRM z6k!UzGzG{MV42Qbr1MmkJ_tNEVVY~pay1-b_)AWpw0q?$r3IUgfv*wo-0u{tR)KqQ!>9 z4_a7PNT^Z}Sbp5vw^<5g6st9k)eXAl^m|!Kz+BKn7AGh}*)W&L*xfQC6P4pI2sXiH zsP;HJK<f6fmr^n&isH^u>SCssDA;M9aX?d5giBq^L z4C4rBV_qLUY&Miww+)Z{9pRW1zGH`Tx5_YP&+94V_uGr3s+9FwrL6a=%!V-B;8dzDD*Sd^)OcG|gza3TePnoot(`5w zyk@gwY}rTH!Z0C!Y+b5O(F9v(JZ8Oj=~UdNWATXZcdVi8gXbT4Jl=oqOGz3rJSkB{ zIvman6=ic82}wyK@T$=#Wmr<5Bs?|8%?Ei8R!XUxU(irjUlLb2JY~MUBxM-x_C+Wabxrf}>R4G_ zg_RL<%ZqEP>#DJsk-K0~VQmRsR`Rm4resV?c8{8$mNBU*ZCGYxPEMbQguK#HJbz2v z$)koP;c=vklpFWd`of~Zx)OI}NmW_Xv+leiJa&ug-8tn&wMQ4$HmIYO;ac-PZr_Kj zv7em~XN$HO)Af4)*CO*|pS_OwmLvEkH&r(I^S=kGW8eg5&^CW4H~QGgHh-<|<`=w& zpMTFj?$qbS$}bWi0TLhq5+DH*AOR8}0TLhq64*lmhT-w;_h-}l|LtKw^hN?CKmsH{ z0wh2JBtQZrKmsH{0wk~t0;#*O-!1Ds3dArLM9udG-%)LK{lj&#YpUx|*Q3t0&K#%R zc~9)>*m1GH#M~CMI%Y)7FVS~IuZu2?wnx7lb%~7s;6VZ;KmsH{0wh2JBtQafMZk`E z7)sAQkqrWyFQFMYJ3uz%tJ4EkWu`_r##q}`gdMxPHTNt#qvc|(U_x=gb=Ki zxn=n5(b!PV7e!`(Ho^N%n#%XZv=X25G&nam>695L$y((O*ig;qU~E8m&CwjGC*{bV ziLxVOASg4QBcQG?n31~?jOlGCHc#| zuL!6>&(JB;GoxAr)O9xk%7G>_(WganWTktVX(m^DYnkd_22{L{z%}PXDI?c+<$We% zw)Ux&serN@qD=dez|v7Ne1Ty)Pb-0mW)(nLse=-a!!T>j=P@k6qg>||$aiakSRG1Y zIZ9`U3rglrSjt-)B>OL!W;ZJtgl{=DCuN!8b-~V}J~d<@zU8RpEg&GgNaIf3|NlMy z%lLomkc0&uQv-iMUWoDkI5{*whzoy`011!)36KB@kN^pg011!)36Q{nNWd_@2pIpb z*Z(^Z)uc`&KmsH{0wh2JBtQZrKmsH{0wh2JdqH4N`~Qp3|BsXX|9ionUP*uiNPq-L zfCNZ@1W14cNPq-LU|$hn|Np)s&3H(F1W14cNPq-LfCNZ@1W14cNMQdFFpNI!a%QFF zhz3|R;MtMw_Ma|{`S(m9cY2Dt3+r4oVdb4!dNq`C?T~{=I4lb*C)GDWT<tR-|HA>KH z@yKd474SbD_A6AUW*SQfvNB2`m~;uDt!39qO-7iKvZ**%=rClJ5s!+)3MR7J%@V|A z#-89aF?ADP-C(;2-;(Yu#3x!#!GEz^iIsuUdO7tmvv&Sbz`J#izen~<4*XORje#qDO-|v^_p%uR@SLP2};9v8T^#sznPEL z%1G_88qiAk7rQ!q%&V`xrY@V^%Qa-V6@SO&8=9q&@eY>w7f$&zSxo{y{%;(@+ z)(jG^-Mr<%x(bD+lwxsLfvb87Vx5GuTJ!GV{av6#kn$qwkd?qp`@Xogs}Ogw_qT*r z*@8|Ny&R6YJWe}h*le<55T2AqmL8!Q2t=m%|hMZo&`I@;}ohY3;?eI6f4HvgnY=9kc_jhOM{6J`Wa#lBIxU z|K3b-WbX)Bw(u6Y6J&{1LhLM(q^i3J5?l|jOJVA^412*)j*D>`C{E&3Ztmu7E;(cr z;yxe>!{}$mWf=V-fSzF=fcKk9I5KPly>_-i5bQ8928*NJ*Nl%cKFV5uQ1Vg6N0~e@ z`6%O~?0|oiwKKkQS@nNBl%JIcDGw4L0TLhq64)C8Qcb!oLZQV^>hx358a73_m9@NW z17xcfJ}{t-#RGGzy~C~BO8m8ZX~mg|7KIY+S8DB2>#xIEiWUGjVky?Odui8j>m|99 zur#+@*Y2fOZ?+dotA&J6jNh_4bP1NW&d_Fs2v6d)%-smNN}EV~gv8rHx4SKe)ZC@C zxcK9qrR}Y@6}N6_T#go#LWCwUH(wjO0V@wnr9oOeEVt5*V`^(^;P)#n8KS3X?$)hy zwW__BN-k>P(*9w#ZMc1*%IeR>Xk)R~!JdH-ijbZO&`BuP!%kjV-S{m;Wd-e8m`WZs zUy9qGH=UCkVmu;sV3)`({LXc=;&wa0^0mVRyRpV0$chZU^Iai_j2b4Fr zx8+OL#}=wWK~_{3R!%RE#c1mlB0bRhDlJ-hC~u_*$81~7!FL_vuR|kF8qy#uuZupS zbAQcG@~a+sR1=UR>0l@RJCQBj{HgQib+;gwxrn6-_R>yUtIRf5v-Qj@ zQxdb}y|l)eF6+>SCe>HdRTk_-_S+5&uuX@p*{&|FdeX`#t!za)t~#X7A5L>S$$Kg7 zWr$O*MZdcOP%zg-FGNU3L6!x-op2%Krs%w_!gB9^9+MaW+yl)15osS4c2d47bsR#)x=%EBBPGcV-K>^;seX>w zh3lz0J&8fS2JgbxbWNxlDK0^~ZkjGxay42lr*d`HgP3)nPKV}YIu5x*mm*HNbDG!A z64*%@aqF@ocYaA}jdmk<0@Ka|Te&u*?OAfbv{?pOa>##2GOuL0H%ME1Yj;ws#2xL^ z(9Ab!=a<+^@Z8utJ*BlTe}rE_5j)8mCIv$Z`Q&B9rtfN_Y#D}uCqYoUWBbhHGt z)tgq*maW}%u>^0UwqZ!4wl`6RG-{*$-f;AR;YVmi7rC$3fyn&=$fc1aY z|5^WM{h#CiIsRW}CUE>e$NzKuKga(cFfXW7gak-{1W4dFA;9`S>;J6(v;NQeKkNUj z|I0cOtpBtA&-(vw^0LW5Nq_`MfCLU00<8bD{?Gb9>;J6(v;NQeKkNTo|L?$C|IaXT z!u{+2&o>u+JYe1+sR#*>011%5!AOAhf7bt5|7ZQ5^?%m?S^sDKpY{I(ul~>d{|?4C zYwAn_BtQZOHUZZES^sDKpY?y%|5^WM{h#%J*8dN@`oCd3;!5$pMjGm+a}Mk`N@_;} wBtQZrKmsH{0wh2JBtQZrKmsHXKp;ZdyuYIGZOI)}f^y;$rNZ&CsR&j1|J@vzIRF3v literal 0 HcmV?d00001 diff --git a/test/LibRed.Core.Tests/Data/ChineseTradStrokeCount.accdb b/test/LibRed.Core.Tests/Data/ChineseTradStrokeCount.accdb new file mode 100644 index 0000000000000000000000000000000000000000..419991ef956f60bf6acb622b8a6706fdc3bc1183 GIT binary patch literal 446464 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 literal 0 HcmV?d00001 diff --git a/test/LibRed.Core.Tests/Data/ChineseTradStrokeCountLegacy.accdb b/test/LibRed.Core.Tests/Data/ChineseTradStrokeCountLegacy.accdb new file mode 100644 index 0000000000000000000000000000000000000000..db2799a621a1125e6b6ed0b1fe6cec486211a9db GIT binary patch literal 446464 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

qVhj;SJD)N>%J?Xg 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 literal 0 HcmV?d00001 diff --git a/test/LibRed.Core.Tests/Data/JapaneseUnicodeLegacy.accdb b/test/LibRed.Core.Tests/Data/JapaneseUnicodeLegacy.accdb new file mode 100644 index 0000000000000000000000000000000000000000..0a9f45224b61f66076e45cb0855684cc6aa05ae2 GIT binary patch literal 446464 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 Hs^I?t)OLI% literal 0 HcmV?d00001 diff --git a/test/LibRed.Core.Tests/Data/KoreanUnicode.accdb b/test/LibRed.Core.Tests/Data/KoreanUnicode.accdb new file mode 100644 index 0000000000000000000000000000000000000000..f037e4a22cff1dd9b2d1bf8394823d2f8891eb30 GIT binary patch literal 446464 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 Access-authored fixture in Data\ whose order LibRed can create: one per locale.

Loaded = new(Load); @@ -97,31 +98,33 @@ private static Table Load() var codePoints = new char[count]; var primaryOffsets = new int[count]; var secondaryOffsets = new int[count]; + var overrideBits = new ulong[(char.MaxValue + 1) / 64]; 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; + Set(overrideBits, codePoint); primaryOffsets[i] = primary; secondaryOffsets[i] = secondary; primary += primaryLengths[i]; secondary += secondaryLengths[i]; } - var starts = new int[rangeCount]; - var lengths = new int[rangeCount]; + var ignorableBits = new ulong[(char.MaxValue + 1) / 64]; 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); + int length = ReadVarInt(rangeLengths, ref lengthCursor); + for (int c = start; c < start + length; c++) + Set(ignorableBits, c); } return new Table( codePoints, primaryLengths, primaryOffsets, primaryBytes, secondaryLengths, secondaryOffsets, secondaryBytes, - starts, lengths); + overrideBits, ignorableBits); } /// Reads one length-prefixed zlib stream — the unit every generated collation resource is built diff --git a/src/LibRed/LibRed.Engine/Execution/EvalScope.cs b/src/LibRed/LibRed.Engine/Execution/EvalScope.cs index dee00c8f..621d9908 100644 --- a/src/LibRed/LibRed.Engine/Execution/EvalScope.cs +++ b/src/LibRed/LibRed.Engine/Execution/EvalScope.cs @@ -16,7 +16,7 @@ internal sealed class EvalScope( // the schema and outer link are fixed for the scope's lifetime. private object?[] row = row; - /// Points this scope at a new row of the same schema (see 's callers for why + /// Points this scope at a new row of the same schema (see 's callers for why /// reuse is worthwhile). Returns the scope for fluent use. public EvalScope Rebind(object?[] newRow) { @@ -24,6 +24,18 @@ public EvalScope Rebind(object?[] newRow) return this; } + // The precomputed aggregates, rebindable with the row: a grouped query evaluates every group through one + // scope, each group bringing its key row and its own aggregate values. + private IReadOnlyDictionary? aggregates = aggregates; + + /// Points this scope at another group: its key row and its aggregates' values. + public EvalScope Rebind(object?[] newRow, IReadOnlyDictionary newAggregates) + { + row = newRow; + aggregates = newAggregates; + return this; + } + /// Resolves a precomputed aggregate (by reference), walking out to enclosing scopes so an outer /// aggregate referenced inside a correlated subquery (e.g. … WHERE x = MAX(o.Col) …) is found. public bool TryResolveAggregate(FunctionCall call, out object? value) diff --git a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs index db0e5c9b..3e7e8148 100644 --- a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs +++ b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs @@ -45,6 +45,13 @@ public ExpressionEvaluator Rebind(object?[] row) return this; } + /// Rebinds to another group of a grouped query: its key row and its aggregates' values. + public ExpressionEvaluator Rebind(object?[] row, IReadOnlyDictionary aggregates) + { + scope.Rebind(row, aggregates); + return this; + } + /// How text compares here: the database's collation, for every comparison whatever its operands. private JetTextComparer Text => subqueries.TextComparer; diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs index 3f5c16f7..471e34f4 100644 --- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs @@ -2255,13 +2255,17 @@ GroupState NewGroupState(object?[]? firstRow) GroupState? state = single; if (state is null) { + // Text is keyed here, once — as GroupKey would key it, so it holds these values as they are — and + // the group keeps them for ordering the output, where they used to be evaluated and keyed again. var keyValues = new object?[node.GroupBy.Count]; for (int i = 0; i < keyValues.Length; i++) - keyValues[i] = Compared(rowEval.Evaluate(node.GroupBy[i]), keyByText[i]); + keyValues[i] = ExpressionEvaluator.SortKey( + Compared(rowEval.Evaluate(node.GroupBy[i]), keyByText[i]), TextComparer); var key = new GroupKey(keyValues, TextComparer); if (!byKey.TryGetValue(key, out state)) { byKey[key] = state = NewGroupState(row); + state.Keys = key.Values; groupStates.Add(state); // first-appearance order, which a stable ORDER BY keeps between ties } } @@ -2276,8 +2280,12 @@ GroupState NewGroupState(object?[]? firstRow) } } - // The groups HAVING keeps, each with the row that resolves its keys and its aggregates' values. - var groups = new List<(object?[] KeyRow, Dictionary Values, ExpressionEvaluator Eval)>(); + // The groups HAVING keeps, each with the row that resolves its keys, its aggregates' values, and its + // grouping key as compared. Every group is evaluated through one scope, rebound to it: HAVING, the windows + // and the projection each take the groups one at a time, and a scope per group made every group resolve its + // column references afresh. + var groups = new List<(object?[] KeyRow, Dictionary Values, object?[] Keys)>(); + ExpressionEvaluator groupEval = Eval(inColumns, [], outer); foreach (GroupState state in groupStates) { var values = new Dictionary(ReferenceComparer.Instance); @@ -2294,45 +2302,45 @@ GroupState NewGroupState(object?[]? firstRow) // reach an outer aggregate). An empty group only happens for an aggregate with no GROUP BY over // zero rows (e.g. COUNT(*) -> 0); there are no key columns to resolve, so a null row suffices. object?[] keyRow = state.KeyRow ?? new object?[inColumns.Count]; - var eval = new ExpressionEvaluator(new EvalScope(inColumns, keyRow, outer, values), this, _parameters, _session); // HAVING filters whole groups after aggregation. - if (node.Having is not null && !eval.IsTrue(node.Having)) + if (node.Having is not null && !groupEval.Rebind(keyRow, values).IsTrue(node.Having)) continue; - groups.Add((keyRow, values, eval)); + groups.Add((keyRow, values, state.Keys)); } // The windows see the groups as their rows, as the standard orders it: after HAVING, before the projection. - var windowValues = new object?[groups.Count][]; - for (int i = 0; i < groups.Count; i++) + var windowValues = new object?[windows.Count > 0 ? groups.Count : 0][]; + for (int i = 0; i < windowValues.Length; i++) windowValues[i] = new object?[windows.Count]; for (int slot = 0; slot < windows.Count; slot++) - ComputeWindow(windows[slot].Function, groups.Count, i => groups[i].Eval, inColumns, windowValues, slot, windowTypes[slot]); - - // Each output row carries its ORDER BY key values AND its grouping-key values, evaluated in the same - // group scope as the projection, to sort the groups afterward: by ORDER BY if present, otherwise — - // matching Access, which returns GROUP BY results ascending by the grouping columns — by the group key - // (this also makes a TOP-1-over-a-GROUP-BY deterministic, as Access/SQL Server do). - var outRows = new List<(object?[] Row, object?[] SortKeys, object?[] GroupKeys)>(); + ComputeWindow(windows[slot].Function, groups.Count, i => groupEval.Rebind(groups[i].KeyRow, groups[i].Values), + inColumns, windowValues, slot, windowTypes[slot]); + + // Each output row carries its ORDER BY key values AND its grouping-key values, to sort the groups + // afterward: by ORDER BY if present, otherwise — matching Access, which returns GROUP BY results ascending + // by the grouping columns — by the group key (this also makes a TOP-1-over-a-GROUP-BY deterministic, as + // Access/SQL Server do). The grouping key is the one the group was found by, already keyed. + var outRows = new List<(object?[] Row, object?[] SortKeys, object?[] GroupKeys)>(groups.Count); bool[] sortByText = ComparedAsText(node.OrderBy.Select(k => k.Value), columns); - bool[] groupByText = ComparedAsText(node.GroupBy, inColumns); + ExpressionEvaluator? windowEval = windows.Count > 0 ? Eval(columns, [], outer) : null; for (int g = 0; g < groups.Count; g++) { - var (keyRow, values, eval) = groups[g]; - if (windows.Count > 0) - eval = new ExpressionEvaluator( - new EvalScope(columns, [.. keyRow, .. windowValues[g]], outer, values), this, _parameters, _session); - - object?[] row = node.Projection - .Select((item, i) => ExpressionEvaluator.ToResultPlaces( - ExpressionEvaluator.AsColumnType(eval.Evaluate(item.Value), conversions[i].ConvertTo, currency: false), outTypes[i])) - .ToArray(); + var (keyRow, values, keys) = groups[g]; + ExpressionEvaluator eval = windowEval is null + ? groupEval.Rebind(keyRow, values) + : windowEval.Rebind([.. keyRow, .. windowValues[g]], values); + + var row = new object?[node.Projection.Count]; + for (int i = 0; i < row.Length; i++) + row[i] = ExpressionEvaluator.ToResultPlaces( + ExpressionEvaluator.AsColumnType(eval.Evaluate(node.Projection[i].Value), conversions[i].ConvertTo, currency: false), + outTypes[i]); // Only compared, never returned — so text keys carry their collation keys (see SortKey). - object?[] sortKeys = node.OrderBy.Select((k, i) => - ExpressionEvaluator.SortKey(Compared(eval.Evaluate(k.Value), sortByText[i]), TextComparer)).ToArray(); - object?[] groupKeys = node.GroupBy.Select((k, i) => - ExpressionEvaluator.SortKey(Compared(eval.Evaluate(k), groupByText[i]), TextComparer)).ToArray(); - outRows.Add((row, sortKeys, groupKeys)); + var sortKeys = node.OrderBy.Count == 0 ? [] : new object?[node.OrderBy.Count]; + for (int i = 0; i < sortKeys.Length; i++) + sortKeys[i] = ExpressionEvaluator.SortKey(Compared(eval.Evaluate(node.OrderBy[i].Value), sortByText[i]), TextComparer); + outRows.Add((row, sortKeys, keys)); } if (node.OrderBy.Count > 0) @@ -2387,6 +2395,9 @@ private sealed class GroupState { public object?[]? KeyRow; public required GroupAggregate?[] Calls; + /// The grouping key's values as compared, which order the groups when there is no ORDER BY; + /// empty without a GROUP BY. + public object?[] Keys = []; } /// @@ -2643,9 +2654,16 @@ internal sealed class GroupKey(object?[] values, LibRed.Storage.JetTextComparer return keyed; } - public bool Equals(GroupKey? other) => - other is not null && _values.Length == other._values.Length - && _values.Zip(other._values).All(p => KeyEquals(p.First, p.Second, _text)); + public bool Equals(GroupKey? other) + { + if (other is null || _values.Length != other._values.Length) return false; + for (int i = 0; i < _values.Length; i++) + if (!KeyEquals(_values[i], other._values[i], _text)) return false; + return true; + } + + /// The values as compared — text as its collation key — which also order the groups. + public object?[] Values => _values; public override bool Equals(object? obj) => Equals(obj as GroupKey); public override int GetHashCode() From 9ae5e33d2daa73563914c88b3b055ed32aa92a3d Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 02:13:09 +0800 Subject: [PATCH 52/88] Seek an index without building a set, a decoder or a key list each time An index-nested-loop join seeks once per outer row, and each seek built its machinery from nothing: a HashSet of pages for the descent and another for the leaf walk, the path list a seek never reads, a new RowDecoder with its own column array, a new IndexWriter, and a growing list for the key. The descent now bounds its depth - no B-tree in a 2 GB file is 21 levels deep, so 64 still catches a cycle - and the leaf walk its steps by the page count; a seek asks for the leaf alone; Table keeps its last decoder (with the mask it was made for) and one reading IndexWriter; IndexKeyEncoder builds in per-thread lists. A comparison's result is one of two cached boxes rather than a new one per row. agg.group_over_join 41.9 to 34.3 ms and 20.3 to 12.8 MB; PointSeek 2 KB to 800 B. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Storage/IndexKeyEncoder.cs | 13 ++++- src/LibRed/LibRed.Core/Storage/IndexWriter.cs | 47 +++++++++++++------ src/LibRed/LibRed.Core/Storage/Table.cs | 35 +++++++++++--- .../Execution/ExpressionEvaluator.cs | 20 +++++--- 4 files changed, 86 insertions(+), 29 deletions(-) diff --git a/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs b/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs index 4056792b..f97e3596 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs @@ -55,10 +55,17 @@ internal static byte[] EncodeWithoutLengthLimit( IReadOnlyList<(ColumnDef Column, bool Ascending)> columns, object?[] values) => Encode(columns, values, enforceLengthLimit: false); + // The lists a key is built in, this thread's, cleared rather than allocated — as the collation encoders keep + // theirs: an index-nested-loop join encodes a key for every outer row, and two growing lists per key were most + // of what the seek allocated. Safe because nothing here re-enters Encode. + [ThreadStatic] private static List? t_buffer; + [ThreadStatic] private static List? t_textKey; + private static byte[] Encode( IReadOnlyList<(ColumnDef Column, bool Ascending)> columns, object?[] values, bool enforceLengthLimit) { - var buffer = new List(); + List buffer = t_buffer ??= []; + buffer.Clear(); for (int i = 0; i < columns.Count; i++) { @@ -107,7 +114,9 @@ private static byte[] Encode( if (column.Type == JetDataType.Memo && text.Length > MemoKeyMaxChars) text = text[..MemoKeyMaxChars]; - var ascendingKey = new List { IndexKeyFlags.AscStart }; + List ascendingKey = t_textKey ??= []; + ascendingKey.Clear(); + ascendingKey.Add(IndexKeyFlags.AscStart); LocaleTailoring? tailoring = JetLocaleTailoring.For(column.Collation); // The word-sort flag is discarded: it existed only to refuse a key whose record the truncation // would drop, and that refusal is gone — see the note at the truncation below. diff --git a/src/LibRed/LibRed.Core/Storage/IndexWriter.cs b/src/LibRed/LibRed.Core/Storage/IndexWriter.cs index f983d618..71e0e5d1 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexWriter.cs @@ -58,11 +58,11 @@ public void AddEntry(IndexDef index, object?[] values, RowId rowId) public bool KeyExists(IndexDef index, object?[] values, int? excludePointer = null) { byte[] key = IndexKeyEncoder.Encode(index.Columns, values); - int leaf = Descend(index.RootPage, WithTrailer(key, 0))[^1]; - var visitedLeaves = new HashSet(); + int leaf = Descend(index.RootPage, WithTrailer(key, 0), path: null); + int steps = 0; while (leaf != 0) { - if (!visitedLeaves.Add(leaf)) + if (!InLeafChain(ref steps)) throw new InvalidDataException($"Index leaf chain contains a cycle at page {leaf}."); ParsedIndexPage page = ReadIndexPage(leaf); if (page.Type != PageType.LeafIndexPage) @@ -91,11 +91,11 @@ public bool KeyExists(IndexDef index, object?[] values, int? excludePointer = nu public IEnumerable Seek(IndexDef index, object?[] values) { byte[] key = IndexKeyEncoder.Encode(index.Columns, values); - int leaf = Descend(index.RootPage, WithTrailer(key, 0))[^1]; - var visitedLeaves = new HashSet(); + int leaf = Descend(index.RootPage, WithTrailer(key, 0), path: null); + int steps = 0; while (leaf != 0) { - if (!visitedLeaves.Add(leaf)) + if (!InLeafChain(ref steps)) throw new InvalidDataException($"Index leaf chain contains a cycle at page {leaf}."); ParsedIndexPage page = ReadIndexPage(leaf); if (page.Type != PageType.LeafIndexPage) @@ -128,11 +128,11 @@ public IEnumerable SeekRange(IndexDef index, object?[]? low, object?[]? h if (!index.Columns[0].Ascending) (lowKey, highKey) = (highKey, lowKey); - int leaf = Descend(index.RootPage, WithTrailer(lowKey ?? [], 0))[^1]; - var visitedLeaves = new HashSet(); + int leaf = Descend(index.RootPage, WithTrailer(lowKey ?? [], 0), path: null); + int steps = 0; while (leaf != 0) { - if (!visitedLeaves.Add(leaf)) + if (!InLeafChain(ref steps)) throw new InvalidDataException($"Index leaf chain contains a cycle at page {leaf}."); ParsedIndexPage page = ReadIndexPage(leaf); if (page.Type != PageType.LeafIndexPage) @@ -267,15 +267,34 @@ internal static bool HasNullKey(IndexDef index, object?[] values) => private List Descend(int rootPage, byte[] fullKey) { var path = new List(); - var visited = new HashSet(); + Descend(rootPage, fullKey, path); + return path; + } + + /// Counts one more leaf of a chain walk, and says whether the walk can still be a chain: it cannot + /// visit more leaves than the file has pages, so one that tries is going round a loop. Counting catches the + /// loop as surely as a set of the leaves visited did, without the set a seek allocated every time. + private bool InLeafChain(ref int steps) => ++steps <= _channel.PageCount; + + /// The deepest a real B-tree can be. A node holds at least two children, and a file of at most 2 GB + /// has at most 2^20 pages even at 2 KB each, so no tree is 21 levels deep; this leaves room to spare. + private const int MaxDepth = 64; + + /// Descends to the leaf that should hold the key, returning it, and recording the path from the + /// root in when there is one — a seek needs only the leaf. + /// A descent that goes on past is circling through a corrupt node. Counting + /// levels catches that as surely as remembering every page visited did, without a set allocated per seek — + /// which an index-nested-loop join does once for every outer row. + private int Descend(int rootPage, byte[] fullKey, List? path) + { int pageNumber = rootPage; - while (true) + for (int depth = 0; ; depth++) { - if (!visited.Add(pageNumber)) + if (depth > MaxDepth) throw new InvalidDataException($"Index descent contains a cycle at page {pageNumber}."); - path.Add(pageNumber); + path?.Add(pageNumber); ParsedIndexPage page = ReadIndexPage(pageNumber); - if (page.Type == PageType.LeafIndexPage) return path; + if (page.Type == PageType.LeafIndexPage) return pageNumber; if (page.Type != PageType.IntermediateIndexPage) throw new InvalidDataException($"Index descent reached non-index page {pageNumber}."); diff --git a/src/LibRed/LibRed.Core/Storage/Table.cs b/src/LibRed/LibRed.Core/Storage/Table.cs index 29179f06..a0be2540 100644 --- a/src/LibRed/LibRed.Core/Storage/Table.cs +++ b/src/LibRed/LibRed.Core/Storage/Table.cs @@ -75,6 +75,29 @@ public bool[] DecodeOnly(IEnumerable columns) private RowDecoder NewDecoder(bool[]? decode = null) => new(Definition.Columns, Channel.Format, new LongValueReader(Channel), decode); + /// The decoder a seek reads its rows with: the last one made, while it was made for the same column + /// mask (the same array — a caller works one out and passes it to every seek), else a new one. + /// An index-nested-loop join seeks once per outer row, and building a decoder each time — its column + /// array copied, a long-value reader made — was a sixth of what such a join allocated. A decoder holds nothing + /// that changes as it decodes, so one serves every seek, interleaved or not. The mask and its decoder are held + /// as one object, so no reader can pair one with the other's partner. + private RowDecoder SeekDecoder(bool[]? decode) + { + if (_seekDecoder is { } held && ReferenceEquals(held.Mask, decode)) return held.Decoder; + RowDecoder decoder = NewDecoder(decode); + _seekDecoder = new MaskedDecoder(decode, decoder); + return decoder; + } + + private sealed record MaskedDecoder(bool[]? Mask, RowDecoder Decoder); + + private MaskedDecoder? _seekDecoder; + + /// The index reader every seek goes through, made once: seeking changes nothing about it. + private IndexWriter IndexReader => _indexReader ??= new IndexWriter(Channel, Definition); + + private IndexWriter? _indexReader; + /// Decodes the row at (following an overflow forward-pointer to a /// relocated row), or if the slot is empty/deleted. Used by an index seek, which /// yields row ids. @@ -107,8 +130,8 @@ private RowDecoder NewDecoder(bool[]? decode = null) => /// the predicate. is as for . public IEnumerable SeekRows(IndexDef index, object?[] values, bool[]? decode = null) { - var decoder = NewDecoder(decode); - foreach (RowId id in new IndexWriter(Channel, Definition).Seek(index, values)) + RowDecoder decoder = SeekDecoder(decode); + foreach (RowId id in IndexReader.Seek(index, values)) if (GetRow(id, decoder) is { } row) yield return row; } @@ -119,8 +142,8 @@ private RowDecoder NewDecoder(bool[]? decode = null) => /// in full. public IEnumerable<(RowId Id, object?[] Values)> SeekRowsWithIds(IndexDef index, object?[] values, bool[]? decode = null) { - var decoder = NewDecoder(decode); - foreach (RowId id in new IndexWriter(Channel, Definition).Seek(index, values)) + RowDecoder decoder = SeekDecoder(decode); + foreach (RowId id in IndexReader.Seek(index, values)) if (GetRow(id, decoder) is { } row) yield return (id, row); } @@ -130,8 +153,8 @@ private RowDecoder NewDecoder(bool[]? decode = null) => /// boundaries; the caller re-checks the predicate. is as for . public IEnumerable SeekRangeRows(IndexDef index, object?[]? low, object?[]? high, bool[]? decode = null) { - var decoder = NewDecoder(decode); - foreach (RowId id in new IndexWriter(Channel, Definition).SeekRange(index, low, high)) + RowDecoder decoder = SeekDecoder(decode); + foreach (RowId id in IndexReader.SeekRange(index, low, high)) if (GetRow(id, decoder) is { } row) yield return row; } diff --git a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs index 3e7e8148..111505dc 100644 --- a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs +++ b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs @@ -2003,6 +2003,12 @@ private static DateTime InDateRange(Func compute) }; } + // A comparison's result as an object without boxing a new bool for every row it is asked of: a filter or a + // join's residual ON compares once per row, and each answer was an allocation. + private static readonly object BoxedTrue = true, BoxedFalse = false; + + private static object Boxed(bool value) => value ? BoxedTrue : BoxedFalse; + private object? EvaluateBinary(BinaryExpression b) { // AND/OR use Kleene three-valued logic, and short-circuit: `false AND x` is false and `true OR x` @@ -2093,7 +2099,7 @@ private static DateTime InDateRange(Func compute) return null; if (b.Operator is BinaryOperator.Equal or BinaryOperator.NotEqual && TruthTest(b, left, right) is bool truth) - return b.Operator == BinaryOperator.Equal ? truth : !truth; + return Boxed(b.Operator == BinaryOperator.Equal ? truth : !truth); // A result that is a Currency (NumberTypeOf) is one at every step, not only in the result column: its four // places and its range apply to it where it is worked out, as CCur applies them (verified vs ACE: Currency @@ -2112,12 +2118,12 @@ private static DateTime InDateRange(Func compute) return b.Operator switch { - BinaryOperator.Equal => CompareOperands(b, left, right) == 0, - BinaryOperator.NotEqual => CompareOperands(b, left, right) != 0, - BinaryOperator.LessThan => CompareOperands(b, left, right) < 0, - BinaryOperator.LessThanOrEqual => CompareOperands(b, left, right) <= 0, - BinaryOperator.GreaterThan => CompareOperands(b, left, right) > 0, - BinaryOperator.GreaterThanOrEqual => CompareOperands(b, left, right) >= 0, + BinaryOperator.Equal => Boxed(CompareOperands(b, left, right) == 0), + BinaryOperator.NotEqual => Boxed(CompareOperands(b, left, right) != 0), + BinaryOperator.LessThan => Boxed(CompareOperands(b, left, right) < 0), + BinaryOperator.LessThanOrEqual => Boxed(CompareOperands(b, left, right) <= 0), + BinaryOperator.GreaterThan => Boxed(CompareOperands(b, left, right) > 0), + BinaryOperator.GreaterThanOrEqual => Boxed(CompareOperands(b, left, right) >= 0), // LIKE reads any other value as the text CStr gives it (verified vs ACE: TRUE LIKE '-1' is True). A binary // value becomes text too, so LIKE is case-insensitive over a binary column even though '=' on the same // column is byte-wise: `B LIKE 'A%'` matches both 0x4100 ('A') and 0x6100 ('a'). From 69b784fa6e3860fd2eef40cd09b46dfa9ad8fc76 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 02:14:19 +0800 Subject: [PATCH 53/88] Build a join's rows from the columns its statement reads A join builds a new row for every match, and built them whole: a grouped join over a ten-column table carried eight columns no expression looked at in every joined row. Where ColumnPruning already prunes a join's table reads - below a projection or aggregate with no star, so no row surfaces whole - it now also gives the join the names the statement reads, and the join builds each row from the columns with those names, the kept positions worked out once. Names match whatever their qualifier, so every column a reference could mean stays and nothing resolves differently; the ON residual's names are among them. All three join paths - index nested loop, nested loop and hash - narrow, outer-join padding included. agg.group_over_join 12.8 to 10.7 MB; join.full_indexed 18.6 to 8.9 MB; join.cross_small 301 to 143 us; nw.group_agg_join 3.5 to 1.2 MB. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Engine/Execution/QueryExecutor.cs | 85 ++++++++++++++++--- src/LibRed/LibRed.Engine/Plan/PlanNodes.cs | 8 ++ .../LibRed.Engine/Planning/ColumnPruning.cs | 17 +++- .../LibRed.Engine.Tests/ColumnPruningTests.cs | 68 +++++++++++++++ 4 files changed, 162 insertions(+), 16 deletions(-) diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs index 471e34f4..fb881e7a 100644 --- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs @@ -203,6 +203,61 @@ public ResultSet ExecuteQuery(PlanNode plan) /// so describing a query costs only the planning. Used to report a view's columns in schema metadata. internal IReadOnlyList DescribeQuery(PlanNode plan) => Execute(plan, null).Columns; + /// + /// What a join passes on of its [left.., right..] rows: every column, or — where + /// says which names the query reads — only the columns with those names, the + /// output built that narrow from the start rather than cut down afterwards. + /// + /// A join's output row is a new array for every match, so its width is paid once per joined row. + /// Built whole, a grouped join over a ten-column table carried eight columns no expression ever looked at, in + /// every row. The kept positions of each side are worked out once, here. + private sealed class JoinShape + { + private readonly int[]? _left, _right; + private readonly int _leftWidth, _rightWidth; + + /// The columns of the whole row, left side first. + /// How many of them are the left side's. + /// The names the query reads, or null to pass on every column. + public JoinShape(IReadOnlyList joined, int leftWidth, IReadOnlySet? keep) + { + _leftWidth = leftWidth; + _rightWidth = joined.Count - leftWidth; + Columns = joined; + if (keep is null || joined.All(c => keep.Contains(c.Name))) return; + + var left = new List(); + var right = new List(); + var columns = new List(); + for (int i = 0; i < joined.Count; i++) + { + if (!keep.Contains(joined[i].Name)) continue; + columns.Add(joined[i]); + if (i < leftWidth) left.Add(i); + else right.Add(i - leftWidth); + } + (_left, _right, Columns) = ([.. left], [.. right], columns); + } + + /// The columns the join passes on, in the order its rows hold them. + public IReadOnlyList Columns { get; } + + /// One output row from a row of each side; a null side stands for a row of nulls, an outer join's + /// padding. + public object?[] Combine(object?[]? left, object?[]? right) + { + if (_left is null) + return [.. left ?? new object?[_leftWidth], .. right ?? new object?[_rightWidth]]; + + var row = new object?[_left.Length + _right!.Length]; + if (left is not null) + for (int i = 0; i < _left.Length; i++) row[i] = left[_left[i]]; + if (right is not null) + for (int i = 0; i < _right.Length; i++) row[_left.Length + i] = right[_right[i]]; + return row; + } + } + /// The columns a join publishes. Joining something already collapsed — an aggregate, a DISTINCT, /// a union — makes the whole join non-updatable, as Access counts updatability, so a stored column reached /// through one no longer stands for a row anybody can write back to. @@ -1573,7 +1628,8 @@ private static HashSet ContributingQualifiers( string innerAlias = seek.Alias ?? seek.Table; int innerWidth = innerTable.Definition.Columns.Count; var seekColumns = innerTable.Definition.Columns.Select(c => OutputColumn.Of(innerAlias, c)).ToList(); - var joinColumns = leftColumns.Concat(seekColumns).ToList(); + var seekShape = new JoinShape(leftColumns.Concat(seekColumns).ToList(), leftColumns.Count, join.Keep); + IReadOnlyList joinColumns = seekShape.Columns; int[] keyCols = seek.Index.Columns.Select(c => c.Column.Index).ToArray(); bool[]? decode = ColumnPruning.Mask(innerTable.Definition, seek.Decode); @@ -1605,7 +1661,7 @@ private static HashSet ContributingQualifiers( ? innerTable.SeekRows(seek.Index, keyValues, decode) : innerTable.Rows(decode)) { - object?[] combined = [.. left, .. right]; + object?[] combined = seekShape.Combine(left, right); if (on is null || onEval.Rebind(combined).IsTrue(on)) { matched = true; @@ -1613,7 +1669,7 @@ private static HashSet ContributingQualifiers( } } if (leftOuter && !matched) - yield return [.. left, .. new object?[innerWidth]]; + yield return seekShape.Combine(left, null); } } @@ -1622,7 +1678,8 @@ private static HashSet ContributingQualifiers( var (rightColumns, rightRowsEnum) = Execute(join.Right, outer); - var columns = JoinedColumns(leftColumns, rightColumns); + var shape = new JoinShape(JoinedColumns(leftColumns, rightColumns), leftColumns.Count, join.Keep); + IReadOnlyList columns = shape.Columns; var rightRows = rightRowsEnum.ToList(); // re-iterated per left row if (on is null && join.Kind != JoinKind.Cross) throw new NotSupportedException("Joins require an ON condition."); @@ -1663,7 +1720,7 @@ private static HashSet ContributingQualifiers( bool matched = false; for (int r = 0; r < rightRows.Count; r++) { - object?[] combined = [.. left, .. rightRows[r]]; + object?[] combined = shape.Combine(left, rightRows[r]); if (rowOn is null || onEval.Rebind(combined).IsTrue(rowOn)) { matched = true; @@ -1673,13 +1730,13 @@ private static HashSet ContributingQualifiers( } if (leftOuter && !matched) - yield return [.. left, .. new object?[rightColumns.Count]]; + yield return shape.Combine(left, null); } // 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]]; + yield return shape.Combine(null, rightRows[r]); } return (columns, Rows()); @@ -1689,8 +1746,8 @@ private static HashSet ContributingQualifiers( { var (leftColumns, leftRows) = Execute(join.Left, outer); var (rightColumns, rightRowsEnum) = Execute(join.Right, outer); - var joinColumns = JoinedColumns(leftColumns, rightColumns); - int leftWidth = leftColumns.Count, rightWidth = rightColumns.Count; + var shape = new JoinShape(JoinedColumns(leftColumns, rightColumns), leftColumns.Count, join.Keep); + IReadOnlyList joinColumns = shape.Columns; Expression on = join.On; // INNER/LEFT build the right side and probe with the left; RIGHT builds the left and probes with the @@ -1752,7 +1809,7 @@ private static HashSet ContributingQualifiers( { object?[] left = buildRight ? p : b; object?[] right = buildRight ? b : p; - object?[] combined = [.. left, .. right]; + object?[] combined = shape.Combine(left, right); if (onEval.Rebind(combined).IsTrue(on)) { matched = true; @@ -1763,8 +1820,8 @@ private static HashSet ContributingQualifiers( } if (preserveProbe && !matched) yield return buildRight - ? [.. p, .. new object?[rightWidth]] // probe is the left side; right is null - : [.. new object?[leftWidth], .. p]; // probe is the right side; left is null + ? shape.Combine(p, null) // probe is the left side; right is null + : shape.Combine(null, 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 @@ -1773,8 +1830,8 @@ private static HashSet ContributingQualifiers( 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 + ? shape.Combine(null, b) // build is the right side; left is null + : shape.Combine(b, null); // build is the left side; right is null foreach (List bucket in table.Values) foreach (object?[] b in bucket) diff --git a/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs b/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs index 57792f7b..7dce84bc 100644 --- a/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs +++ b/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs @@ -74,6 +74,11 @@ public sealed record ProjectNode(PlanNode Input, IReadOnlyList Proje public sealed record JoinNode(PlanNode Left, PlanNode Right, JoinKind Kind, Expression? On) : PlanNode { public override IReadOnlyList Children => [Left, Right]; + + /// The column names the query reads, when ColumnPruning has shown nothing else can reach the + /// output: the join passes on only the columns with those names, and builds its rows that narrow. Null + /// passes on every column of both sides. + public IReadOnlySet? Keep { get; init; } } /// @@ -89,6 +94,9 @@ public sealed record HashJoinNode( IReadOnlyList LeftKeys, IReadOnlyList RightKeys, Expression On) : PlanNode { public override IReadOnlyList Children => [Left, Right]; + + /// As . + public IReadOnlySet? Keep { get; init; } } /// diff --git a/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs b/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs index 27139d99..ec37e67e 100644 --- a/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs +++ b/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs @@ -68,8 +68,21 @@ FilterNode or SortNode or LimitNode or DistinctNode or DerivedTableNode or Windo DistinctNode d => d with { Input = Rewrite(d.Input, visible, names) }, DerivedTableNode dt => dt with { Input = Rewrite(dt.Input, visible, names) }, WindowNode w => w with { Input = Rewrite(w.Input, visible, names) }, - JoinNode j => j with { Left = Rewrite(j.Left, visible, names), Right = Rewrite(j.Right, visible, names) }, - HashJoinNode h => h with { Left = Rewrite(h.Left, visible, names), Right = Rewrite(h.Right, visible, names) }, + // A join builds each output row anew, so where its rows cannot surface whole it builds them from the read + // columns alone. Dropping a column no reference names cannot change what any reference resolves to: the + // names are matched whatever their qualifier, so every column a reference could mean stays. + JoinNode j => j with + { + Left = Rewrite(j.Left, visible, names), + Right = Rewrite(j.Right, visible, names), + Keep = visible ? null : names, + }, + HashJoinNode h => h with + { + Left = Rewrite(h.Left, visible, names), + Right = Rewrite(h.Right, visible, names), + Keep = visible ? null : names, + }, SetOperationNode so => so with { Left = Rewrite(so.Left, visible, names), Right = Rewrite(so.Right, visible, names) }, // DistinctRowNode compares whole underlying rows; anything else is unknown. Neither is pruned below. diff --git a/test/LibRed.Engine.Tests/ColumnPruningTests.cs b/test/LibRed.Engine.Tests/ColumnPruningTests.cs index 2b681506..c3c1f7dd 100644 --- a/test/LibRed.Engine.Tests/ColumnPruningTests.cs +++ b/test/LibRed.Engine.Tests/ColumnPruningTests.cs @@ -87,6 +87,47 @@ public void An_outer_column_named_only_inside_a_correlated_subquery_is_read() Assert.Equal([(1, "D1"), (2, "D2"), (3, "D0")], rows); } + // --- a join passes on only the columns its statement names, and builds its rows that narrow ----------- + + [Fact] + public void An_aggregate_over_a_join_reads_what_it_names() + { + // Group sums of V over T.Grp = U.K: 0 → 300, 1 → 220, 2 → 260. + var rows = Rows(Seeded(), "SELECT U.Descr, SUM(T.V) FROM T INNER JOIN U ON T.Grp = U.K GROUP BY U.Descr") + .Select(r => ((string)r[0]!, Convert.ToInt32(r[1]))).ToArray(); + Assert.Equal([("D0", 300), ("D1", 220), ("D2", 260)], rows); + } + + [Fact] + public void A_narrowed_left_join_pads_its_unmatched_rows() + { + // Only V = 100 meets an X; every other T row comes back with a null Descr. + var rows = Rows(Seeded(), "SELECT T.Id, U.Descr FROM T LEFT JOIN U ON T.V = U.X") + .ToDictionary(r => Convert.ToInt32(r[0]), r => r[1]); + Assert.Equal(12, rows.Count); + Assert.Equal("D1", rows[10]); + Assert.All(rows.Where(p => p.Key != 10), p => Assert.Null(p.Value)); + } + + [Fact] + public void A_narrowed_full_join_pads_either_side() + { + // U rows 0 and 2 (X 0 and 200) meet no V, so they come back with a null Id; T row 10 meets K 1. + var rows = Rows(Seeded(), "SELECT T.Id, U.K FROM T FULL JOIN U ON T.V = U.X") + .Select(r => (r[0] is null ? (int?)null : Convert.ToInt32(r[0]), r[1] is null ? (int?)null : Convert.ToInt32(r[1]))) + .ToList(); + Assert.Equal(14, rows.Count); + Assert.Contains((10, 1), rows); + Assert.Contains((null, 0), rows); + Assert.Contains((null, 2), rows); + } + + [Fact] + public void A_name_both_sides_share_is_still_ambiguous_when_the_join_is_narrowed() + // Every column a reference could mean is kept, so an unqualified Id still finds two. + => Assert.ThrowsAny(() => + Rows(Seeded(), "SELECT Id FROM T AS a INNER JOIN T AS b ON a.Grp = b.Grp")); + [Fact] public void A_column_named_only_in_an_in_subquery_is_read() => Assert.Equal([10], Ints(Seeded(), "SELECT Id FROM T WHERE V IN (SELECT X FROM U)")); @@ -175,6 +216,33 @@ public void A_projection_prunes_its_scan_to_the_names_it_reads() Assert.DoesNotContain("Note", decode); } + private static IReadOnlySet? KeepOf(PlanNode join) => join switch + { + JoinNode j => j.Keep, + HashJoinNode h => h.Keep, + _ => throw new InvalidOperationException($"{join.GetType().Name} is not a join."), + }; + + [Fact] + public void A_join_under_an_aggregate_keeps_the_names_it_reads() + { + PlanNode join = Nodes(Seeded().PlanFor( + "SELECT U.Descr, SUM(T.V) FROM T INNER JOIN U ON T.Grp = U.K GROUP BY U.Descr")) + .Single(n => n is JoinNode or HashJoinNode); + IReadOnlySet? keep = KeepOf(join); + Assert.NotNull(keep); + Assert.Superset(new HashSet(["Descr", "V", "Grp", "K"]), new HashSet(keep, StringComparer.OrdinalIgnoreCase)); + Assert.DoesNotContain("Note", keep); + } + + [Theory] + [InlineData("SELECT * FROM T INNER JOIN U ON T.Grp = U.K")] + [InlineData("SELECT T.*, U.Descr FROM T INNER JOIN U ON T.Grp = U.K")] + [InlineData("SELECT DISTINCTROW T.Grp FROM T INNER JOIN U ON U.K >= T.Grp")] + public void A_join_whose_rows_can_reach_the_output_whole_keeps_every_column(string sql) + => Assert.All(Nodes(Seeded().PlanFor(sql)).Where(n => n is JoinNode or HashJoinNode), + join => Assert.Null(KeepOf(join))); + [Theory] [InlineData("SELECT * FROM T")] [InlineData("SELECT DISTINCT * FROM T")] From 3f5508407f6d9a31b2eac40656a329f6c4706d59 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 04:55:18 +0800 Subject: [PATCH 54/88] Cascade by the relationship's index blocks, and list schema names in collation order A relationship is recorded twice: MSysRelationships.grbit, and the action bytes (0x15/0x16) of its blocks in both tables' definitions. Made to disagree, ACE cascades by the blocks - a delete grbit calls cascading but the blocks do not is refused, and the reverse cascades - so the catalog now takes a relationship's update and delete actions from the child's block, and grbit only where there is none (an unenforced relationship, which has nothing to cascade). LogicalIndexDef carries both bytes; the action values move to IndexBlockFormat, shared by writer and reader. grbit 0x01 is a one-to-one relationship. Only Access's dialog and DAO set it; SQL never does, even from one primary key to another. On an enforced relationship the child's index is created unique with it, and what ACE then enforces is that index's unique flag, not the bit. ForeignKey.IsOneToOne reads it, and INFORMATION_SCHEMA.RELATIONS reports RELATION_TYPE from it as DAO does. It used to guess from whichever index covered the child columns, which called a foreign key between primary keys one-to-one and an unenforced one-to-one one-to-many. The spec gains 0x01 and the two join-type bits, 0x1000000 and 0x2000000. CREATE INDEX and the parent side of a relationship spliced their table's definition by hand rather than going through ParseTdef and WriteTdef, and so missed what WriteTdef does - among it moving the table's DateUpdate, which ACE does for both. They now go through it. GetSchema sorted names ordinally where ACE sorts them in the database's collation - Order_X before OrderT, O'Brien after OBrien. Tables, Views, Procedures and Statistics now match ACE row for row, and TableConstraints, now sorted too, breaks ties between the keys Access names alike by table. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/ForeignKey.cs | 5 +- src/LibRed/LibRed.Core/Catalog/JetCatalog.cs | 35 +++-- .../LibRed.Core/Catalog/LogicalIndexDef.cs | 7 +- .../LibRed.Core/Formats/IndexBlockFormat.cs | 6 + .../LibRed.Core/Formats/RelationshipFlags.cs | 5 + .../LibRed.Core/Pages/TableDefinitionPage.cs | 10 +- .../LibRed.Core/Storage/TableCreator.cs | 146 ++++-------------- .../Internal/LibRedDatabaseModelFactory.cs | 4 +- .../LibRed.Engine/Schema/InformationSchema.cs | 22 +-- .../LibRed.Engine/Schema/SchemaRowsets.cs | 32 +++- src/LibRed/docs/format/appendix-structures.md | 4 +- .../docs/format/page-02d-constraints.md | 17 +- src/LibRed/docs/format/system-catalog.md | 27 +++- test/LibRed.Ado.Tests/LibRedSchemaTests.cs | 34 ++++ .../CatalogDateUpdateTests.cs | 68 ++++++++ .../ForeignKeyLinkageTests.cs | 68 ++++++++ .../RelationTypeAccessTests.cs | 62 ++++++++ .../InformationSchemaTests.cs | 11 ++ 18 files changed, 391 insertions(+), 172 deletions(-) create mode 100644 test/LibRed.Core.Tests/CatalogDateUpdateTests.cs create mode 100644 test/LibRed.Engine.AccessTests/RelationTypeAccessTests.cs diff --git a/src/LibRed/LibRed.Core/Catalog/ForeignKey.cs b/src/LibRed/LibRed.Core/Catalog/ForeignKey.cs index 36577423..7d2c937e 100644 --- a/src/LibRed/LibRed.Core/Catalog/ForeignKey.cs +++ b/src/LibRed/LibRed.Core/Catalog/ForeignKey.cs @@ -19,6 +19,8 @@ namespace LibRed.Catalog; /// ON UPDATE SET NULL). /// ON UPDATE SET NULL — pathway only; not read back yet (its storage is /// unverified: the ACE OLE DB provider rejects the DDL, so the grbit/info-block bytes could not be probed). +/// Whether the relationship is one-to-one (grbit 0x01), as Access and DAO +/// report it. Recorded, not derived: SQL cannot create one, and ACE never infers it from the indexes. public sealed record ForeignKey( string Name, string Table, @@ -29,4 +31,5 @@ public sealed record ForeignKey( bool CascadeDelete, bool IsInherited = false, bool DeleteSetNull = false, - bool UpdateSetNull = false); \ No newline at end of file + bool UpdateSetNull = false, + bool IsOneToOne = false); \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs index c9344657..7c343b45 100644 --- a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs +++ b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs @@ -295,15 +295,32 @@ private List LoadRelationships() } return groups - .Select(kvp => new ForeignKey( - kvp.Key, - kvp.Value.Child, - kvp.Value.Parent, - kvp.Value.Columns.OrderBy(x => x.Order).Select(x => (x.Column, x.ReferencedColumn)).ToList(), - IsEnforced: (kvp.Value.Flags & RelationshipFlags.DontEnforce) == 0, - CascadeUpdate: (kvp.Value.Flags & RelationshipFlags.UpdateCascade) != 0, - CascadeDelete: (kvp.Value.Flags & RelationshipFlags.DeleteCascade) != 0, - DeleteSetNull: (kvp.Value.Flags & RelationshipFlags.DeleteSetNull) != 0)) + .Select(kvp => + { + // The actions come from the child's relationship block in its TDEF (0x15/0x16), not from grbit: + // made to disagree, ACE cascades by the block (measured against ACE). grbit answers only for a + // relationship with no block — an unenforced one, which has nothing to cascade. + LogicalIndexDef? block = FindTable(kvp.Value.Child)?.LogicalIndexes.FirstOrDefault(l => + l.IsRelationship && !l.IsIncomingRelationship && + string.Equals(l.Name, kvp.Key, StringComparison.OrdinalIgnoreCase)); + int flags = kvp.Value.Flags; + return new ForeignKey( + kvp.Key, + kvp.Value.Child, + kvp.Value.Parent, + kvp.Value.Columns.OrderBy(x => x.Order).Select(x => (x.Column, x.ReferencedColumn)).ToList(), + IsEnforced: (flags & RelationshipFlags.DontEnforce) == 0, + CascadeUpdate: block is null + ? (flags & RelationshipFlags.UpdateCascade) != 0 + : block.UpdateAction == IndexBlockFormat.CascadeAction, + CascadeDelete: block is null + ? (flags & RelationshipFlags.DeleteCascade) != 0 + : block.DeleteAction == IndexBlockFormat.CascadeAction, + DeleteSetNull: block is null + ? (flags & RelationshipFlags.DeleteSetNull) != 0 + : block.DeleteAction == IndexBlockFormat.SetNullAction, + IsOneToOne: (flags & RelationshipFlags.OneToOne) != 0); + }) .ToList(); } diff --git a/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs b/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs index d559cb80..cf59a6c4 100644 --- a/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs +++ b/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs @@ -14,8 +14,13 @@ namespace LibRed.Catalog; /// The relationship's direction as the info block records it: 0 none, 1 incoming /// (this table is the parent — the half Access names .r… and keeps out of its schema views), 2 outgoing /// (this table holds the foreign key). +/// The info block's update action (0x15): 0x04 on a plain index; on a +/// relationship 0x00 no cascade, 0x01 cascade update. Written on both ends of a relationship. +/// The info block's delete action (0x16): 0x04 on a plain index; on a +/// relationship 0x00 no cascade, 0x01 cascade delete, 0x02 ON DELETE SET NULL. public sealed record LogicalIndexDef( - string Name, int RealIndexOrdinal, bool IsRelationship, bool IsPrimaryKey, byte ForeignKeyType) + string Name, int RealIndexOrdinal, bool IsRelationship, bool IsPrimaryKey, byte ForeignKeyType, + byte UpdateAction, byte DeleteAction) { /// True for the parent side of a relationship, which Access hides. public bool IsIncomingRelationship => ForeignKeyType == IncomingRelationship; diff --git a/src/LibRed/LibRed.Core/Formats/IndexBlockFormat.cs b/src/LibRed/LibRed.Core/Formats/IndexBlockFormat.cs index c738960c..7e4dabd2 100644 --- a/src/LibRed/LibRed.Core/Formats/IndexBlockFormat.cs +++ b/src/LibRed/LibRed.Core/Formats/IndexBlockFormat.cs @@ -36,6 +36,12 @@ internal static class IndexBlockFormat /// Update/delete action byte on a plain (non-relationship) index. public const byte PlainAction = 0x04; + // Update/delete action bytes on a relationship's blocks, verified against ACE-created relationships. These, + // not MSysRelationships.grbit, are what ACE acts on: with the two made to disagree, it cascades by the block. + public const byte NoCascadeAction = 0x00; + public const byte CascadeAction = 0x01; + public const byte SetNullAction = 0x02; // delete only: ON DELETE SET NULL + // Index-info block type byte (at InfoTypeOffset). public const byte TypeSecondary = 0x00; public const byte TypePrimary = 0x01; diff --git a/src/LibRed/LibRed.Core/Formats/RelationshipFlags.cs b/src/LibRed/LibRed.Core/Formats/RelationshipFlags.cs index 713558e0..57bdb196 100644 --- a/src/LibRed/LibRed.Core/Formats/RelationshipFlags.cs +++ b/src/LibRed/LibRed.Core/Formats/RelationshipFlags.cs @@ -6,6 +6,11 @@ namespace LibRed.Formats; /// internal static class RelationshipFlags { + /// A one-to-one relationship (DAO's dbRelationUnique). Set by whoever creates the relationship + /// — Access's dialog when both sides are unique — and never by ACE's SQL; on an enforced relationship it makes + /// the child's backing index unique. + public const int OneToOne = 0x00000001; + /// Referential integrity is NOT enforced. public const int DontEnforce = 0x00000002; diff --git a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs index dc3c837b..c9012436 100644 --- a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs +++ b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs @@ -254,7 +254,7 @@ private void ReadLongValueMaps(PageBuffer buffer, int pos) private int ResolveIndexNames(PageBuffer buffer, int infoStart) { int logicalCount = LogicalIndexCount; // 0x2F — the logical-index (slot) count - var info = new (int DataNumber, bool IsRelationship, byte Type, byte FkType)[logicalCount]; + var info = new (int DataNumber, bool IsRelationship, byte Type, byte FkType, byte UpdateAction, byte DeleteAction)[logicalCount]; for (int i = 0; i < logicalCount; i++) { int block = infoStart + i * IndexBlockFormat.InfoBlockSize; @@ -262,7 +262,9 @@ private int ResolveIndexNames(PageBuffer buffer, int infoStart) buffer.ReadInt32(block + IndexBlockFormat.InfoDataNumberOffset), buffer.ReadInt32(block + IndexBlockFormat.InfoFkTablePageOffset) != 0, buffer.ReadByte(block + IndexBlockFormat.InfoTypeOffset), - buffer.ReadByte(block + IndexBlockFormat.InfoFkTypeOffset)); + buffer.ReadByte(block + IndexBlockFormat.InfoFkTypeOffset), + buffer.ReadByte(block + IndexBlockFormat.InfoUpdateActionOffset), + buffer.ReadByte(block + IndexBlockFormat.InfoDeleteActionOffset)); } int namePos = infoStart + logicalCount * IndexBlockFormat.InfoBlockSize; @@ -272,7 +274,7 @@ private int ResolveIndexNames(PageBuffer buffer, int infoStart) { (string name, namePos) = ReadName(buffer, namePos, $"logical index {i}"); - (int dataNumber, bool isRelationship, byte type, byte fkType) = info[i]; + (int dataNumber, bool isRelationship, byte type, byte fkType, byte updateAction, byte deleteAction) = info[i]; // Every logical index names the data block that holds its columns and root page. One that names a // block the table does not have is corruption; skipping it used to make the index vanish from the // table's schema while its B-tree stayed on disk, maintained by nobody. @@ -282,7 +284,7 @@ private int ResolveIndexNames(PageBuffer buffer, int infoStart) + $"{dataNumber}, and the table has {_indexes.Count}."); _logicalIndexes.Add(new LogicalIndexDef(name, dataNumber, isRelationship, - !isRelationship && type == IndexBlockFormat.TypePrimary, fkType)); + !isRelationship && type == IndexBlockFormat.TypePrimary, fkType, updateAction, deleteAction)); // Prefer a real index name over a relationship's; prefer the primary among real ones. int p = isRelationship ? 1 : type == IndexBlockFormat.TypePrimary ? 3 : 2; diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 294c93f3..107afda2 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -265,8 +265,9 @@ public void Create( RelationshipSpec fk = plan.Fk; if (fk.UpdateSetNull) throw UpdateSetNullNotImplemented(); - byte upd = fk.CascadeUpdate ? CascadeAction : NoCascadeAction; - byte del = fk.CascadeDelete ? CascadeAction : fk.DeleteSetNull ? SetNullAction : NoCascadeAction; + byte upd = fk.CascadeUpdate ? IndexBlockFormat.CascadeAction : IndexBlockFormat.NoCascadeAction; + byte del = fk.CascadeDelete ? IndexBlockFormat.CascadeAction + : fk.DeleteSetNull ? IndexBlockFormat.SetNullAction : IndexBlockFormat.NoCascadeAction; byte outgoingType = fk.NoIndex ? FkTypeOutgoingNoIndex : FkTypeOutgoing; // A self-referencing FK: the table is not in the catalog yet (we are creating it), so resolve @@ -393,12 +394,6 @@ private int AllocateIndexRoot(JetFormatBase format, int tdefPage, int mapRow, in /// version byte it needs — see . private const string CalculatedMinimumVersion = "14.0.0000.0000"; - // Index-info block field values (§3.6), verified against ACE-created relationships. - // (PlainAction and the index-type bytes are shared with the reader via IndexBlockFormat.) - private const byte NoCascadeAction = 0x00; // relationship without ON UPDATE/DELETE CASCADE - private const byte CascadeAction = 0x01; // relationship with cascade - private const byte SetNullAction = 0x02; // ON DELETE SET NULL (verified vs ACE, index-info block +0x16) - /// ON UPDATE SET NULL pathway: the docs list it, but the ACE OLE DB provider rejects it via SQL, /// so its on-disk storage (the grbit flag + the index-info +0x15 action byte) is unverified. Rather than /// guess the bytes, fail loudly until a UI/DAO-created sample can be probed. @@ -662,10 +657,9 @@ private int InsertIndex(TableDef table, string indexName, List<(int Id, bool Asc $"Cannot create index '{indexName}' on '{table.Name}' over {slots.Count} columns: " + $"an index holds at most {IndexBlockFormat.MaxColumns} fields."); - // Read the whole definition (stitching any existing continuation pages) so the surgical insert - // works in absolute coordinates; the old continuation pages are reused when we write it back. - (LibRed.IO.PageBuffer buf, IReadOnlyList existingContinuations) = ReadDefinition(table.DefinitionPage); - int existingRowCount = buf.ReadInt32(format.TdefRowCountOffset); + TdefParts parts = ParseTdef(table.DefinitionPage); // stitches continuation pages for a multi-page TDEF + var header = new LibRed.IO.PageBuffer(parts.Header, table.DefinitionPage); + int existingRowCount = header.ReadInt32(format.TdefRowCountOffset); // A unique index over rows that already exist has to be rejected if those rows aren't unique, and a // required one if any row leaves a key column NULL — ACE refuses the DDL for both. Done *here*, before a @@ -675,39 +669,19 @@ private int InsertIndex(TableDef table, string indexName, List<(int Id, bool Asc if ((unique || required) && existingRowCount != 0) EnsureExistingRowsFitIndex(table, indexName, slots, unique, required); - TdefRegions regions = TdefRegions.Of(buf.Span, format); - int dataCount = regions.DataCount; - int logicalCount = regions.LogicalCount; - int afterStats = regions.ColumnDescriptors; - int infoStart = regions.InfoBlocks; + int dataCount = parts.DataBlocks.Count; // A plain index or an outgoing FK adds one of each. Checked before a byte moves, like the // duplicate-key scan above, so a rejection leaves the file exactly as it was. - EnsureIndexCapacity(table.Name, $"index '{indexName}'", dataCount + 1, logicalCount + 1); + EnsureIndexCapacity(table.Name, $"index '{indexName}'", dataCount + 1, parts.Logical.Count + 1); - // Existing logical blocks and names; the new block takes the lowest free index_num. - int namePos = regions.IndexNames; - var blocks = new List(logicalCount + 1); - for (int i = 0; i < logicalCount; i++) - blocks.Add(buf.Slice(infoStart + i * IndexBlockFormat.InfoBlockSize, IndexBlockFormat.InfoBlockSize).ToArray()); + // The new block takes the lowest free index_num. int newNum = NextLogicalIndexNumber(table.DefinitionPage); - var names = new List(logicalCount + 1); - var nameBytes = new List(logicalCount + 1); - for (int i = 0; i < logicalCount; i++) - { - int len = buf.ReadUInt16(namePos); - nameBytes.Add(buf.Slice(namePos, 2 + len).ToArray()); - names.Add(System.Text.Encoding.Unicode.GetString(buf.Slice(namePos + 2, len))); - namePos += 2 + len; - } - - int defEnd = buf.ReadInt32(format.TdefLengthOffset); - byte[] lvalRegion = buf.Slice(namePos, defEnd - namePos).ToArray(); // §3.3.2 list + 0xFFFF terminator // Allocate the new index's root (empty leaf) and its usage-map row (appended after existing rows). int rootPage = _allocator.Allocate(); WriteEmptyLeafIndexPage(format, rootPage, owner: table.DefinitionPage); - int usageMapPage = ReadInt24(buf, format.TdefOwnedPagesOffset + 1); + int usageMapPage = ReadInt24(header, format.TdefOwnedPagesOffset + 1); // Where the new index's usage map goes: appended after the last row, as ACE does. A dropped index // leaves its row behind and ACE never hands it out again, so the row cannot be derived from the // table's shape — after a DROP INDEX that names a row already taken, on a table without long-value @@ -739,42 +713,15 @@ private int InsertIndex(TableDef table, string indexName, List<(int Id, bool Asc // page until it splits, after which IndexWriter adds each page it allocates). new UsageMapWriter(_channel).SetBit(newIndexUsageRow, usageMapPage, rootPage, set: true); - // Assemble the new definition: header + existing stats, a new stats block, columns + names + - // existing data blocks, the new data block, then the logical blocks (new one inserted, name-sorted) - // and their names, and finally the unchanged long-value region. - byte[] newData = BuildIndexDataBlock(slots, rootPage, newIndexUsageRow, usageMapPage, unique, required, ignoreNulls); - byte[] newInfo = buildInfo(newNum, dataCount); - - // Name-sorted insert position. ACE's order ignores case (verified: a3 goes before IX2, and an FK - // named fk before IX2); how it orders punctuation and accented letters is not measured. - int k = names.Count(n => string.Compare(n, indexName, StringComparison.OrdinalIgnoreCase) < 0); - blocks.Insert(k, newInfo); - nameBytes.Insert(k, EncodeName(indexName)); - - int newDefEnd = infoStart + IndexBlockFormat.DataBlockSize // one new data block shifts info start - + blocks.Count * IndexBlockFormat.InfoBlockSize + nameBytes.Sum(n => n.Length) + lvalRegion.Length - + format.RealIndexEntrySize; // one new stats block at the front - - // Build the full definition buffer (may exceed one page — split across continuation pages below). - var def = new byte[newDefEnd]; - var src = buf.Span; - int w = 0; - void Append(ReadOnlySpan s) { s.CopyTo(def.AsSpan(w)); w += s.Length; } - - Append(src[..afterStats]); // header + existing stats blocks - Append(new byte[format.RealIndexEntrySize]); // new (zero) stats block - Append(src[afterStats..infoStart]); // columns + names + existing data blocks - Append(newData); // new index-data block - foreach (byte[] b in blocks) Append(b); // logical blocks (new inserted, sorted) - foreach (byte[] n in nameBytes) Append(n); // their names, same order - Append(lvalRegion); // §3.3.2 list + terminator (unchanged) - - // Bump the two index counts and the definition length in the header. - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(def.AsSpan(format.TdefIndexCountOffset, 4), dataCount + 1); - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(def.AsSpan(format.TdefLogicalIndexCountOffset, 4), logicalCount + 1); - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(def.AsSpan(format.TdefLengthOffset, 4), newDefEnd); - - WriteDefinition(table.DefinitionPage, def, existingContinuations, rewrite: true); + // The new index adds a (zero) stats block and its data block after the existing ones, and a logical + // block in name order. ACE's order ignores case (verified: a3 goes before IX2, and an FK named fk before + // IX2); how it orders punctuation and accented letters is not measured. + parts.Stats.Add(new byte[format.RealIndexEntrySize]); + parts.DataBlocks.Add(BuildIndexDataBlock(slots, rootPage, newIndexUsageRow, usageMapPage, unique, required, ignoreNulls)); + int k = parts.Logical.Count(b => string.Compare(NameOf(b.Name), indexName, StringComparison.OrdinalIgnoreCase) < 0); + parts.Logical.Insert(k, (buildInfo(newNum, dataCount), EncodeName(indexName))); + + WriteTdef(table.DefinitionPage, parts); _catalog.Invalidate(); // Back-fill the new (empty) index B-tree with an entry per existing row, so the index is complete. @@ -962,8 +909,9 @@ public void AddForeignKey(string childTable, RelationshipSpec fk) return; } - byte upd = fk.CascadeUpdate ? CascadeAction : NoCascadeAction; - byte del = fk.CascadeDelete ? CascadeAction : fk.DeleteSetNull ? SetNullAction : NoCascadeAction; + byte upd = fk.CascadeUpdate ? IndexBlockFormat.CascadeAction : IndexBlockFormat.NoCascadeAction; + byte del = fk.CascadeDelete ? IndexBlockFormat.CascadeAction + : fk.DeleteSetNull ? IndexBlockFormat.SetNullAction : IndexBlockFormat.NoCascadeAction; var slots = ResolveSlots(child, fk.Columns.Select(c => (c.Column, Ascending: true))); // A self-reference (child == parent) hosts both ends in the same TDEF: the outgoing block takes the @@ -3515,59 +3463,23 @@ private static int ReadInt24(LibRed.IO.PageBuffer buf, int offset) => /// Adds an incoming-relationship logical index-info block (§3.6) to a parent table's already-written /// TDEF: it reuses the parent's referenced-key data block (no new data block), links back to the /// child's outgoing block, and grows the logical-index list by one (kept name-sorted). The definition is - /// rewritten through , so it may span or spill onto continuation pages. + /// rewritten through , so it may span or spill onto continuation pages. /// private void AddIncomingRelationshipBlock(IncomingRelationship inc) { - JetFormatBase format = _channel.Format; - (LibRed.IO.PageBuffer buf, IReadOnlyList existingContinuations) = ReadDefinition(inc.ParentPage); - - TdefRegions regions = TdefRegions.Of(buf.Span, format); - int dataCount = regions.DataCount; // 0x33 real data blocks - int logicalCount = regions.LogicalCount; // 0x2F logical blocks - int infoStart = regions.InfoBlocks; + TdefParts parts = ParseTdef(inc.ParentPage); // stitches continuation pages for a multi-page TDEF // An incoming relationship adds a logical block and no data block, so this is the path a referenced // table overruns: 0x33 stays where it was while 0x2F climbs with every table that points here. EnsureIndexCapacity( _catalog.Tables.FirstOrDefault(t => t.DefinitionPage == inc.ParentPage)?.Name ?? $"page {inc.ParentPage}", - "an incoming relationship", dataCount, logicalCount + 1); - - var blocks = new List(logicalCount + 1); - for (int i = 0; i < logicalCount; i++) - blocks.Add(buf.Slice(infoStart + i * IndexBlockFormat.InfoBlockSize, IndexBlockFormat.InfoBlockSize).ToArray()); - - int namePos = regions.IndexNames; - var names = new List(logicalCount + 1); - var nameBytes = new List(logicalCount + 1); - for (int i = 0; i < logicalCount; i++) - { - int len = buf.ReadUInt16(namePos); - nameBytes.Add(buf.Slice(namePos, 2 + len).ToArray()); - names.Add(System.Text.Encoding.Unicode.GetString(buf.Slice(namePos + 2, len))); - namePos += 2 + len; - } - - int defEnd = buf.ReadInt32(format.TdefLengthOffset); - byte[] lvalRegion = buf.Slice(namePos, defEnd - namePos).ToArray(); // §3.3.2 list + 0xFFFF terminator + "an incoming relationship", parts.DataBlocks.Count, parts.Logical.Count + 1); string newName = HiddenRelationshipName(inc.Number); - int k = names.Count(n => string.Compare(n, newName, StringComparison.OrdinalIgnoreCase) < 0); // name-sorted, ignoring case - blocks.Insert(k, BuildIncomingInfoBlock(inc)); - nameBytes.Insert(k, EncodeName(newName)); - - int newDefEnd = infoStart + blocks.Count * IndexBlockFormat.InfoBlockSize + nameBytes.Sum(n => n.Length) + lvalRegion.Length; - - var def = new byte[newDefEnd]; - buf.Span[..infoStart].CopyTo(def); - int w = infoStart; - foreach (byte[] b in blocks) { b.CopyTo(def.AsSpan(w)); w += b.Length; } - foreach (byte[] n in nameBytes) { n.CopyTo(def.AsSpan(w)); w += n.Length; } - lvalRegion.CopyTo(def.AsSpan(w)); - - BinaryPrimitives.WriteInt32LittleEndian(def.AsSpan(format.TdefLogicalIndexCountOffset, 4), logicalCount + 1); - BinaryPrimitives.WriteInt32LittleEndian(def.AsSpan(format.TdefLengthOffset, 4), newDefEnd); - WriteDefinition(inc.ParentPage, def, existingContinuations, rewrite: true); + int k = parts.Logical.Count(b => string.Compare(NameOf(b.Name), newName, StringComparison.OrdinalIgnoreCase) < 0); // name-sorted, ignoring case + parts.Logical.Insert(k, (BuildIncomingInfoBlock(inc), EncodeName(newName))); + + WriteTdef(inc.ParentPage, parts); } private static byte[] BuildIncomingInfoBlock(IncomingRelationship inc) diff --git a/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedDatabaseModelFactory.cs b/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedDatabaseModelFactory.cs index e40671b7..5a900f55 100644 --- a/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedDatabaseModelFactory.cs +++ b/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedDatabaseModelFactory.cs @@ -235,8 +235,8 @@ private void GetRelations(JetDatabase database, IReadOnlyList tab DatabaseTable? referencingTable = Find(relation.Table); if (referencingTable is null) continue; - // Jet supports ON DELETE NO ACTION / CASCADE / SET NULL (read from MSysRelationships.grbit + - // the index-info action byte). EF's scaffolding models OnDelete only (no OnUpdate). + // Jet supports ON DELETE NO ACTION / CASCADE / SET NULL (read from the relationship's index-info action + // byte, which is what ACE acts on). EF's scaffolding models OnDelete only (no OnUpdate). ReferentialAction onDeleteAction = relation.CascadeDelete ? ReferentialAction.Cascade : relation.DeleteSetNull ? ReferentialAction.SetNull : ReferentialAction.NoAction; diff --git a/src/LibRed/LibRed.Engine/Schema/InformationSchema.cs b/src/LibRed/LibRed.Engine/Schema/InformationSchema.cs index 94c7106f..a085f7fa 100644 --- a/src/LibRed/LibRed.Engine/Schema/InformationSchema.cs +++ b/src/LibRed/LibRed.Engine/Schema/InformationSchema.cs @@ -120,11 +120,9 @@ public static bool IsInformationSchema(string tableName) => case "RELATIONS": foreach (ForeignKey fk in catalog.Relationships) // RELATION_TYPE: EFCore.Jet's live provider is PreciseSchema, which overrides ADOX's value - // with DAO's GetRelationTypes — "ONE" for a one-to-one relationship, else "MANY". This is - // NOT a grbit flag (MSysRelationships.grbit only carries enforce/cascade/set-null); Access - // encodes one-to-one by making the child-side FK backing index UNIQUE (a 1:many child index - // is non-unique). So derive it from that index's uniqueness, which LibRed already models. - rows.Add([fk.Name, fk.Table, fk.ReferencedTable, RelationType(fk, catalog), + // with DAO's GetRelationTypes — "ONE" when the relation's attributes carry dbRelationUnique, + // which is grbit 0x01, else "MANY". + rows.Add([fk.Name, fk.Table, fk.ReferencedTable, fk.IsOneToOne ? "ONE" : "MANY", RefAction(fk.CascadeDelete, fk.DeleteSetNull), RefAction(fk.CascadeUpdate, fk.UpdateSetNull), fk.IsEnforced, fk.IsInherited]); break; @@ -156,18 +154,4 @@ private static string TableType(TableDef t) => private static string IndexType(IndexDef ix) => ix.IsPrimaryKey ? "PRIMARY" : ix.IsUnique ? "UNIQUE" : "INDEX"; private static string RefAction(bool cascade, bool setNull) => cascade ? "CASCADE" : setNull ? "SET NULL" : "NO ACTION"; - - /// RELATION_TYPE — "ONE" for a one-to-one relationship, else "MANY", as DAO reports it. A - /// relationship is one-to-one when the child (referencing) table's backing index over the FK columns is - /// UNIQUE; a one-to-many child index is non-unique (see system-catalog.md). Defaults to "MANY" when no - /// matching child index is found (e.g. an unenforced FK that Access left unindexed). - private static string RelationType(ForeignKey fk, JetCatalog catalog) - { - TableDef? child = catalog.Tables.FirstOrDefault(t => t.Name.Equals(fk.Table, StringComparison.OrdinalIgnoreCase)); - var fkColumns = fk.Columns.Select(c => c.Column).ToHashSet(StringComparer.OrdinalIgnoreCase); - IndexDef? backing = child?.Indexes.FirstOrDefault(ix => - ix.Columns.Count == fkColumns.Count - && ix.Columns.All(c => fkColumns.Contains(c.Column.Name))); - return backing?.IsUnique == true ? "ONE" : "MANY"; - } } \ No newline at end of file diff --git a/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs b/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs index edfc7898..3c6e2206 100644 --- a/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs +++ b/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs @@ -1,4 +1,5 @@ using LibRed.Catalog; +using LibRed.Storage; namespace LibRed.Engine.Schema; @@ -148,7 +149,7 @@ public static bool IsKnown(string collection) => rows.Add([null, null, t.Name, type, null, null, null, null, null]); foreach (string name in ViewNames(catalog)) rows.Add([null, null, name, "VIEW", null, null, null, null, null]); - rows.Sort(ByName(2)); + rows.Sort(ByName(database, 2)); break; case "Columns": @@ -226,7 +227,7 @@ public static bool IsKnown(string collection) => case "Views": foreach (string name in ViewNames(catalog)) rows.Add([null, null, name, catalog.Views[name], null, true, null, null, null]); - rows.Sort(ByName(2)); + rows.Sort(ByName(database, 2)); break; case "Procedures": @@ -238,7 +239,7 @@ public static bool IsKnown(string collection) => rows.Add([null, null, name, ProcedureTypeReturnsRows, sql, null, null, null]); foreach ((string name, StoredActionQuery query) in catalog.ActionQueries) rows.Add([null, null, name, ProcedureTypeReturnsRows, query.Sql, null, null, null]); - rows.Sort(ByName(2)); + rows.Sort(ByName(database, 2)); break; case "ForeignKeys": @@ -264,8 +265,10 @@ public static bool IsKnown(string collection) => break; case "TableConstraints": + // ACE orders the rows by constraint name, then — for the many keys Access names alike — by table. foreach ((TableDef table, string name, string kind, IndexDef? _, ForeignKey? __) in Constraints(catalog)) rows.Add([null, null, name, null, null, table.Name, kind, false, false, null]); + rows.Sort(ByName(database, 2, thenBy: 5)); break; case "KeyColumnUsage": @@ -318,7 +321,7 @@ public static bool IsKnown(string collection) => foreach (TableDef t in catalog.Tables) if (TableType(t) is not null) rows.Add([null, null, t.Name, (decimal)t.RowCount]); - rows.Sort(ByName(2)); + rows.Sort(ByName(database, 2)); break; case "ProcedureParameters": @@ -342,7 +345,7 @@ public static bool IsKnown(string collection) => p.Type is { } named ? ProviderTypeName(named) : null, p.Type is { } local ? ProviderTypeName(local) : null]); } - rows.Sort(ByName(2)); + rows.Sort(ByName(database, 2)); break; case "ViewColumns": @@ -550,8 +553,23 @@ private static Dictionary ColumnOwners(JetCatalog catalog) private static IEnumerable ViewNames(JetCatalog catalog) => catalog.Views.Keys.Where(name => !catalog.QueryParameters.ContainsKey(name)); - private static Comparison ByName(int column) => - (left, right) => string.Compare((string?)left[column], (string?)right[column], StringComparison.OrdinalIgnoreCase); + /// Orders rows by the name in , then by , as ACE does: in + /// the database's collation — Order_Details before Orders, an underscore sorting before letters — not + /// ordinally. A database whose collation LibRed cannot encode keeps the ordinal, case-insensitive order rather + /// than failing to list its schema. + private static Comparison ByName(JetDatabase database, int column, int? thenBy = null) + { + Func compare = database.Collation.IsIndexKeyEncodable + ? JetTextComparer.For(database.Collation).Compare + : (a, b) => string.Compare(a, b, StringComparison.OrdinalIgnoreCase); + return (left, right) => + { + int byName = compare((string?)left[column] ?? "", (string?)right[column] ?? ""); + return byName != 0 || thenBy is not int next + ? byName + : compare((string?)left[next] ?? "", (string?)right[next] ?? ""); + }; + } /// The OLE DB type code ACE reports for a column. A memo and a text column share one code, as do an /// OLE and a binary column; what separates them is the long-value flag in . diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index 6d538dc5..00a052de 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -166,7 +166,7 @@ descriptor. | `0x23` | 3 | Usage-map page | | `0x26` | 4 | B-tree root page | | `0x2A` | 4 | Unknown / reserved (zero) | -| `0x2E` | 2 | Flags: `0x01` unique, `0x02` ignore-nulls, `0x08` required, `0x80` always-set | +| `0x2E` | 2 | Flags: `0x01` unique (on a foreign key's child block: the relationship is one-to-one), `0x02` ignore-nulls, `0x08` required, `0x80` always-set | | `0x30` | 4 | Unknown / reserved (zero) — trailing bytes of the 52-byte block | ## Index-info block — 28 bytes, one per logical index → [page-02d](page-02d-constraints.md) @@ -259,7 +259,7 @@ Global maps, located by page 0: free pages at `0x18` (page 1 row 0 as ACE writes - **MSysObjects** (TDEF page 2): `Id`, `Name`, `Type` (1 table, 5 query/view), `Flags`, `ParentId`, `Owner`, `DateCreate`/`DateUpdate`, `LvProp` (property blob). - **MSysACEs** (4 cols): `ObjectId`, `SID`, `ACM`, `FInheritable` — two rows per object. - **MSysQueries** (8 cols): `ObjectId`, `Attribute`, `Flag`, `Name1`, `Name2`, `Expression`, `Order`, `LvExtra`; PK `(ObjectId, Attribute, Order)`. -- **MSysRelationships**: `szRelationship`, `szObject`, `szColumn`, `szReferencedObject`, `szReferencedColumn`, `icolumn`, `ccolumn`, `grbit` (`0x02` don't-enforce, `0x100` cascade-update, `0x1000` cascade-delete, `0x2000` delete-set-null). +- **MSysRelationships**: `szRelationship`, `szObject`, `szColumn`, `szReferencedObject`, `szReferencedColumn`, `icolumn`, `ccolumn`, `grbit` (`0x01` one-to-one, `0x02` don't-enforce, `0x100` cascade-update, `0x1000` cascade-delete, `0x2000` delete-set-null, `0x1000000`/`0x2000000` join type: all records from the parent / from the child). The cascades ACE applies come from the index-info blocks' `0x15`/`0x16`, not from these bits. - **LvProp blob:** `MR2\0` signature, then `[int len][short type][body]` blocks; type `0x80` = name pool; value-block type `0x00` = table, `0x01` = column (`0x02` = index, unverified); per-owner entries are `[short entryLen][byte flags][byte dataType][short nameIndex][short valueLen][value]`. The flag byte is a bit field — `0x01` marks a definition-protected/schema property, and `0x80` also occurs in Access-written files — preserved whole per entry. Values include `DefaultValue`, `Required`, and `CheckConstraints`. --- diff --git a/src/LibRed/docs/format/page-02d-constraints.md b/src/LibRed/docs/format/page-02d-constraints.md index b16c4e6d..9f4f7fb5 100644 --- a/src/LibRed/docs/format/page-02d-constraints.md +++ b/src/LibRed/docs/format/page-02d-constraints.md @@ -91,7 +91,10 @@ differently from a delete: | `0x23` | 3 | Usage-map page | | `0x26` | 4 | **B-tree root page** | | `0x2A` | 4 | Unknown / reserved — zero in every file checked. mdbtools calls it uninitialised page residue (unverified) | -| `0x2E` | 2 | Flags: `0x01` unique, `0x02` ignore-nulls (`WITH IGNORE NULL` — null-keyed rows excluded from the index), `0x08` required (`WITH DISALLOW NULL` / part of a primary key), `0x80` always-set (Access 2000+). Verified vs ACE: a plain index is `0x0080`, `IGNORE NULL` `0x0082`, `DISALLOW NULL` `0x0088`, a PK `0x0089`. There is **no clustered flag**: `CLUSTERED`/`NONCLUSTERED` after `PRIMARY KEY` or `UNIQUE` in a constraint is accepted and stores nothing (the file is byte-identical without it), and DAO's `Index.Clustered` reads `False` even on an index created with it set. | +| `0x2E` | 2 | Flags: `0x01` unique, `0x02` ignore-nulls (`WITH IGNORE NULL` — null-keyed rows excluded from the index), `0x08` required (`WITH DISALLOW NULL` / part of a primary key), `0x80` always-set (Access 2000+). Verified vs ACE: a plain index is `0x0080`, `IGNORE NULL` `0x0082`, `DISALLOW NULL` `0x0088`, a PK `0x0089`. A foreign key's child block (§3.6) carries `0x01` exactly when the relationship is one-to-one +(`MSysRelationships.grbit` `0x01`), and **this bit, not `grbit`, is what ACE enforces**: made to disagree, a +child index with the bit refuses a second child row with the same key although `grbit` says one-to-many, and +one without it accepts the duplicate although `grbit` says one-to-one (verified vs ACE). There is **no clustered flag**: `CLUSTERED`/`NONCLUSTERED` after `PRIMARY KEY` or `UNIQUE` in a constraint is accepted and stores nothing (the file is byte-identical without it), and DAO's `Index.Clustered` reads `False` even on an index created with it set. | | `0x30` | 4 | Unknown / reserved (zero observed) — trailing bytes of the 52-byte block | > **The 10-column cap must be enforced on the incremental path too**, as must the 32-index cap below. @@ -155,8 +158,9 @@ something *references* it. So the budget is: 0x2F = 0x33 + one entry per INCOMING relationship (this table as the referenced end) ``` -The child side of a foreign key is a real index and costs one from each count; only the parent end is free -of storage, reusing the index already over the referenced columns. So being *referenced* is what spends the +The child side of a foreign key is a real index and costs one from each count — its **own**, even when a +primary key or unique index already covers exactly those columns; only the parent end is free of storage, +reusing the index already over the referenced columns. So being *referenced* is what spends the budget invisibly. A self-reference lands both ends on one table: one data block, two logical blocks. Because a data block must be named by a logical block, `0x33 ≤ 0x2F` always holds — which makes `0x2F` the @@ -193,7 +197,7 @@ isolation: 33 plain indexes push both counts to 33 together. | `0x0C` | 1 | Foreign-key index type: `0x00` = none, `0x01` = **incoming** (this table is the parent/referenced end), `0x02` = **outgoing** (this table is the child/referencing end), `0x03` = **outgoing, `FOREIGN KEY NO INDEX`** (verified vs ACE: identical to `0x02` — same data block and same parent incoming `0x01` block — only this type byte differs) | | `0x0D` | 4 | Foreign-key index number: the `index_num` (`0x04`) of the **matching logical block on the other table**; `0xFFFFFFFF` when not a relationship | | `0x11` | 4 | Foreign-key table page (the *other* table's TDEF page; non-zero ⇒ a relationship index) | -| `0x15` | 1 | Update action: `0x04` plain index; on a relationship `0x00` = no cascade, `0x01` = cascade update | +| `0x15` | 1 | Update action: `0x04` plain index; on a relationship `0x00` = no cascade, `0x01` = cascade update. With `0x16`, what ACE acts on — not `MSysRelationships.grbit` (verified vs ACE: made to disagree, ACE follows these bytes) | | `0x16` | 1 | Delete action: `0x04` plain index; on a relationship `0x00` = no cascade, `0x01` = cascade delete, `0x02` = **`ON DELETE SET NULL`** (verified vs ACE) | | `0x17` | 1 | Index type: `0x00` = plain secondary, `0x01` = primary, `0x02` = foreign/relationship | | `0x18` | 4 | Unknown / reserved (zero observed) — trailing bytes of the 28-byte block | @@ -205,6 +209,11 @@ physical (data-block) index, prefer a real index's name over a foreign-key relat > **Writing a relationship's logical blocks (verified byte-for-byte vs ACE).** The **child** (referencing) > table gives its FK-column index a *single* logical block that **is** the relationship: `index_num2` → the FK-column data > block, `0x0C = 0x02` (outgoing), `0x11` = parent page, `0x17 = 0x02`, name = the constraint name. +> That FK-column data block is a real index of the relationship's own, never shared with a primary key or +> unique index over the same columns, and it is **non-unique unless the relationship is one-to-one** +> (`MSysRelationships.grbit` `0x01`, which SQL never sets): then its unique flag (§3.5, `0x2E` bit `0x01`) +> is set, and ACE refuses a second child row with the same key (verified vs ACE, on a child primary-key column +> and on a plain one). > The **parent** (referenced) table gains an **extra** logical block beyond its data blocks: > `index_num2` → its referenced-key (PK) data block, `0x0C = 0x01` (incoming), `0x11` = child page, > `0x17 = 0x02`, name = the hidden name **`.r` followed by the letter `'A' + index_num`** — `.rB` for block 1, diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index a952b7ec..63a34d7a 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -535,15 +535,30 @@ - **MSysRelationships** defines foreign keys (one row per relationship column): `szRelationship` (name), `szObject` (child/referencing table), `szColumn` (child column), `szReferencedObject` (parent table), `szReferencedColumn`, `icolumn` (0-based column order within the key), - `ccolumn` (total column count of the key, repeated on every row), `grbit` (flags: `0x02` - don't-enforce, `0x100` cascade-update, `0x1000` cascade-delete, `0x2000` delete-set-null). Verified: an - enforced, no-cascade single-column FK stores `ccolumn = 1`, `icolumn = 0`, `grbit = 0`; a relationship - cascading both update and delete stores `grbit = 0x1100`. + `ccolumn` (total column count of the key, repeated on every row), `grbit` (flags: `0x01` one-to-one, + `0x02` don't-enforce, `0x100` cascade-update, `0x1000` cascade-delete, `0x2000` delete-set-null, + `0x1000000` join type "all records from the parent" (`szReferencedObject`), `0x2000000` join type "all + records from the child" (`szObject`); an inner join sets neither). Verified: an enforced, no-cascade + single-column FK stores `ccolumn = 1`, `icolumn = 0`, `grbit = 0`; a relationship cascading both update + and delete stores `grbit = 0x1100`; Access's relationship dialog writes each bit above as its option says. + + > **`grbit` records the relationship; ACE does not act on its cascade bits.** The update and delete actions + > ACE applies are the ones in the relationship's index-info blocks (§3.6, `0x15`/`0x16`): with the two made + > to disagree, a parent delete that `grbit` calls cascading but the blocks do not is refused, and one the + > blocks call cascading but `grbit` does not cascades (verified vs ACE). + > + > **`0x01` is set by whoever creates the relationship; ACE never derives it.** A SQL `FOREIGN KEY` stores + > `0x00` even from one primary key to another, and DAO's `CreateRelation` stores exactly the attributes it + > is given, `0x01` included on a child column that is not unique. Access's dialog sets it when both sides + > are unique. On an **enforced** relationship the child's backing index is created **unique** with it, and + > non-unique without it even when the child column is the table's primary key. What ACE then enforces is + > that index's unique flag (page-02d §3.5, `0x2E`), not this bit: made to disagree, ACE follows the flag + > (verified vs ACE). The bit is what Access and DAO report as the relationship's type. > **Writing a relationship.** An **enforced** relationship is `MSysRelationships` rows, an `MSysObjects` > row of its own (`Type` = 8, under the relationship container, with the `MSysACEs` rows that container - > grants, §11) **and** a non-unique index on the - > child table's FK column(s) — enforcement requires the child FK to be indexed and the parent key to be + > grants, §11) **and** an index of its own on the child table's FK column(s), non-unique unless the + > relationship is one-to-one (`0x01`) — enforcement requires the child FK to be indexed and the parent key to be > uniquely indexed (the parent PK). LibRed writes all of it, including the byte-faithful relationship > logical-index linkage in *both* tables' TDEFs (§3.6: outgoing block on the child, incoming block on the > parent, cross-referenced by `index_num`) at `CREATE TABLE` time. diff --git a/test/LibRed.Ado.Tests/LibRedSchemaTests.cs b/test/LibRed.Ado.Tests/LibRedSchemaTests.cs index e1a76ef7..d38d5ef4 100644 --- a/test/LibRed.Ado.Tests/LibRedSchemaTests.cs +++ b/test/LibRed.Ado.Tests/LibRedSchemaTests.cs @@ -225,6 +225,40 @@ public void DataTypes_names_every_type_the_file_can_hold() Assert.Equal(135, types["DateTime2"]); } + // ACE lists names in the database's collation, not ordinally: an apostrophe or hyphen counts only after the + // letters, case is ignored, digits compare one at a time. The expected order is ACE's own, over these names. + [Fact] + public void Tables_are_ordered_in_the_database_collation() + { + string path = TemporaryDatabase.CopyPath(Northwind, "collation-order-"); + using var connection = new LibRedConnection($"Data Source={path}"); + connection.Open(); + string[] names = ["Order_X", "OrderT", "Order-Y", "OrderY", "O'Brien", "OBrien", "Order2", "Order10", "Ölmühle", "Olive", "order_lower"]; + foreach (string name in names) + { + using DbCommand create = connection.CreateCommand(); + create.CommandText = $"CREATE TABLE [{name}] (ID LONG PRIMARY KEY)"; + create.ExecuteNonQuery(); + } + + List listed = [.. connection.GetSchema("Tables").Rows.Cast() + .Select(r => (string)r["TABLE_NAME"]).Where(names.Contains)]; + Assert.Equal( + ["OBrien", "O'Brien", "Olive", "Ölmühle", "order_lower", "Order_X", "Order10", "Order2", "OrderT", "OrderY", "Order-Y"], + listed); + } + + // Access names many keys alike, and ACE breaks those ties by table name. + [Fact] + public void TableConstraints_sharing_a_name_are_ordered_by_table() + { + List tables = [.. Schema("TableConstraints").Rows.Cast() + .Where(r => (string)r["CONSTRAINT_NAME"] == "Id").Select(r => (string)r["TABLE_NAME"])]; + Assert.Equal( + ["MSysAccessStorage", "MSysNavPaneGroupCategories", "MSysNavPaneGroups", "MSysNavPaneGroupToObjects", "MSysResources"], + tables); + } + // A type's CreateFormat declares a column the Columns collection reports as that same type. ACE leaves the // field empty for every type. [Fact] diff --git a/test/LibRed.Core.Tests/CatalogDateUpdateTests.cs b/test/LibRed.Core.Tests/CatalogDateUpdateTests.cs new file mode 100644 index 00000000..0f0bffe0 --- /dev/null +++ b/test/LibRed.Core.Tests/CatalogDateUpdateTests.cs @@ -0,0 +1,68 @@ +using LibRed; +using LibRed.Catalog; +using Xunit; + +namespace LibRed.Core.Tests; + +/// +/// A table's MSysObjects.DateUpdate moves whenever its definition changes, and its DateCreate never +/// does — as ACE does it, measured against ACE: an index created on the table, and a new table referencing it, both +/// move the table's DateUpdate. +/// +public class CatalogDateUpdateTests +{ + [Fact] + public void Creating_an_index_moves_the_tables_date_update() + { + string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "dateupd-ix-"); + try + { + using var db = JetDatabase.Open(path, readOnly: false); + db.CreateTable("T", [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), + new ColumnSpec("X", JetDataType.Int32, 4, IsFixedLength: true)], primaryKey: ["Id"]); + (DateTime created, DateTime updated) before = Dates(db, "T"); + + Thread.Sleep(50); + db.CreateIndex("T", "ix", [("X", false)]); + + (DateTime created, DateTime updated) after = Dates(db, "T"); + Assert.Equal(before.created, after.created); + Assert.True(after.updated > before.updated, $"DateUpdate stayed at {before.updated:HH:mm:ss.fff}"); + } + finally { TemporaryDatabase.Delete(path); } + } + + [Fact] + public void A_table_referencing_another_moves_the_parents_date_update() + { + string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "dateupd-fk-"); + try + { + using var db = JetDatabase.Open(path, readOnly: false); + db.CreateTable("P", [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true)], primaryKey: ["Id"]); + (DateTime created, DateTime updated) before = Dates(db, "P"); + + Thread.Sleep(50); + db.CreateTable("C", + [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), + new ColumnSpec("Pid", JetDataType.Int32, 4, IsFixedLength: true)], + primaryKey: ["Id"], + relationships: [new RelationshipSpec("fk", "P", [("Pid", "Id")], true, CascadeUpdate: false, CascadeDelete: false)]); + + (DateTime created, DateTime updated) after = Dates(db, "P"); + Assert.Equal(before.created, after.created); + Assert.True(after.updated > before.updated, $"DateUpdate stayed at {before.updated:HH:mm:ss.fff}"); + } + finally { TemporaryDatabase.Delete(path); } + } + + private static (DateTime Created, DateTime Updated) Dates(JetDatabase db, string name) + { + db.Catalog.Invalidate(); + var objects = db.OpenTable("MSysObjects"); + var def = objects.Definition; + int Col(string n) => def.FindColumn(n)!.Index; + object?[] row = objects.Rows().Single(r => (string?)r[Col("Name")] == name && (short)r[Col("Type")]! == 1); + return ((DateTime)row[Col("DateCreate")]!, (DateTime)row[Col("DateUpdate")]!); + } +} diff --git a/test/LibRed.Core.Tests/ForeignKeyLinkageTests.cs b/test/LibRed.Core.Tests/ForeignKeyLinkageTests.cs index 4b092a81..808f76a6 100644 --- a/test/LibRed.Core.Tests/ForeignKeyLinkageTests.cs +++ b/test/LibRed.Core.Tests/ForeignKeyLinkageTests.cs @@ -169,7 +169,75 @@ [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), Block incoming = parent.Single(b => b.FkType == 0x01); Assert.Equal(0x01, incoming.Upd); Assert.Equal(0x01, incoming.Del); + + // And the catalog reads them back, on both ends. + var catalog = new JetCatalog(ch); + LogicalIndexDef childEnd = catalog.FindTable("C2")!.LogicalIndexes.Single(l => l.Name == "FKcas"); + LogicalIndexDef parentEnd = catalog.FindTable("P2")!.LogicalIndexes.Single(l => l.IsIncomingRelationship); + Assert.Equal((0x01, 0x01), (childEnd.UpdateAction, childEnd.DeleteAction)); + Assert.Equal((0x01, 0x01), (parentEnd.UpdateAction, parentEnd.DeleteAction)); + Assert.Equal((0x04, 0x04), (catalog.FindTable("C2")!.LogicalIndexes.Single(l => l.IsPrimaryKey).UpdateAction, + catalog.FindTable("C2")!.LogicalIndexes.Single(l => l.IsPrimaryKey).DeleteAction)); } finally { TemporaryDatabase.Delete(path); } } + + // ACE cascades by the child's relationship block, not by MSysRelationships.grbit: made to disagree, a delete + // grbit calls cascading is refused and one grbit calls plain cascades (measured against ACE). So when the two + // disagree, the relationship LibRed reads must carry the block's actions. + [Fact] + public void Cascade_actions_are_read_from_the_relationship_block_not_grbit() + { + string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "fkblock-"); + try + { + using (var db = JetDatabase.Open(path, readOnly: false)) + { + db.CreateTable("P4", [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true)], primaryKey: ["Id"]); + db.CreateTable("C4", + [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), + new ColumnSpec("Pid", JetDataType.Int32, 4, IsFixedLength: true)], + primaryKey: ["Id"], + relationships: [new RelationshipSpec("FKblock", "P4", [("Pid", "Id")], true, CascadeUpdate: true, CascadeDelete: true)]); + } + + // grbit keeps cascade update and delete; the child's block now says no update action and SET NULL. + using (var ch = PageChannel.Open(path, readOnly: false)) + { + int page = new JetCatalog(ch).FindTable("C4")!.DefinitionPage; + byte[] buf = ch.ReadPage(page).Span.ToArray(); + int block = LogicalBlockOffset(ch, new PageBuffer(buf, page), "FKblock"); + buf[block + 0x15] = 0x00; + buf[block + 0x16] = 0x02; + ch.WritePage(page, buf); + } + + using var reopened = JetDatabase.Open(path); + ForeignKey fk = reopened.Catalog.Relationships.Single(r => r.Name == "FKblock"); + Assert.False(fk.CascadeUpdate); + Assert.False(fk.CascadeDelete); + Assert.True(fk.DeleteSetNull); + } + finally { TemporaryDatabase.Delete(path); } + } + + private static int LogicalBlockOffset(PageChannel ch, PageBuffer buf, string name) + { + var fmt = ch.Format; + int dataCount = buf.ReadInt32(fmt.TdefIndexCountOffset); + int logicalCount = buf.ReadInt32(fmt.TdefLogicalIndexCountOffset); + int cols = buf.ReadUInt16(fmt.TdefColumnCountOffset); + int pos = fmt.TdefRealIndexBlockOffset + dataCount * fmt.RealIndexEntrySize + cols * fmt.ColumnDescriptorSize; + for (int i = 0; i < cols; i++) pos += 2 + buf.ReadUInt16(pos); + int infoStart = pos + dataCount * 52; + int namePos = infoStart + logicalCount * 28; + for (int i = 0; i < logicalCount; i++) + { + int len = buf.ReadUInt16(namePos); + string blockName = System.Text.Encoding.Unicode.GetString(buf.Slice(namePos + 2, len)); + if (blockName == name) return infoStart + i * 28; + namePos += 2 + len; + } + throw new InvalidOperationException($"No logical index named {name}."); + } } diff --git a/test/LibRed.Engine.AccessTests/RelationTypeAccessTests.cs b/test/LibRed.Engine.AccessTests/RelationTypeAccessTests.cs new file mode 100644 index 00000000..f8615c5e --- /dev/null +++ b/test/LibRed.Engine.AccessTests/RelationTypeAccessTests.cs @@ -0,0 +1,62 @@ +using System.Reflection; +using LibRed; +using LibRed.Engine; +using Xunit; + +namespace LibRed.Engine.Tests; + +// A one-to-one relationship is the grbit bit 0x01, which only Access's dialog and DAO set (SQL cannot). LibRed +// reports it as DAO does — RELATION_TYPE "ONE" — whether or not the relationship is enforced; an unenforced one +// has no child index for anything to be inferred from. +[Collection(AceCollection.Name)] +public class RelationTypeAccessTests +{ + private const int UseJet = 2, Ace12 = 128, OneToOne = 1, DontEnforce = 2; + + [Fact] + public void A_one_to_one_created_by_dao_reads_as_one_enforced_or_not() + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not available in this process."); + object workspace = Invoke(engine!, "CreateWorkspace", "", "admin", "", UseJet)!; + + string path = TemporaryDatabase.CreatePath("reltype-", ".accdb"); + try + { + object database = Invoke(workspace, "CreateDatabase", path, ";LANGID=0x0409;CP=1252;COUNTRY=0", Ace12)!; + foreach (string sql in (string[]) + ["CREATE TABLE P (ID LONG PRIMARY KEY)", "CREATE TABLE E (ID LONG PRIMARY KEY)", "CREATE TABLE U (ID LONG PRIMARY KEY)"]) + Invoke(database, "Execute", sql, 128); + Relate(database, "enforced_one", "E", OneToOne); + Relate(database, "unenforced_one", "U", OneToOne | DontEnforce); + Invoke(database, "Close"); + + using var db = JetDatabase.Open(path, readOnly: true); + Assert.True(db.Catalog.Relationships.Single(r => r.Name == "enforced_one").IsOneToOne); + Assert.True(db.Catalog.Relationships.Single(r => r.Name == "unenforced_one").IsOneToOne); + + var types = new QueryEngine(db) + .ExecuteQuery("SELECT `RELATION_NAME`, `RELATION_TYPE` FROM `INFORMATION_SCHEMA.RELATIONS`") + .Rows.ToDictionary(r => (string)r[0]!, r => (string)r[1]!); + Assert.Equal("ONE", types["enforced_one"]); + Assert.Equal("ONE", types["unenforced_one"]); + } + finally { TemporaryDatabase.Delete(path); } + } + + // DAO: CreateRelation(name, primary table, foreign table, attributes); Field.Name = primary field, + // Field.ForeignName = foreign field. + private static void Relate(object database, string name, string child, int attributes) + { + object relation = Invoke(database, "CreateRelation", name, "P", child, attributes)!; + object field = Invoke(relation, "CreateField", "ID")!; + field.GetType().InvokeMember("ForeignName", BindingFlags.SetProperty, null, field, ["ID"]); + object fields = relation.GetType().InvokeMember("Fields", BindingFlags.GetProperty, null, relation, null)!; + Invoke(fields, "Append", field); + object relations = database.GetType().InvokeMember("Relations", BindingFlags.GetProperty, null, database, null)!; + Invoke(relations, "Append", relation); + } + + private static object? Invoke(object target, string member, params object?[] args) => + target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); +} diff --git a/test/LibRed.Engine.Tests/InformationSchemaTests.cs b/test/LibRed.Engine.Tests/InformationSchemaTests.cs index f6abad6d..047baa5e 100644 --- a/test/LibRed.Engine.Tests/InformationSchemaTests.cs +++ b/test/LibRed.Engine.Tests/InformationSchemaTests.cs @@ -26,6 +26,17 @@ public void Tables_view_lists_user_tables_and_filters_by_name() Assert.Equal(0, miss); } + // RELATION_TYPE is grbit's one-to-one bit, which SQL never sets — not the uniqueness of whatever index covers + // the child columns. A foreign key from one primary key to another is still one-to-many. + [Fact] + public void Relations_view_reports_a_sql_foreign_key_between_primary_keys_as_many() + { + var e = Seeded(); + e.ExecuteNonQuery("CREATE TABLE Part (Id LONG PRIMARY KEY, CONSTRAINT fk_part FOREIGN KEY (Id) REFERENCES Widget (Id))"); + var row = e.ExecuteQuery("SELECT `RELATION_TYPE` FROM `INFORMATION_SCHEMA.RELATIONS` WHERE `RELATION_NAME` = 'fk_part'").Rows.Single(); + Assert.Equal("MANY", row[0]); + } + [Fact] public void Columns_view_lists_columns() { From 1cd4a6b5cd085e434450b39b62eef06695a492d5 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 05:32:39 +0800 Subject: [PATCH 55/88] Read no collation from a complex column, and name the complex-column table flag A complex column's descriptor bytes 0x0B/0x0C hold its MSysComplexColumns key, as page-02b already said, but the reader decoded them as a collation for every column but a decimal one: an attachment column with ComplexID 1 came back in LCID 1. It now reads no collation from a complex column, as from a decimal. Nothing took the key from there - the catalog finds it by name, and a rebuild keeps the raw bytes. MSysObjects.Flags 0x00040000 is not the plain user-table bit the spec called it. It is set on exactly the tables that own a complex column, system ones included (MSysResources carries 0x0004000A), and on no other - checked across every table in a corpus of Access-written files. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/ColumnDef.cs | 3 ++- src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs | 8 +++++--- src/LibRed/docs/format/system-catalog.md | 12 ++++++++---- test/LibRed.Core.Tests/CollationTests.cs | 11 +++++++++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs b/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs index d8cfff31..fd8f7618 100644 --- a/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs +++ b/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs @@ -109,7 +109,8 @@ public sealed class ColumnDef /// The text collation (LCID + sort id + sort-order version) for a non-numeric column — read from /// the descriptor's locale bytes (0x0B/0x0C), sort id (0x0D) and version byte - /// (0x0E). Numeric columns reuse the locale bytes for precision/scale and carry no collation. + /// (0x0E). Numeric columns reuse the locale bytes for precision/scale, and complex columns for their + /// MSysComplexColumns key; neither carries a collation. /// Defaults to General legacy. /// The version is at 0x0E, not 0x0D. Reading it from 0x0D — which is 0 in /// both General orders — made LibRed report every database as v0, so the byte is worth naming exactly. diff --git a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs index c9012436..5c1ea3d5 100644 --- a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs +++ b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs @@ -322,9 +322,11 @@ private int ReadColumns(PageBuffer buffer, JetFormatBase format, int columnBlock if (!columnIds.Add(columnId)) throw new InvalidDataException($"TDEF contains duplicate column id {columnId}."); - // Bytes 0x0B/0x0C are precision/scale for a Decimal/Numeric column and the text-collation LCID - // for everything else; 0x0D is the collation's sort-order version. Read whichever applies. + // Bytes 0x0B/0x0C are precision/scale for a Decimal/Numeric column, the MSysComplexColumns key for a + // Complex one (page-02b §3.4), and the text-collation LCID for everything else; 0x0D is the + // collation's sort-order version. Read whichever applies — a complex column has no collation to read. bool numeric = type == JetDataType.FixedPoint; + bool complex = type == JetDataType.Complex; descriptors[i] = ( type, columnId, @@ -339,7 +341,7 @@ private int ReadColumns(PageBuffer buffer, JetFormatBase format, int columnBlock // ACE's DROP COLUMN removes a descriptor but does NOT renumber the survivors or rewrite // rows, so a survivor keeps its original variable index even though ranking would shift it. buffer.ReadUInt16(entry + format.ColumnVariableIndexOffset), - numeric ? Collation.GeneralLegacy + numeric || complex ? 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). diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index 63a34d7a..db550681 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -15,7 +15,8 @@ > **Why the hidden bit / `#` prefix matter.** Missing them makes a hidden helper such as > EFCore.Jet's `#Dual` (`Flags = 0x08`) count as a *user* table, so a "has any user tables?" check > wrongly reports a schema-less database as populated — which makes EF Core's `EnsureCreated` skip - > creating the model's tables. Real user tables carry `Flags = 0x00000000`, so excluding the + > creating the model's tables. Real user tables carry `Flags = 0x00000000` — or `0x00040000`, the bit + > marking a table that has a complex column (§ *Catalog rows for the hidden tables*) — so excluding the > system/hidden bits never drops a genuine table. > **The same bits name the object kind in a schema rowset** (verified against ACE's `Tables`, which @@ -695,7 +696,7 @@ tables as far as the catalog is concerned, distinguished only by `Flags` and `Ow | object | `Flags` | `Owner` | | --- | --- | --- | -| an ordinary **user** table | `0x00040000` | user SID | +| a **user** table with a complex column | `0x00040000` (`0` without one) | user SID | | **`f__`** flat table | `0x800A0000` | **the same user SID** | | **`MSysComplexType_*`** template | `0x80030000` | `NULL` | | **`MSysComplexColumns`** | `0x80000000` | `NULL` | @@ -703,8 +704,11 @@ tables as far as the catalog is concerned, distinguished only by `Flags` and `Ow A flat table is thus system-flagged (`0x80000000`) yet owned by the *user* SID, unlike a real system table — consistent with it holding user data. `0x00020000` is common to flat and template tables; flat adds -`0x00080000` and templates `0x00010000`, while the plain user-table bit `0x00040000` is on neither. A flat -table can carry its own `LvProp`. Verified in `complex1.accdb` and `LIBRARY.accdb`. +`0x00080000` and templates `0x00010000`. **`0x00040000` marks the table that owns complex columns**: it is +set on every table with a complex column and on no table without one, system tables included — +`MSysResources`, whose `Data` is an attachment column, carries `0x0004000A` — and on neither the flat nor +the template tables, which hold the values rather than the column (verified across Access-written ACE +files). A flat table can carry its own `LvProp`. ### Attachment payload — `FileData` diff --git a/test/LibRed.Core.Tests/CollationTests.cs b/test/LibRed.Core.Tests/CollationTests.cs index 51655f87..4f353dc2 100644 --- a/test/LibRed.Core.Tests/CollationTests.cs +++ b/test/LibRed.Core.Tests/CollationTests.cs @@ -60,6 +60,17 @@ public void Non_numeric_columns_carry_the_database_collation_and_round_trip() finally { TemporaryDatabase.Delete(path); } } + // A complex column's locale bytes hold its MSysComplexColumns key (page-02b §3.4), not a collation: + // MSysResources.Data's 0x0B is ComplexID 1, which read as an LCID is the Arabic neutral order. + [Fact] + public void A_complex_column_carries_no_collation() + { + using var db = JetDatabase.Open(TestDatabases.NorthwindAccdb); + ColumnDef data = db.Catalog.FindTable("MSysResources")!.Columns.Single(c => c.Name == "Data"); + Assert.Equal(JetDataType.Complex, data.Type); + Assert.Equal(Collation.GeneralLegacy, data.Collation); + } + [Fact] public void The_written_locale_bytes_are_byte_identical_to_the_old_hardcoded_constant() { From bf88d23ed3b34f25170f73de1a6dcdddf76e92f4 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 14:02:22 +0800 Subject: [PATCH 56/88] Record a full benchmark run at 1cd4a6b5 Co-Authored-By: Claude Opus 5.5 --- test/LibRed.Benchmarks/History.tsv | 148 +++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/test/LibRed.Benchmarks/History.tsv b/test/LibRed.Benchmarks/History.tsv index c73791ea..6d0dddd1 100644 --- a/test/LibRed.Benchmarks/History.tsv +++ b/test/LibRed.Benchmarks/History.tsv @@ -352,3 +352,151 @@ utc commit dirty host suite benchmark params mean_us stddev_us alloc_kb 2026-09-27 07:34 ac9ced90 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10_indexed 19.00 0.23 19.35 2026-09-27 07:34 ac9ced90 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sub.scalar_correlated 1242.74 11.66 3.39 2026-09-27 07:34 ac9ced90 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sub.scalar_correlated 124.40 0.47 103.20 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV CatalogBenchmarks OpenAndLoadCatalog - 211.15 1.71 304.68 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV CatalogBenchmarks OpenOnly - 65.02 0.79 20.09 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV CatalogBenchmarks EnumerateSchema - 215.75 1.41 304.75 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV DdlBenchmarks CreateEmptyDatabase - 9827.19 119.89 8962.83 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV DdlBenchmarks CreateTable - 563.58 10.33 1319.87 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV DdlBenchmarks CreateIndexOverExistingRows - 1157.86 89.94 1830.31 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV DdlBenchmarks AddColumn - 156.74 12.51 380.06 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV WriteBenchmarks InsertSqlAutocommit - 101.18 1.63 148.81 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV WriteBenchmarks InsertSqlInTransaction - 69.37 0.95 148.50 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV WriteBenchmarks InsertStorage - 69.58 0.39 119.83 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV WriteBenchmarks InsertStorageInTransaction - 42.46 1.02 119.06 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV WriteBenchmarks UpdateByPrimaryKey - 42.16 1.01 38.85 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV WriteBenchmarks UpdateIndexedColumn - 81.40 1.85 112.83 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV WriteBenchmarks DeleteByPrimaryKey - 184.37 2.81 374.70 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV WriteBenchmarks UpdateSetBased - 460.47 19.35 502.13 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.count_distinct 1825.58 17.49 2445.48 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.distinct_rows 3213.73 40.86 2920.15 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.global 3351.53 14.61 2234.19 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_high_card 4373.77 140.39 4013.64 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks Parse Scale=10000 Case=agg.group_low_card 8.82 0.23 10.10 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks BindAndPlan Scale=10000 Case=agg.group_low_card 0.23 0.00 0.68 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks ExecutePrepared Scale=10000 Case=agg.group_low_card 4151.38 68.10 6056.99 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks EndToEnd Scale=10000 Case=agg.group_low_card 2294.27 15.67 2960.55 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_low_card 2233.44 7.08 2960.55 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_multi_agg 4415.82 53.67 4211.46 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_over_join 35523.12 205.70 10747.02 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.group_text_key 12690.43 67.74 6766.37 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=agg.having 2436.64 27.80 3128.49 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=catalog.schema_columns 8.47 0.04 15.42 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=catalog.schema_tables 6.12 0.01 9.66 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.arithmetic 4028.44 19.18 5263.61 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.concat 2502.64 13.95 3682.71 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.date_add 2308.57 9.42 3387.17 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.date_functions 3481.68 15.84 3784.15 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.iif 1810.52 6.58 2062.48 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.math_functions 4605.81 18.13 4802.59 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.nested_iif 3703.82 18.23 2594.55 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=expr.string_functions 4507.13 82.84 5555.86 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.cross_small 170.81 1.10 286.53 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.full_indexed 31350.69 95.72 8869.70 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks Parse Scale=10000 Case=join.hash_unindexed 14.07 0.03 17.30 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks BindAndPlan Scale=10000 Case=join.hash_unindexed 1.24 0.02 3.27 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks ExecutePrepared Scale=10000 Case=join.hash_unindexed 2966.48 19.87 4880.60 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks EndToEnd Scale=10000 Case=join.hash_unindexed 1707.79 87.23 2236.69 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.hash_unindexed 1607.02 6.99 2236.69 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks Parse Scale=10000 Case=join.index_nested_loop 17.85 0.17 21.16 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks BindAndPlan Scale=10000 Case=join.index_nested_loop 2.38 0.01 7.05 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks ExecutePrepared Scale=10000 Case=join.index_nested_loop 49.43 0.55 24.80 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks EndToEnd Scale=10000 Case=join.index_nested_loop 78.67 1.12 48.77 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.index_nested_loop 77.95 0.15 48.77 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.left_outer 14012.27 48.13 8419.88 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.self 3866.32 75.25 2906.80 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.small_probe 567.34 2.90 304.31 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=join.three_way 123.25 0.44 116.58 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.arithmetic 2225.84 7.89 2139.60 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.boolean 1131.51 5.67 1436.30 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.composite_eq 36.58 0.33 31.94 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.date_range 2441.59 13.76 2184.47 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.in_list 3240.16 20.33 2145.78 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.index_eq 25.32 0.09 23.05 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.index_open_range 44.76 0.17 35.88 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks Parse Scale=10000 Case=predicate.index_range 9.26 0.04 10.42 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks BindAndPlan Scale=10000 Case=predicate.index_range 0.80 0.01 1.98 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks ExecutePrepared Scale=10000 Case=predicate.index_range 51.29 0.09 60.35 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks EndToEnd Scale=10000 Case=predicate.index_range 54.59 0.78 42.00 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.index_range 52.85 0.15 42.00 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.is_null 1554.20 4.72 3097.00 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.like_contains 5391.24 29.73 8438.15 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.like_prefix 2731.62 28.42 5576.03 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.negated 1669.65 16.60 1905.75 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.or_two_indexes 6581.88 54.07 2610.49 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks Parse Scale=10000 Case=predicate.pk_point 10.65 0.02 12.60 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks BindAndPlan Scale=10000 Case=predicate.pk_point 0.82 0.00 2.47 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks ExecutePrepared Scale=10000 Case=predicate.pk_point 4.99 0.02 7.29 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks EndToEnd Scale=10000 Case=predicate.pk_point 24.87 0.26 23.50 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.pk_point 24.26 0.07 23.50 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.text_index_eq 23.83 0.13 21.57 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=predicate.unindexed_eq 1612.62 9.51 1904.04 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.count_star 675.83 3.62 1431.15 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.long_text 1646.99 0.60 3212.69 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.one_column 954.05 4.79 1976.88 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.star 2904.03 16.33 4769.29 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks Parse Scale=10000 Case=scan.three_columns 7.51 0.04 9.76 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks BindAndPlan Scale=10000 Case=scan.three_columns 0.26 0.00 0.62 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks ExecutePrepared Scale=10000 Case=scan.three_columns 2735.82 15.08 5234.73 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks EndToEnd Scale=10000 Case=scan.three_columns 1612.75 9.29 2840.75 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=scan.three_columns 1555.40 4.74 2840.75 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=setop.union_all 39.05 0.35 37.86 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=setop.union_distinct 423.26 2.01 282.68 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.by_indexed 9118.99 71.99 3454.97 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.by_two_columns 9846.94 50.06 3454.59 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.by_unindexed 9649.03 35.03 3455.03 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks Parse Scale=10000 Case=sort.top_10 9.29 0.02 10.98 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks BindAndPlan Scale=10000 Case=sort.top_10 0.62 0.00 1.48 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks ExecutePrepared Scale=10000 Case=sort.top_10 4897.86 90.51 5159.45 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks EndToEnd Scale=10000 Case=sort.top_10 3293.26 4.42 2299.27 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.top_10 3218.53 13.29 2299.27 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks Parse Scale=10000 Case=sort.top_10_indexed 7.99 0.04 9.34 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks BindAndPlan Scale=10000 Case=sort.top_10_indexed 0.51 0.01 1.43 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks ExecutePrepared Scale=10000 Case=sort.top_10_indexed 5.61 0.17 9.82 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks EndToEnd Scale=10000 Case=sort.top_10_indexed 19.18 0.28 19.00 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.top_10_indexed 18.96 0.24 19.00 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sort.top_percent 7276.88 50.67 3398.23 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sub.derived_table 2381.30 25.47 3016.78 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sub.exists_correlated 133.68 0.99 101.61 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sub.in_uncorrelated 1976.27 7.52 2598.22 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sub.not_exists 135.56 0.48 102.58 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks Parse Scale=10000 Case=sub.scalar_correlated 19.89 0.24 22.47 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks BindAndPlan Scale=10000 Case=sub.scalar_correlated 0.96 0.03 2.80 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks ExecutePrepared Scale=10000 Case=sub.scalar_correlated 86.10 0.75 73.65 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV PipelineBenchmarks EndToEnd Scale=10000 Case=sub.scalar_correlated 122.53 0.62 97.70 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=sub.scalar_correlated 122.39 0.72 97.70 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=window.rank_partitioned 41.63 0.16 37.95 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV SyntheticQueryBenchmarks Query Scale=10000 Case=window.row_number 39.23 0.11 34.27 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV StorageBenchmarks FullScan Scale=10000 2874.12 64.80 4760.56 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV StorageBenchmarks PointSeek Scale=10000 0.81 0.00 0.78 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV StorageBenchmarks DuplicateKeySeek Scale=10000 4.20 0.01 4.86 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV StorageBenchmarks RangeSeek Scale=10000 41.86 0.21 48.88 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=agg.group_low_card 10612.49 65.20 49.86 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=agg.group_low_card 2315.70 18.14 2960.54 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=join.hash_unindexed 4018.80 41.09 27.97 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=join.hash_unindexed 1638.92 9.94 2236.69 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=join.index_nested_loop 917.57 7.52 3.83 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=join.index_nested_loop 80.46 1.78 48.77 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.correlated_subquery 803.61 17.85 730.95 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.group_agg_join 3690.99 8.61 1207.93 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.group_by_customer 413.98 1.00 364.01 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.left_join 553.83 1.43 382.30 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.like_prefix 53.58 0.20 81.08 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.order_by_text 122.53 1.56 64.79 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.orders_details_join 2867.16 11.68 986.99 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.orders_scan 165.47 2.11 277.53 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.text_pk_lookup 23.91 0.11 22.85 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.three_way_join 3336.47 36.64 1068.02 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.top_10 392.59 1.84 247.98 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV NorthwindQueryBenchmarks Query Case=nw.unindexed_join 1378.46 2.22 903.17 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.index_range 831.71 12.66 4.94 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.index_range 53.19 0.12 42.00 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=predicate.pk_point 713.85 14.46 3.53 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=predicate.pk_point 25.98 0.63 23.50 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=scan.three_columns 19167.96 70.46 941.90 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=scan.three_columns 1593.16 11.46 2840.74 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10 5394.58 27.06 3.44 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10 3363.69 49.18 2299.26 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sort.top_10_indexed 3895.23 27.97 2.68 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sort.top_10_indexed 18.70 0.04 19.00 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks Ace Case=sub.scalar_correlated 1321.68 15.39 3.39 +2026-09-27 22:03 1cd4a6b5 clean DESKTOP-D1RBTPV AceComparisonBenchmarks LibRed Case=sub.scalar_correlated 123.62 0.87 97.70 From f223943fe47be387e602958b365a34f4af4491a6 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 14:46:05 +0800 Subject: [PATCH 57/88] Read and write every property type, and keep a rewritten blob's name pool A property's value was read as UTF-16 text unless it was a Boolean, so every numeric, date and binary property Access writes came back as noise, and a property built from its Value text was written as UTF-16 whatever its type. Each is now decoded by its type and its stored length - which is what fixes the width, since Access writes some Booleans and Int16s as four bytes, and a four-byte Int16 is a signed 32-bit value - and TypedValue gives the value itself. A constructed property is encoded as its type, and PropertyBlob.Of builds one from any CLR value as Access stores it, a GUID as 16 binary bytes in Guid.ToByteArray order. Access keeps a deleted property's name in the pool, in the order the names were first defined, and Write rebuilt the pool from the surviving properties - so every read-modify-write of an Access blob (a column's default or nullability, a CHECK) changed its bytes, 2,134 of the 3,046 blobs in a corpus of Access files. Write now takes the original blob, keeps its pool whole and appends new names; all 3,046 rewrite unchanged, and every one of their properties decodes to its type. The spec records that any owner can carry any type, text as 0x0A or 0x0C by property, the wide encodings, and the pool outliving its properties. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Catalog/PropertyBlob.cs | 210 +++++++++++++++--- .../LibRed.Core/Storage/TableCreator.cs | 6 +- src/LibRed/docs/format/appendix-structures.md | 2 +- src/LibRed/docs/format/system-catalog.md | 26 ++- .../PropertyBlobRoundTripTests.cs | 180 +++++++++++++++ 5 files changed, 387 insertions(+), 37 deletions(-) diff --git a/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs b/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs index cca4d90b..fe5b456f 100644 --- a/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs +++ b/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs @@ -36,16 +36,20 @@ public static class PropertyBlob /// A calculated column's real result type, as a one-byte Jet type code (§3.4a). public const string ResultTypeProperty = "ResultType"; - /// A single property: the owning column (or "" for the table), the property name, its value - /// (text; for a boolean, "1"/"0"), and its stored type. The type is an ordinary - /// code — the same byte used by column descriptors and MSysQueries — so Access - /// stores DefaultValue/CheckConstraints as and Required - /// as . + /// A single property: the owning column (or "" for the table), the property name, its value, and + /// its stored type. The type is an ordinary code — the same byte used by column + /// descriptors and MSysQueries — and any of them can occur on any owner (§11): Access stores + /// DefaultValue/CheckConstraints as , Description as + /// , Required as , ColumnWidth + /// as , a GUID as . + /// is the value as invariant text — the text itself for a text type, + /// "1"/"0" for a boolean, the number, date or GUID in invariant form, and hex for binary — + /// and the value itself. The stored length decides the width, not the type: a + /// Boolean or an Int16 can be four bytes. /// holds the exact stored value bytes when the property was - /// from a blob; emits it verbatim, so a property LibRed does not model (a numeric - /// DecimalPlaces, a designer ValidationRule/Format, …) round-trips byte-for-byte even - /// though its string is only a best-effort UTF-16 decode. It is null for a - /// property LibRed constructs, which is then encoded from /. + /// from a blob; emits it verbatim, so every property round-trips byte-for-byte. It is + /// null for a property constructed from a string, which is then encoded from + /// /; builds one from a CLR value instead. /// is the entry's flag byte, kept whole and written back unchanged. It is a bit /// field: 0x01 marks a DDL property, protected as part of the object's definition, and Access writes /// 0x80 on its own account (seen on stored queries). It defaults to 0x01 because the properties @@ -71,6 +75,11 @@ public bool IsOwnedBy(string owner) => && string.Equals(Owner, owner, StringComparison.OrdinalIgnoreCase); internal ushort BlockType => Block ?? (Owner.Length == 0 ? TableBlock : ColumnBlock); + + /// The value as its type holds it: , , a signed integer + /// as wide as the stored value, for Currency, /, + /// , , for text, else the bytes. + public object? TypedValue => Decode(Type, PropertyValue(this)); } #pragma warning restore CA1716 @@ -78,20 +87,54 @@ public bool IsOwnedBy(string owner) => public static Property Bool(string owner, string name, bool value) => new(owner, name, value ? "1" : "0", JetDataType.Boolean); + /// A property holding , stored as the type Access uses for that CLR type — + /// Boolean, Byte, Int16, Int32, + /// Int64, Currency, Single, + /// Double, DateTime, and byte[] Binary (Access stores a + /// GUID property as 16 binary bytes in order), + /// Memo — or as when given (Text for a string Access keeps as Text, Ole + /// for a blob). + public static Property Of(string owner, string name, object value, JetDataType? type = null) + { + ArgumentNullException.ThrowIfNull(value); + JetDataType stored = type ?? value switch + { + bool => JetDataType.Boolean, + byte => JetDataType.Byte, + short => JetDataType.Int16, + int => JetDataType.Int32, + long => JetDataType.Int64, + decimal => JetDataType.Currency, + float => JetDataType.Single, + double => JetDataType.Double, + DateTime => JetDataType.DateTime, + Guid or byte[] => JetDataType.Binary, + string => JetDataType.Memo, + _ => throw new ArgumentException($"A {value.GetType().Name} has no property type.", nameof(value)), + }; + byte[] raw = Encode(stored, value); + return new Property(owner, name, Format(Decode(stored, raw)), stored, raw); + } + /// Builds the blob for a set of properties, grouped by owner in the given order — matching /// what ACE writes (verified byte-for-byte for column DefaultValues). /// The properties to write, grouped by owner in the order given. - /// The 4-byte signature to stamp. Pass the ORIGINAL blob's first four bytes when - /// rewriting one, so a Jet-4 KKD\0 blob is not silently reissued as an ACE MR2\0 one — two - /// of the four write paths here already preserve it, and the faithful round-trip rule says all four - /// should. Omit it only when authoring a blob from nothing, where ACE's is the right default. - public static byte[] Write(IReadOnlyList properties, ReadOnlySpan signature = default) + /// The blob being rewritten, when rewriting one: its signature is stamped, so a + /// Jet-4 KKD\0 blob is not silently reissued as an ACE MR2\0 one, and its name pool is kept + /// whole and in order, new names appended. Access's pool keeps the names of properties since deleted + /// (measured: Description, Filter, OrderBy on tables whose entries are gone), so a pool + /// rebuilt from the surviving properties changes every such blob it rewrites. Four bytes stamp the + /// signature alone. Omit it only when authoring a blob from nothing, where ACE's signature is the default. + public static byte[] Write(IReadOnlyList properties, ReadOnlySpan original = default) { - ValidateForWrite(properties); - var names = properties.Select(p => p.Name).Distinct().ToList(); - var nameIndex = names.Select((n, i) => (n, i)).ToDictionary(x => x.n, x => x.i); + var names = original.Length > 4 ? Parse(original).Names.ToList() : []; + foreach (string name in properties.Select(p => p.Name)) + if (!names.Contains(name)) names.Add(name); + ValidateForWrite(properties, names); + var nameIndex = new Dictionary(); + for (int i = 0; i < names.Count; i++) nameIndex.TryAdd(names[i], i); - var blob = new List(signature.Length == 4 ? signature.ToArray() : SignatureAce); + var blob = new List(original.Length >= 4 ? original[..4].ToArray() : SignatureAce); var namesBody = new List(); foreach (string name in names) AppendString(namesBody, name); @@ -434,11 +477,8 @@ private static void ReadProperties(ushort block, ReadOnlySpan body, List raw = body.Slice(pos + 8, valueLength); - string value = dataType == JetDataType.Boolean - ? (raw.Length > 0 && raw[0] != 0 ? "1" : "0") - : Encoding.Unicode.GetString(raw); - properties.Add(new Property(owner, names[nameIndex], value, dataType, raw.ToArray()) + byte[] raw = body.Slice(pos + 8, valueLength).ToArray(); + properties.Add(new Property(owner, names[nameIndex], Format(Decode(dataType, raw)), dataType, raw) { Flags = body[pos + 2], Block = block, @@ -447,11 +487,11 @@ private static void ReadProperties(ushort block, ReadOnlySpan body, List properties) + private static void ValidateForWrite(IReadOnlyList properties, List names) { - var names = properties.Select(p => p.Name).Distinct().ToList(); ValidateNames(names); - var nameIndex = names.Select((name, index) => (name, index)).ToDictionary(x => x.name, x => x.index); + var nameIndex = new Dictionary(); + for (int i = 0; i < names.Count; i++) nameIndex.TryAdd(names[i], i); foreach (var group in GroupByOwnerPreservingOrder(properties)) ValidateOwnerProperties(group.Owner, group.Properties, nameIndex); } @@ -494,10 +534,120 @@ private static void ValidateOwnerProperties( throw new ArgumentException($"Property block for owner '{owner}' exceeds its 32-bit block length."); } - private static byte[] PropertyValue(Property property) => property.RawValue - ?? (property.Type == JetDataType.Boolean - ? [(byte)(property.Value is "1" or "true" or "True" ? 1 : 0)] - : Encoding.Unicode.GetBytes(property.Value)); + private static byte[] PropertyValue(Property property) => property.RawValue ?? Encode(property.Type, property.Value); + + private static bool IsText(JetDataType type) => type is JetDataType.Text or JetDataType.Memo; + + /// A stored value as its type holds it. The stored length sets the width — Access writes some + /// Booleans and Int16s as four bytes (measured), and a four-byte "Int16" is a signed 32-bit value + /// (ColumnWidth FFFFFFFF is -1). A Boolean is true when any byte is non-zero (Access writes both + /// 01 and FF). A value no reading fits comes back as its bytes. + private static object? Decode(JetDataType type, ReadOnlySpan raw) + { + if (IsText(type)) return Encoding.Unicode.GetString(raw); + switch (type) + { + case JetDataType.Boolean: + return raw.IndexOfAnyExcept((byte)0) >= 0; + case JetDataType.Byte when raw.Length == 1: + return raw[0]; + case JetDataType.Byte or JetDataType.Int16 or JetDataType.Int32 or JetDataType.Int64: + switch (raw.Length) + { + case 1: return (short)(sbyte)raw[0]; + case 2: return BinaryPrimitives.ReadInt16LittleEndian(raw); + case 4: return BinaryPrimitives.ReadInt32LittleEndian(raw); + case 8: return BinaryPrimitives.ReadInt64LittleEndian(raw); + } + break; + case JetDataType.Currency when raw.Length == 8: + return BinaryPrimitives.ReadInt64LittleEndian(raw) / 10000m; + case JetDataType.Single when raw.Length == 4: + return BinaryPrimitives.ReadSingleLittleEndian(raw); + case JetDataType.Double when raw.Length == 8: + return BinaryPrimitives.ReadDoubleLittleEndian(raw); + case JetDataType.DateTime when raw.Length == 8: + double serial = BinaryPrimitives.ReadDoubleLittleEndian(raw); + return serial is >= -657435.0 and < 2958466.0 ? DateTime.FromOADate(serial) : raw.ToArray(); + case JetDataType.Guid when raw.Length == 16: + return new Guid(raw); + } + return raw.ToArray(); + } + + /// A decoded value as text: invariant, round-trippable through + /// , and hex for bytes. + private static string Format(object? value) => value switch + { + null => "", + string s => s, + bool b => b ? "1" : "0", + float f => f.ToString("R", System.Globalization.CultureInfo.InvariantCulture), + double d => d.ToString("R", System.Globalization.CultureInfo.InvariantCulture), + DateTime dt => dt.ToString("O", System.Globalization.CultureInfo.InvariantCulture), + byte[] bytes => Convert.ToHexString(bytes), + IFormattable f => f.ToString(null, System.Globalization.CultureInfo.InvariantCulture), + _ => value.ToString() ?? "", + }; + + /// Encodes a constructed property's text as its type stores it — the + /// inverse of . Booleans are one byte, integers the type's own width. + private static byte[] Encode(JetDataType type, string value) + { + var invariant = System.Globalization.CultureInfo.InvariantCulture; + if (IsText(type)) return Encoding.Unicode.GetBytes(value); + return type switch + { + JetDataType.Boolean => [(byte)(value is "1" or "-1" or "true" or "True" ? 1 : 0)], + JetDataType.Byte => Encode(type, byte.Parse(value, invariant)), + JetDataType.Int16 => Encode(type, short.Parse(value, invariant)), + JetDataType.Int32 => Encode(type, int.Parse(value, invariant)), + JetDataType.Int64 => Encode(type, long.Parse(value, invariant)), + JetDataType.Currency => Encode(type, decimal.Parse(value, invariant)), + JetDataType.Single => Encode(type, float.Parse(value, invariant)), + JetDataType.Double => Encode(type, double.Parse(value, invariant)), + JetDataType.DateTime => Encode(type, DateTime.Parse(value, invariant, System.Globalization.DateTimeStyles.RoundtripKind)), + JetDataType.Guid => Encode(type, Guid.Parse(value)), + _ => Convert.FromHexString(value), + }; + } + + /// Encodes a CLR value as stores it. + private static byte[] Encode(JetDataType type, object value) + { + var invariant = System.Globalization.CultureInfo.InvariantCulture; + if (IsText(type)) return Encoding.Unicode.GetBytes(Convert.ToString(value, invariant) ?? ""); + byte[] b; + switch (type) + { + case JetDataType.Boolean: + return [(byte)(Convert.ToBoolean(value, invariant) ? 1 : 0)]; + case JetDataType.Byte: + return [Convert.ToByte(value, invariant)]; + case JetDataType.Int16: + b = new byte[2]; BinaryPrimitives.WriteInt16LittleEndian(b, Convert.ToInt16(value, invariant)); return b; + case JetDataType.Int32: + b = new byte[4]; BinaryPrimitives.WriteInt32LittleEndian(b, Convert.ToInt32(value, invariant)); return b; + case JetDataType.Int64: + b = new byte[8]; BinaryPrimitives.WriteInt64LittleEndian(b, Convert.ToInt64(value, invariant)); return b; + case JetDataType.Currency: + b = new byte[8]; + BinaryPrimitives.WriteInt64LittleEndian(b, decimal.ToInt64(decimal.Round(Convert.ToDecimal(value, invariant) * 10000m))); + return b; + case JetDataType.Single: + b = new byte[4]; BinaryPrimitives.WriteSingleLittleEndian(b, Convert.ToSingle(value, invariant)); return b; + case JetDataType.Double: + b = new byte[8]; BinaryPrimitives.WriteDoubleLittleEndian(b, Convert.ToDouble(value, invariant)); return b; + case JetDataType.DateTime: + b = new byte[8]; BinaryPrimitives.WriteDoubleLittleEndian(b, ((DateTime)value).ToOADate()); return b; + } + return value switch + { + Guid g => g.ToByteArray(), + byte[] bytes => bytes.ToArray(), + _ => throw new ArgumentException($"A {value.GetType().Name} cannot be stored as a {type} property.", nameof(value)), + }; + } /// The properties grouped into value blocks — one per block type and owner, since an index's block /// can carry the same name as a column's — in the order each block first appears. diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 107afda2..6b655171 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -1481,7 +1481,7 @@ private static byte[] RewriteCalculatedReferences(byte[] blob, string oldName, s changed = true; } return changed - ? PropertyBlob.Write(properties, blob.Length >= 4 ? blob.AsSpan(0, 4) : default) + ? PropertyBlob.Write(properties, blob) : blob; } @@ -2006,7 +2006,7 @@ private void MutateLvPropForColumn(string tableName, string columnName, Action= 4 ? blob.AsSpan(0, 4) : default); + byte[] updated = PropertyBlob.Write(props, blob); byte[] descriptor = new RowInserter(_channel, msys).StorePackedLongValue(lvProp.ColumnId, updated); values[lvProp.Index] = new LongValueDescriptor(descriptor); table.Update(id, values, new HashSet { lvProp.Index }); @@ -2227,7 +2227,7 @@ private static byte[] ReplaceTableProperty(byte[] blob, string name, string? val props.RemoveAll(p => p.IsOwnedBy("") && p.Name == name); if (value is not null) props.Add(new PropertyBlob.Property("", name, value)); - return PropertyBlob.Write(props, blob.Length >= 4 ? blob.AsSpan(0, 4) : default); + return PropertyBlob.Write(props, blob); } /// Refuses a narrowing ALTER when a stored value would no longer fit, reporting the same way the diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index 00a052de..1ef31131 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -260,7 +260,7 @@ Global maps, located by page 0: free pages at `0x18` (page 1 row 0 as ACE writes - **MSysACEs** (4 cols): `ObjectId`, `SID`, `ACM`, `FInheritable` — two rows per object. - **MSysQueries** (8 cols): `ObjectId`, `Attribute`, `Flag`, `Name1`, `Name2`, `Expression`, `Order`, `LvExtra`; PK `(ObjectId, Attribute, Order)`. - **MSysRelationships**: `szRelationship`, `szObject`, `szColumn`, `szReferencedObject`, `szReferencedColumn`, `icolumn`, `ccolumn`, `grbit` (`0x01` one-to-one, `0x02` don't-enforce, `0x100` cascade-update, `0x1000` cascade-delete, `0x2000` delete-set-null, `0x1000000`/`0x2000000` join type: all records from the parent / from the child). The cascades ACE applies come from the index-info blocks' `0x15`/`0x16`, not from these bits. -- **LvProp blob:** `MR2\0` signature, then `[int len][short type][body]` blocks; type `0x80` = name pool; value-block type `0x00` = table, `0x01` = column (`0x02` = index, unverified); per-owner entries are `[short entryLen][byte flags][byte dataType][short nameIndex][short valueLen][value]`. The flag byte is a bit field — `0x01` marks a definition-protected/schema property, and `0x80` also occurs in Access-written files — preserved whole per entry. Values include `DefaultValue`, `Required`, and `CheckConstraints`. +- **LvProp blob:** `MR2\0` signature, then `[int len][short type][body]` blocks; type `0x80` = name pool; value-block type `0x00` = table, `0x01` = column (`0x02` = index, unverified); per-owner entries are `[short entryLen][byte flags][byte dataType][short nameIndex][short valueLen][value]`. The flag byte is a bit field — `0x01` marks a definition-protected/schema property, and `0x80` also occurs in Access-written files — preserved whole per entry. `dataType` is any `JetDataType` code on any owner (text as `0x0A` or `0x0C`), and `valueLen`, not the type, gives the value's size (a Boolean or Int16 can be 4 bytes). Values include `DefaultValue`, `Required`, and `CheckConstraints`. --- diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index db550681..77752c9c 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -90,9 +90,29 @@ > marker. LibRed accepts any flag byte, writes it back unchanged, and defaults newly constructed schema > properties to `0x01`. The > `dataType` is an ordinary **`JetDataType` code** (the same byte used by column descriptors and - > MSysQueries): **`0x0C`** (Memo) for a text value stored as **UTF-16**, **`0x01`** (Boolean) for a single - > **0/1 byte**, and — on the `MSysDb` object's UI/nav settings only — `0x0A` (Text), `0x02`/`0x03`/`0x04` - > (Byte/Int16/Int32). The value-block **type** is `0x01` for a column-owned map and `0x00` for the + > MSysQueries), and any owner — the database, a table or its columns, a query, a relationship, Access's own + > document objects — can carry any of them (verified across Access-written files): + > - **Text `0x0A` and Memo `0x0C` both hold UTF-16 text**, split by property rather than by owner: + > `Description`, `Format`, `InputMask`, `Title` and `ValidationText` are `0x0A`; `DefaultValue`, + > `ValidationRule`, `CheckConstraints`, `Caption`, `RowSource` and `Expression` are `0x0C` (with + > `DefaultValue` and `RowSource` occasionally `0x0A`). + > - Boolean `0x01` (`Required`, `AllowZeroLength`, `UnicodeCompression`, `ColumnHidden`), Byte `0x02` + > (`ResultType`, `DecimalPlaces`, `IMEMode`), Int16 `0x03` (`ColumnWidth`, `ColumnOrder`, `DisplayControl`), + > Int32 `0x04` (`CurrencyLCID`, `Build`), Single `0x06` (the theme `…Tint`/`…Shade` adjustments), DateTime + > `0x08` (on `MSysDb`), Binary `0x09` (`GUID`, 16 bytes) and OLE `0x0B` (`NameMap`, `DOL`). + > - **The type does not fix the value's size; the entry's `valueLen` does.** Some Booleans are 4 bytes + > (`TotalsRow` and `HideNewField` always, `AppendOnly` sometimes) and some Int16s are too (`ColumnWidth` + > always, `ProjVer`, `ColumnOrder` sometimes). A reader takes the length from the entry, never from the type. + > A four-byte Int16 is a signed 32-bit value (`ColumnWidth` `FFFFFFFF` is -1, `18060000` is 1560), and a + > Boolean is true when any byte is non-zero — Access writes a one-byte true as both `01` and `FF`. + > + > **The name pool outlives its properties.** Access keeps a property's name in the pool after deleting the + > property itself, in the order the names were first defined — tables carrying `Description`, `Filter` and + > `OrderBy` names with no entry using them are common (verified across Access-written files). A writer + > rewriting a blob keeps the pool whole and appends new names; rebuilding it from the surviving entries + > changes the blob's bytes though nothing it means has changed. + > + > The value-block **type** is `0x01` for a column-owned map and `0x00` for the > table-owned map (empty owner name). mdbtools gives `0x02` as an **index**-owned map, named for the index — > which Access usually names for its column — though no example file has one (unverified). LibRed keeps each > block's type as read, so a column's properties are only ever those in a `0x01` block of its name. A `DefaultValue` (column property) is the expression's **source diff --git a/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs b/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs index 533ee84e..c6d2cf1f 100644 --- a/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs +++ b/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs @@ -80,6 +80,186 @@ public void Replacing_the_check_property_keeps_the_tables_other_properties() Assert.Equal(blob[..4], updated[..4]); // and the signature is carried across, not restamped } + // Every type Access stores, as Access stores it (values taken from Access-written files): the stored length + // sets the width — a Boolean or Int16 can be four bytes, and a four-byte "Int16" is a signed 32-bit value — and + // a Boolean is true for 01 and for FF alike. + public static TheoryData AccessValues() => new() + { + { JetDataType.Boolean, "01", true, "1" }, + { JetDataType.Boolean, "FF", true, "1" }, + { JetDataType.Boolean, "00", false, "0" }, + { JetDataType.Boolean, "01000000", true, "1" }, + { JetDataType.Boolean, "00000000", false, "0" }, + { JetDataType.Byte, "FF", (byte)255, "255" }, + { JetDataType.Int16, "0A00", (short)10, "10" }, + { JetDataType.Int16, "FFFFFFFF", -1, "-1" }, + { JetDataType.Int16, "18060000", 1560, "1560" }, + { JetDataType.Int32, "9DFFFFFF", -99, "-99" }, + { JetDataType.Single, "0000C842", 100f, "100" }, + { JetDataType.Text, "4400650073006300", "Desc", "Desc" }, + { JetDataType.Binary, "85276F677377BE40AC361424EE65776D", + Convert.FromHexString("85276F677377BE40AC361424EE65776D"), "85276F677377BE40AC361424EE65776D" }, + }; + + [Theory] + [MemberData(nameof(AccessValues))] + public void A_property_of_every_type_reads_as_its_value_and_writes_back_unchanged( + JetDataType type, string hex, object expected, string text) + { + byte[] raw = Convert.FromHexString(hex); + byte[] blob = PropertyBlob.Write([new PropertyBlob.Property("C", "P", "", type, raw)]); + + PropertyBlob.Property read = Assert.Single(PropertyBlob.Read(blob)); + Assert.Equal(expected, read.TypedValue); + Assert.Equal(text, read.Value); + Assert.Equal(blob, PropertyBlob.Write([.. PropertyBlob.Read(blob)])); + } + + [Fact] + public void A_datetime_property_reads_as_its_date() + { + // MSysDb's local_ReportDate, as Access stored it: an OLE Automation date. + byte[] raw = Convert.FromHexString("214365C752E7E440"); + PropertyBlob.Property read = Assert.Single(PropertyBlob.Read( + PropertyBlob.Write([new PropertyBlob.Property("", "local_ReportDate", "", JetDataType.DateTime, raw)]))); + Assert.Equal(DateTime.FromOADate(BitConverter.ToDouble(raw)), read.TypedValue); + } + + // Of stores each CLR type as Access does; a GUID is 16 binary bytes in Guid.ToByteArray order — testf's + // Table1 GUID property holds exactly the GUID MSysNameMap records for the table. + [Fact] + public void Of_stores_every_clr_type_and_reads_it_back() + { + var guid = Guid.Parse("676f2785-7773-40be-ac36-1424ee65776d"); + var when = new DateTime(2024, 4, 25, 18, 16, 0); + PropertyBlob.Property[] built = + [ + PropertyBlob.Of("C", "Bool", true), + PropertyBlob.Of("C", "Byte", (byte)2), + PropertyBlob.Of("C", "Int16", (short)-1), + PropertyBlob.Of("C", "Int32", 1560), + PropertyBlob.Of("C", "Currency", 12.5m), + PropertyBlob.Of("C", "Single", 100f), + PropertyBlob.Of("C", "Double", 0.1), + PropertyBlob.Of("C", "Date", when), + PropertyBlob.Of("C", "GUID", guid), + PropertyBlob.Of("C", "Memo", "text"), + PropertyBlob.Of("C", "Text", "text", JetDataType.Text), + ]; + var read = PropertyBlob.Read(PropertyBlob.Write(built)).ToDictionary(p => p.Name); + + Assert.Equal(new byte[] { 1 }, read["Bool"].RawValue); + Assert.Equal(true, read["Bool"].TypedValue); + Assert.Equal((byte)2, read["Byte"].TypedValue); + Assert.Equal((short)-1, read["Int16"].TypedValue); + Assert.Equal(1560, read["Int32"].TypedValue); + Assert.Equal(12.5m, read["Currency"].TypedValue); + Assert.Equal("0000C842", Convert.ToHexString(read["Single"].RawValue!)); + Assert.Equal(0.1, read["Double"].TypedValue); + Assert.Equal(when, read["Date"].TypedValue); + Assert.Equal(JetDataType.Binary, read["GUID"].Type); + Assert.Equal("85276F677377BE40AC361424EE65776D", Convert.ToHexString(read["GUID"].RawValue!)); + Assert.Equal((JetDataType.Memo, "text"), (read["Memo"].Type, read["Memo"].Value)); + Assert.Equal((JetDataType.Text, "text"), (read["Text"].Type, read["Text"].Value)); + } + + // A property constructed from its Value text is encoded as its type, not as UTF-16 text whatever the type. + [Fact] + public void A_property_built_from_text_is_encoded_as_its_type() + { + byte[] blob = PropertyBlob.Write( + [ + new PropertyBlob.Property("C", "DecimalPlaces", "2", JetDataType.Byte), + new PropertyBlob.Property("C", "ColumnWidth", "1560", JetDataType.Int16), + new PropertyBlob.Property("C", "CurrencyLCID", "3081", JetDataType.Int32), + ]); + var read = PropertyBlob.Read(blob).ToDictionary(p => p.Name); + Assert.Equal(new byte[] { 2 }, read["DecimalPlaces"].RawValue); + Assert.Equal((short)1560, read["ColumnWidth"].TypedValue); + Assert.Equal(3081, read["CurrencyLCID"].TypedValue); + } + + // Every property blob in the Access-written fixtures decodes — each value as its type's CLR type — and a + // rewrite of it is byte for byte the original. + [Fact] + public void Every_property_in_the_access_fixtures_decodes_and_writes_back_unchanged() + { + var typesSeen = new HashSet(); + foreach (string path in Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, "Data"), "*.accdb")) + { + JetDatabase db; + try { db = JetDatabase.Open(path); } + catch (InvalidOperationException) { continue; } // the password-encrypted fixture + using (db) + { + var objects = db.OpenTable("MSysObjects"); + int lvProp = objects.Definition.FindColumn("LvProp")!.Index; + foreach (object?[] row in objects.Rows()) + { + if (row[lvProp] is not byte[] blob) continue; + IReadOnlyList properties = PropertyBlob.Read(blob); + Assert.Equal(blob, PropertyBlob.Write(properties, blob)); + foreach (PropertyBlob.Property p in properties) + { + object? value = p.TypedValue; + Type expected = p.Type switch + { + JetDataType.Text or JetDataType.Memo => typeof(string), + JetDataType.Boolean => typeof(bool), + JetDataType.Single => typeof(float), + JetDataType.Binary or JetDataType.Ole => typeof(byte[]), + _ => value!.GetType(), + }; + Assert.IsType(expected, value); + typesSeen.Add(p.Type); + } + } + } + } + Assert.Superset( + new HashSet { JetDataType.Boolean, JetDataType.Byte, JetDataType.Int32, JetDataType.Text, JetDataType.Memo, JetDataType.Binary }, + typesSeen); + } + + // Access's name pool keeps the names of properties it has since deleted, in the order they were first + // defined. Rewriting a blob keeps that pool whole — a pool rebuilt from the surviving properties changed every + // such blob a rewrite touched — and appends only names that are new. + [Fact] + public void Rewriting_a_blob_keeps_its_name_pool_whole_and_in_order() + { + byte[] original = PropertyBlob.Write( + [ + new PropertyBlob.Property("", "Description", "gone soon", JetDataType.Text), + new PropertyBlob.Property("C", PropertyBlob.DefaultValueProperty, "1"), + PropertyBlob.Bool("C", PropertyBlob.RequiredProperty, true), + ]); + var props = PropertyBlob.Read(original).ToList(); + props.RemoveAll(p => p.Name == "Description"); // as Access leaves it: entry gone, name pooled + byte[] withoutDescription = PropertyBlob.Write(props, original); + + props.Add(PropertyBlob.Of("C", "ColumnWidth", (short)1560)); + byte[] rewritten = PropertyBlob.Write(props, withoutDescription); + + Assert.Equal( + ["Description", PropertyBlob.DefaultValueProperty, PropertyBlob.RequiredProperty, "ColumnWidth"], + NamePool(rewritten)); + Assert.Equal(withoutDescription, PropertyBlob.Write([.. PropertyBlob.Read(withoutDescription)], withoutDescription)); + } + + private static List NamePool(byte[] blob) + { + // The pool is the first block: [int length][short 0x80][ [short len][UTF-16] … ]. + int length = BitConverter.ToInt32(blob, 4); + var names = new List(); + for (int q = 10; q < 4 + length;) + { + int n = BitConverter.ToUInt16(blob, q); + names.Add(Encoding.Unicode.GetString(blob, q + 2, n)); + q += 2 + n; + } + return names; + } + // The flag byte is a bit field, not a DDL boolean: Access writes 0x80 on its own account (every stored query // in one of the example databases carries it). And an index's properties sit in a block of type 0x0002 under // the index's name, which is usually its column's. Reading such a blob used to throw on the flag, and writing From 5c4e92251e81d83da42336e409f155da195b960e Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 15:02:31 +0800 Subject: [PATCH 58/88] Keep a complex-column table's flags through a rebuild, and name index flag 0x0200 Index flag 0x0200 marks an index over a complex column: every such index Access writes is 0x0289, and no other index carries the bit. LibRed had no name for it, and the logical rebuild an ALTER to or from Memo/OLE takes recreates a table from scratch - so on a table owning a complex column it brought the column's index back as 0x0089 and the table's MSysObjects.Flags back as 0, dropping 0x00040000 and anything else the table carried. An index built over a complex column now gets 0x0200, a table created with one gets 0x00040000, and the rebuild carries the table's original Flags across whole, as it already carries its property blob and permissions. IndexDef.Flags exposes the whole flags word. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/IndexDef.cs | 4 +++ .../LibRed.Core/Formats/CatalogFormat.cs | 4 +++ src/LibRed/LibRed.Core/Formats/IndexFlags.cs | 5 +++ .../LibRed.Core/Pages/TableDefinitionPage.cs | 8 +++-- .../LibRed.Core/Storage/TableCreator.cs | 34 ++++++++++++++++--- src/LibRed/docs/format/appendix-structures.md | 2 +- .../docs/format/page-02d-constraints.md | 2 +- test/LibRed.Core.Tests/ComplexColumnTests.cs | 21 +++++++++++- 8 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/LibRed/LibRed.Core/Catalog/IndexDef.cs b/src/LibRed/LibRed.Core/Catalog/IndexDef.cs index ce219223..8e9c50db 100644 --- a/src/LibRed/LibRed.Core/Catalog/IndexDef.cs +++ b/src/LibRed/LibRed.Core/Catalog/IndexDef.cs @@ -19,6 +19,10 @@ public sealed record IndexDef /// required (index flag 0x08). Surfaced by scaffolding as the index DISALLOW NULL filter. public bool Required { get; init; } + /// The whole flags word at the index-data block's 0x2E — the bits above included, and + /// 0x0200 on an index over a complex column. + public ushort Flags { get; init; } + /// /// The index's unique-entry count from the TDEF statistics block. This is a cumulative /// count of distinct entries ever added that Access increments but never decrements, diff --git a/src/LibRed/LibRed.Core/Formats/CatalogFormat.cs b/src/LibRed/LibRed.Core/Formats/CatalogFormat.cs index 442a6a25..f3465c67 100644 --- a/src/LibRed/LibRed.Core/Formats/CatalogFormat.cs +++ b/src/LibRed/LibRed.Core/Formats/CatalogFormat.cs @@ -15,6 +15,10 @@ internal static class CatalogFormat /// MSysObjects.Type value for a table object. public const short ObjectTypeTable = 1; + /// MSysObjects.Flags bit of a table that owns a complex column — set on exactly those tables, + /// and on no table without one (measured across Access-written files; system-catalog §11). + public const int ObjectFlagOwnsComplexColumns = 0x00040000; + /// MSysObjects.Type value for a relationship object. public const short ObjectTypeRelationship = 8; } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/IndexFlags.cs b/src/LibRed/LibRed.Core/Formats/IndexFlags.cs index aeffb694..c74d8bd2 100644 --- a/src/LibRed/LibRed.Core/Formats/IndexFlags.cs +++ b/src/LibRed/LibRed.Core/Formats/IndexFlags.cs @@ -20,4 +20,9 @@ internal static class IndexFlags /// Always set on Access 2000+ index-data blocks. public const ushort AlwaysSet = 0x0080; + + /// The index is over a complex (multi-value / attachment) column. Set on every such index Access + /// writes — always as 0x0289, with unique, required and always-set — and on no other (measured across + /// Access-written files). + public const ushort ComplexColumn = 0x0200; } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs index 5c1ea3d5..9fae3af1 100644 --- a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs +++ b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs @@ -171,13 +171,15 @@ private void ReadIndexes(PageBuffer buffer, JetFormatBase format, int blockStart columns.Add((column, (buffer.ReadByte(entry + 2) & IndexBlockFormat.ColumnAscending) != 0)); } + ushort flags = buffer.ReadUInt16(block + IndexBlockFormat.FlagsOffset); _indexes.Add(new IndexDef { Name = string.Empty, Columns = columns, - IsUnique = (buffer.ReadUInt16(block + IndexBlockFormat.FlagsOffset) & IndexFlags.Unique) != 0, - IgnoreNulls = (buffer.ReadUInt16(block + IndexBlockFormat.FlagsOffset) & IndexFlags.IgnoreNulls) != 0, - Required = (buffer.ReadUInt16(block + IndexBlockFormat.FlagsOffset) & IndexFlags.Required) != 0, + IsUnique = (flags & IndexFlags.Unique) != 0, + IgnoreNulls = (flags & IndexFlags.IgnoreNulls) != 0, + Required = (flags & IndexFlags.Required) != 0, + Flags = flags, IsPrimaryKey = false, UniqueEntryCount = uniqueEntryCount, RootPage = buffer.ReadInt32(block + IndexBlockFormat.RootPageOffset), diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 6b655171..6a3b5da9 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -334,7 +334,8 @@ public void Create( columnProps.AddRange(CalculatedProperties(col)); } - AddCatalogRow(name, tdefPage, columnProps, checkConstraints); + AddCatalogRow(name, tdefPage, columnProps, checkConstraints, + ownsComplexColumns: columns.Any(c => c.Type == JetDataType.Complex)); AddPermissionRows(tdefPage); // Each foreign key's index is built after its relationship's rows, as ACE builds it (verified: a database's @@ -717,7 +718,11 @@ private int InsertIndex(TableDef table, string indexName, List<(int Id, bool Asc // block in name order. ACE's order ignores case (verified: a3 goes before IX2, and an FK named fk before // IX2); how it orders punctuation and accented letters is not measured. parts.Stats.Add(new byte[format.RealIndexEntrySize]); - parts.DataBlocks.Add(BuildIndexDataBlock(slots, rootPage, newIndexUsageRow, usageMapPage, unique, required, ignoreNulls)); + // An index over a complex column carries the complex-column flag — every one Access writes does, which a + // rebuild restoring the index from its IndexDef would otherwise drop. + bool complexColumn = slots.Any(s => table.Columns.Any(c => c.ColumnId == s.Id && c.Type == JetDataType.Complex)); + parts.DataBlocks.Add(BuildIndexDataBlock( + slots, rootPage, newIndexUsageRow, usageMapPage, unique, required, ignoreNulls, complexColumn)); int k = parts.Logical.Count(b => string.Compare(NameOf(b.Name), indexName, StringComparison.OrdinalIgnoreCase) < 0); parts.Logical.Insert(k, (buildInfo(newNum, dataCount), EncodeName(indexName))); @@ -1418,6 +1423,14 @@ private void RenameColumnProperties(int tdefPage, string oldName, string newName .Select(r => r.Values[lvProp.Index] as byte[]).FirstOrDefault(); } + /// The object's MSysObjects.Flags, or null when it has no row. + private int? ReadObjectFlags(int objectId) + { + (TableDef msys, Table table, int idIdx, _) = ObjectProperties(); + int flags = msys.FindColumn("Flags")!.Index; + return RowsKeyed(table, idIdx, objectId).Select(r => r.Values[flags] as int?).FirstOrDefault(); + } + /// Replaces an object's extended-property blob with . Not an /// call, because LvProp is a long value: the blob has to be stored first /// — inline or on a page — and the row given the descriptor for it. @@ -2468,6 +2481,10 @@ private void RewriteColumn(string tableName, string columnName, ColumnSpec newCo // which is not necessarily the set of grants this one carried. var permissions = ReadPermissionRows(def.DefinitionPage); + // And its MSysObjects.Flags: recreating it writes a new table's, dropping anything else it carried — the + // hidden bit, or the complex-column bit Access sets on the table that owns one. + int? objectFlags = ReadObjectFlags(def.DefinitionPage); + // 3. Pre-check: convert every target value in memory BEFORE touching disk. An unconvertible value // (e.g. non-numeric text → INT) throws here, with nothing written — the caller sees a clean failure. foreach (object?[] row in rows) @@ -2506,6 +2523,7 @@ private void RewriteColumn(string tableName, string columnName, ColumnSpec newCo foreach (int complexId in complexColumns) SetComplexColumnOwner(complexId, rebuiltPage); if (properties is { Length: > 0 }) WriteObjectProperties(rebuiltPage, properties); + if (objectFlags is int flags) UpdateCatalogRows("MSysObjects", "Id", rebuiltPage, required: true, ("Flags", flags)); RestorePermissionRows(rebuiltPage, permissions); _catalog.Invalidate(); @@ -3416,7 +3434,8 @@ private void WriteDefinition(int firstPage, byte[] def, IReadOnlyList oldCo } } - private static byte[] BuildIndexDataBlock(List<(int Id, bool Ascending)> columns, int rootPage, int usageRow, int usagePage, bool unique, bool required, bool ignoreNulls) + private static byte[] BuildIndexDataBlock(List<(int Id, bool Ascending)> columns, int rootPage, int usageRow, int usagePage, + bool unique, bool required, bool ignoreNulls, bool complexColumn) { var b = new byte[IndexBlockFormat.DataBlockSize]; System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0, 4), IndexBlockFormat.DataMarker); @@ -3439,6 +3458,7 @@ private static byte[] BuildIndexDataBlock(List<(int Id, bool Ascending)> columns if (unique) flags |= IndexFlags.Unique; if (ignoreNulls) flags |= IndexFlags.IgnoreNulls; if (required) flags |= IndexFlags.Required; + if (complexColumn) flags |= IndexFlags.ComplexColumn; System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(b.AsSpan(IndexBlockFormat.FlagsOffset, 2), flags); return b; } @@ -3580,7 +3600,8 @@ private void WriteUsageMaps(JetFormatBase format, int pageNumber, int mapCount) /// private void AddCatalogRow(string name, int tdefPage, IReadOnlyList columnProps, - IReadOnlyList<(string Name, string Expression)> checkConstraints) + IReadOnlyList<(string Name, string Expression)> checkConstraints, + bool ownsComplexColumns) { // Per-column properties (DefaultValue / Required) and CHECK constraints (a table property) both // live in the object's extended-properties (LvProp) blob. @@ -3589,8 +3610,11 @@ private void AddCatalogRow(string name, int tdefPage, props.Add(new PropertyBlob.Property("", PropertyBlob.CheckConstraintsProperty, PropertyBlob.WriteCheckList(checkConstraints))); + // A table owning a complex column is flagged so — the rebuild recreates such tables, and Access marks + // every one it writes. new CatalogWriter(_channel, _catalog).AddObjectRow( - name, tdefPage, CatalogFormat.ObjectTypeTable, CatalogFormat.ObjectContainerParentId, flags: 0, props); + name, tdefPage, CatalogFormat.ObjectTypeTable, CatalogFormat.ObjectContainerParentId, + flags: ownsComplexColumns ? CatalogFormat.ObjectFlagOwnsComplexColumns : 0, props); } // A new table's permission rows: what the Tables container grants what it creates (system-catalog §11). diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index 1ef31131..cf9f4c6e 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -166,7 +166,7 @@ descriptor. | `0x23` | 3 | Usage-map page | | `0x26` | 4 | B-tree root page | | `0x2A` | 4 | Unknown / reserved (zero) | -| `0x2E` | 2 | Flags: `0x01` unique (on a foreign key's child block: the relationship is one-to-one), `0x02` ignore-nulls, `0x08` required, `0x80` always-set | +| `0x2E` | 2 | Flags: `0x01` unique (on a foreign key's child block: the relationship is one-to-one), `0x02` ignore-nulls, `0x08` required, `0x80` always-set, `0x0200` complex column (always `0x0289` on one) | | `0x30` | 4 | Unknown / reserved (zero) — trailing bytes of the 52-byte block | ## Index-info block — 28 bytes, one per logical index → [page-02d](page-02d-constraints.md) diff --git a/src/LibRed/docs/format/page-02d-constraints.md b/src/LibRed/docs/format/page-02d-constraints.md index 9f4f7fb5..c4eb3b31 100644 --- a/src/LibRed/docs/format/page-02d-constraints.md +++ b/src/LibRed/docs/format/page-02d-constraints.md @@ -91,7 +91,7 @@ differently from a delete: | `0x23` | 3 | Usage-map page | | `0x26` | 4 | **B-tree root page** | | `0x2A` | 4 | Unknown / reserved — zero in every file checked. mdbtools calls it uninitialised page residue (unverified) | -| `0x2E` | 2 | Flags: `0x01` unique, `0x02` ignore-nulls (`WITH IGNORE NULL` — null-keyed rows excluded from the index), `0x08` required (`WITH DISALLOW NULL` / part of a primary key), `0x80` always-set (Access 2000+). Verified vs ACE: a plain index is `0x0080`, `IGNORE NULL` `0x0082`, `DISALLOW NULL` `0x0088`, a PK `0x0089`. A foreign key's child block (§3.6) carries `0x01` exactly when the relationship is one-to-one +| `0x2E` | 2 | Flags: `0x01` unique, `0x02` ignore-nulls (`WITH IGNORE NULL` — null-keyed rows excluded from the index), `0x08` required (`WITH DISALLOW NULL` / part of a primary key), `0x80` always-set (Access 2000+), `0x0200` an index over a **complex** (multi-value / attachment) column. Verified vs ACE: a plain index is `0x0080`, `IGNORE NULL` `0x0082`, `DISALLOW NULL` `0x0088`, a PK `0x0089`. Every index Access writes over a complex column is `0x0289` — unique, required, always-set and `0x0200` — and no other index carries `0x0200` (verified across Access-written files). A foreign key's child block (§3.6) carries `0x01` exactly when the relationship is one-to-one (`MSysRelationships.grbit` `0x01`), and **this bit, not `grbit`, is what ACE enforces**: made to disagree, a child index with the bit refuses a second child row with the same key although `grbit` says one-to-many, and one without it accepts the duplicate although `grbit` says one-to-one (verified vs ACE). There is **no clustered flag**: `CLUSTERED`/`NONCLUSTERED` after `PRIMARY KEY` or `UNIQUE` in a constraint is accepted and stores nothing (the file is byte-identical without it), and DAO's `Index.Clustered` reads `False` even on an index created with it set. | diff --git a/test/LibRed.Core.Tests/ComplexColumnTests.cs b/test/LibRed.Core.Tests/ComplexColumnTests.cs index 57e2fe69..e00247af 100644 --- a/test/LibRed.Core.Tests/ComplexColumnTests.cs +++ b/test/LibRed.Core.Tests/ComplexColumnTests.cs @@ -144,7 +144,7 @@ public void A_table_with_a_complex_column_keeps_it_through_the_rebuild() try { string flatTable; - int complexId, counter; + int complexId, counter, objectFlags; List<(int Id, int Values)> before; using (var db = JetDatabase.Open(path, readOnly: false)) { @@ -154,6 +154,9 @@ public void A_table_with_a_complex_column_keeps_it_through_the_rebuild() counter = db.Catalog.FindTable("MSysResources")!.ComplexAutoNumber; before = RecordValueCounts(db, data); Assert.NotEmpty(before); + objectFlags = ObjectFlags(db, "MSysResources"); + Assert.Equal(0x0004000A, objectFlags); // owns a complex column (0x40000), hidden (0x0A) + Assert.Equal(0x0289, ComplexIndexFlags(db)); // Text -> Memo is a storage-type change, which takes the full logical rebuild. db.AlterColumn("MSysResources", "Name", new ColumnSpec("Name", JetDataType.Memo, 0, IsFixedLength: false)); @@ -172,10 +175,26 @@ public void A_table_with_a_complex_column_keeps_it_through_the_rebuild() Assert.Equal(table.DefinitionPage, after.OwnerTable.DefinitionPage); Assert.Equal(counter, table.ComplexAutoNumber); Assert.Equal(before, RecordValueCounts(reopened, after)); + + // And the table and its index come back flagged as Access flags them: the table's MSysObjects.Flags + // whole (the complex-column bit and the hidden bits), the complex column's index with its 0x0200. + Assert.Equal(objectFlags, ObjectFlags(reopened, "MSysResources")); + Assert.Equal(0x0289, ComplexIndexFlags(reopened)); } finally { TemporaryDatabase.Delete(path); } } + private static int ObjectFlags(JetDatabase db, string table) + { + var objects = db.OpenTable("MSysObjects"); + var def = objects.Definition; + return (int)objects.Rows().Single(r => + (string?)r[def.FindColumn("Name")!.Index] == table && (short)r[def.FindColumn("Type")!.Index]! == 1)[def.FindColumn("Flags")!.Index]!; + } + + private static int ComplexIndexFlags(JetDatabase db) => + db.Catalog.FindTable("MSysResources")!.Indexes.Single(i => i.Columns.Any(c => c.Column.Name == "Data")).Flags; + // A complex column's catalog row names its column by NAME, and the catalog drops a row whose name the // owning table does not have — so a rename that left the row behind did not misname the column, it // disconnected it: the values stayed on disk with nothing able to reach them. From 9d00745d0c6c583d85347e87cdb7a9d3caaa9bd0 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 15:22:19 +0800 Subject: [PATCH 59/88] Check an object's name within its own container, as ACE does A name only has to be free among the objects of its MSysObjects container - the rule the unique (ParentId, Name) index states. A table or query clashes with a table, query or linked table (the Tables container) and with nothing else; measured against ACE for every object kind. CREATE TABLE and SELECT INTO now share the rename check, which tests the container rather than the table and query types, so a query's or linked table's name is refused as "Table 'X' already exists." instead of surfacing as an index violation. CREATE VIEW no longer refuses the names of relationships, forms, reports, macros, modules, database documents and containers, and reports a clash in ACE's words. The spec gains a table of the MSysObjects object kinds - their types, containers and filled columns - and the name rule, and drops the claim that tables and queries sit in different containers. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Storage/TableCreator.cs | 27 ++++++------ src/LibRed/LibRed.Core/Storage/ViewCreator.cs | 12 +++--- .../Execution/StatementExecutor.cs | 4 +- src/LibRed/docs/format/appendix-structures.md | 2 +- src/LibRed/docs/format/system-catalog.md | 43 ++++++++++++++++--- .../CreateTableDefaultTests.cs | 23 ++++++++++ test/LibRed.Engine.Tests/CreateViewTests.cs | 21 ++++++++- test/LibRed.Engine.Tests/SelectIntoTests.cs | 12 ++++++ 8 files changed, 113 insertions(+), 31 deletions(-) diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 6a3b5da9..60717419 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -106,9 +106,9 @@ public void Create( foreach (UniqueIndexSpec u in uniqueConstraints) JetName.Validate(u.Name, "unique constraint name"); foreach ((string checkName, _) in checkConstraints) JetName.Validate(checkName, "check constraint name"); - // 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) + // A table's name is free only if no table, query or linked table has it (case-insensitively) — ACE's + // rule, reported as ACE reports it, rather than as the MSysObjects index violation writing the row hits. + if (ObjectNameExists(name, exceptObjectId: 0)) 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 @@ -1525,32 +1525,29 @@ private void RepointRelationshipColumns(string tableName, string oldName, string } } - /// MSysObjects.Type for a table object (queries use ). - private const short ObjectTypeTable = 1; - /// - /// Whether any table or saved query already uses this name. Access keeps tables and queries in a - /// single namespace: ACE rejects renaming a table onto either (verified — RenameFanOutProbeTest), - /// even though they live in different MSysObjects containers, so the unique (ParentId, Name) index - /// would not catch a table/query collision on its own. Scanned straight from MSysObjects rather than - /// the catalog's reconstructed Views/ActionQueries, which omit queries LibRed can't rebuild. + /// Whether a table, saved query or linked table already uses this name — the objects of the Tables + /// container, whose names MSysObjects' unique (ParentId, Name) index keeps distinct. That is ACE's rule + /// for a new or renamed table (measured, and RenameFanOutProbeTest for renames): a form, report, macro, + /// module, relationship or database document of the same name does not collide. Scanned straight from + /// MSysObjects rather than the catalog's reconstructed tables and Views, which omit linked tables and + /// queries LibRed can't rebuild. /// private bool ObjectNameExists(string name, int exceptObjectId) { TableDef mo = _catalog.RequireTable("MSysObjects"); int idIndex = mo.RequireColumn("Id").Index; int nameIndex = mo.RequireColumn("Name").Index; - int typeIndex = mo.RequireColumn("Type").Index; + int parentIndex = mo.RequireColumn("ParentId").Index; var objects = new Table(_channel, mo); - foreach (object?[] values in objects.Rows(objects.DecodeOnly([idIndex, nameIndex, typeIndex]))) + foreach (object?[] values in objects.Rows(objects.DecodeOnly([idIndex, nameIndex, parentIndex]))) { if (!NameMatches(values[nameIndex], name)) continue; // Skip the object being renamed — it can't collide with itself (same-name and case-only renames). if (values[idIndex] is not null && Convert.ToInt32(values[idIndex], CultureInfo.InvariantCulture) == exceptObjectId) continue; - short type = Convert.ToInt16(values[typeIndex] ?? (short)0, CultureInfo.InvariantCulture); - if (type is ObjectTypeTable or StoredQueryFormat.ObjectTypeQuery) return true; + if (values[parentIndex] is int parent && parent == CatalogFormat.ObjectContainerParentId) return true; } return false; diff --git a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs index dde01c26..5b1f6cb2 100644 --- a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs @@ -74,19 +74,21 @@ private int AllocateObject(string name, short type, int parentId, int flags) int nameIndex = msysObjects.RequireColumn("Name").Index; int parentIndex = msysObjects.RequireColumn("ParentId").Index; - // A query's name must be unique among all objects (it also cannot equal an existing table name); a - // relationship's only among the relationships, as ACE has it. Find the next free negative id (they - // increment from 0x80000000) in the same scan. + // A name must be free within the object's own container — the rule MSysObjects' unique (ParentId, Name) + // index states, and the one ACE applies (measured): a query collides with a table, a query or a linked + // table, all in the Tables container, and not with a form, report, macro, module, relationship, database + // document or container; a relationship only with another relationship. Find the next free negative id + // (they increment from 0x80000000) in the same scan. bool relationship = parentId == CatalogFormat.RelationshipContainerParentId; int nextId = unchecked((int)0x80000000); var objects = new Table(_channel, msysObjects); foreach (object?[] row in objects.Rows(objects.DecodeOnly([idIndex, nameIndex, parentIndex]))) { - if ((!relationship || row[parentIndex] is int parent && parent == parentId) + if (row[parentIndex] is int parent && parent == parentId && string.Equals(row[nameIndex] as string, name, StringComparison.OrdinalIgnoreCase)) throw new SchemaObjectExistsException(relationship ? $"There is already a relationship named '{name}' in the current database." - : $"An object named '{name}' already exists.", name); + : $"Object '{name}' already exists.", name); if (row[idIndex] is int id && id < 0 && id >= nextId) nextId = id + 1; } diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 2304afce..a84c54b6 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -818,9 +818,9 @@ private int ExecuteCreateActionProcedure(CreateActionProcedureStatement statemen /// private int ExecuteSelectInto(SelectStatement statement) { + // A taken name — a table's, a query's or a linked table's — is refused by CreateTable, before anything is + // written; the read below has no side effect to undo. 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 diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index cf9f4c6e..901b320e 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -256,7 +256,7 @@ Global maps, located by page 0: free pages at `0x18` (page 1 row 0 as ACE writes ## Catalog tables → [system-catalog](system-catalog.md) -- **MSysObjects** (TDEF page 2): `Id`, `Name`, `Type` (1 table, 5 query/view), `Flags`, `ParentId`, `Owner`, `DateCreate`/`DateUpdate`, `LvProp` (property blob). +- **MSysObjects** (TDEF page 2): `Id`, `Name`, `Type` (1 table, 2 database, 3 container, 5 query/view, 6 linked table, 8 relationship; negative for Access documents — see system-catalog §11 *Object kinds*), `Flags`, `ParentId` (the container; `Name` is unique within it), `Owner`, `DateCreate`/`DateUpdate`, `LvProp` (property blob). - **MSysACEs** (4 cols): `ObjectId`, `SID`, `ACM`, `FInheritable` — two rows per object. - **MSysQueries** (8 cols): `ObjectId`, `Attribute`, `Flag`, `Name1`, `Name2`, `Expression`, `Order`, `LvExtra`; PK `(ObjectId, Attribute, Order)`. - **MSysRelationships**: `szRelationship`, `szObject`, `szColumn`, `szReferencedObject`, `szReferencedColumn`, `icolumn`, `ccolumn`, `grbit` (`0x01` one-to-one, `0x02` don't-enforce, `0x100` cascade-update, `0x1000` cascade-delete, `0x2000` delete-set-null, `0x1000000`/`0x2000000` join type: all records from the parent / from the child). The cascades ACE applies come from the index-info blocks' `0x15`/`0x16`, not from these bits. diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index 77752c9c..c1c2d65f 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -28,6 +28,34 @@ > **`TABLE`**, which is why `MSysAccessStorage` (`Flags = 0`) appears among the user tables despite its > name. Stored queries are listed in the same rowset as **`VIEW`**. + **Object kinds.** `Type` names the kind and `ParentId` the **container** it sits in; the containers are + themselves rows, of `Type 3`. Measured across a corpus of Access-written files: + + | `Type` | Kind | `ParentId` (container) | Non-null blob/link columns | + |---|---|---|---| + | `1` | local table | `Tables` (`0x0F000001`) | `LvProp`, `Owner` | + | `2` | the database object, `MSysDb` | `Databases` | — | + | `3` | container: `Tables`, `Databases`, `Relationships`, `Forms`, `Reports`, `Scripts`, `Modules`, `DataAccessPages`, `SysRel`; `Flags` `0` or `0x80000000` | `0x0F000000`, which has no row of its own | — | + | `5` | query | `Tables` | `Lv`, `LvExtra`, `LvProp` | + | `6` | linked table; `Flags` `0x00200000` or `0x00B00000` | `Tables` | `Connect`, `Database`, `ForeignName`, `Lv`, `LvProp` | + | `8` | relationship | `Relationships` (`0x0F000003`) | `LvProp` | + | `-32757` (`0x800B`) | database document: `SummaryInfo`, `UserDefined`, `AccessLayout` | `Databases` | `LvExtra`, `LvProp` | + | `-32758` (`0x800A`) | a row per user (`Admin`) | `SysRel` | `LvExtra` | + | `-32761` (`0x8007`) | module | `Modules` | — | + | `-32764` (`0x8004`) | report | `Reports` | — | + | `-32766` (`0x8002`) | macro | `Scripts` | — | + | `-32768` (`0x8000`) | form | `Forms` | — | + + The last column lists what a row of that kind *can* carry, not what every one does: a query or relationship + written by ACE or LibRed leaves `LvProp` null (below), where Access fills it. + + > **A name is unique within its container, and only there.** That is the rule the unique `(ParentId, Name)` + > index states, and the one ACE applies (verified, case-insensitively): a new table (`CREATE TABLE`, + > `SELECT … INTO`, a rename) or query (`CREATE VIEW` / `CREATE PROCEDURE`) is refused when a table, query or + > linked table already has its name — *"Table 'X' already exists."* and *"Object 'X' already exists."* + > respectively — and may take the name of a relationship, form, report, macro, module, database document or + > container. A relationship's name collides only with another relationship's (§ *Relationships* below). + **Writing a table object** (verified against Access-written rows). A complete user-table row sets: `Id` = TDEF page; `ParentId` = `0x0F000001` (the database's "Tables" container, constant); `Type` = `1`; `Name`; `Flags` = `0`; `Owner` = the 2-byte binary SID of the **Users** account *as this file @@ -289,7 +317,8 @@ > (see §3.7). - **Views / queries** are `MSysObjects` rows of **Type 5** with a **negative synthetic `Id`** (queries - increment from `0x80000000`), `ParentId 0x0F000001`, `Flags 0x10000000`, `LvProp` null. + increment from `0x80000000`), `ParentId 0x0F000001`, `Flags 0x10000000`, `LvProp` null as ACE writes it + (Access fills it with the query's properties). - **Relationships** are `MSysObjects` rows of **Type 8** too — one per relationship, alongside its `MSysRelationships` rows (verified vs ACE: every relationship Access or ACE creates has one, whether from @@ -297,7 +326,8 @@ `ParentId 0x0F000003` (the Relationships container), `Flags 0`, `Owner` = the file's Users SID (the SID note above), `DateCreate` = `DateUpdate` = the creation time, and `LvProp`, `Lv`, `LvExtra`, `LvModule`, `Connect`, `Database`, `ForeignName`, - `RmtInfoShort`, `RmtInfoLong` all null. + `RmtInfoShort`, `RmtInfoLong` all null — for one ACE creates. An Access-written relationship can carry + `LvProp`, in the ordinary property-blob format (§11 *Property blob*). - **`Id`** is the next negative synthetic id: one past the highest in the file, from the sequence queries draw on, so relationships and queries interleave (`0x8000002C` relationship, `0x8000002D` view, `0x8000002E` relationship), and a dropped relationship's id is taken by the next object. @@ -625,11 +655,10 @@ > query"* (Name AutoCorrect is an Access *application* feature, so it never runs for an engine-level rename). > LibRed reproduces exactly this, deliberately including the dangling query. > - > **Name collisions.** Tables and saved queries share **one namespace**: ACE rejects renaming a table onto - > the name of an existing table *or* an existing query (both verified). Note the unique `(ParentId, Name)` - > index does **not** enforce the table/query half of that on its own — the two object kinds sit in different - > containers, so they differ in `ParentId`. A rename therefore has to pre-check `MSysObjects` for a matching - > `Name` with `Type` 1 (table) or 5 (query), which is what LibRed does — **excluding the object being + > **Name collisions.** A rename follows the container rule of §11 *Object kinds*: ACE rejects renaming a + > table onto the name of an existing table *or* an existing query (both verified), the two sharing the + > `Tables` container. The check has to come before the rename, since the unique `(ParentId, Name)` index + > would only object once the row is being rewritten — **excluding the object being > renamed**, which cannot collide with itself: ACE allows renaming a table to its own name, and allows a > case-only change (both verified). The self-rename case is not hypothetical — EF models "move a table to > another schema" as a rename, and on a schema-less engine that degrades to `RENAME TO` the *same* name. diff --git a/test/LibRed.Engine.Tests/CreateTableDefaultTests.cs b/test/LibRed.Engine.Tests/CreateTableDefaultTests.cs index 20a56577..8d2c0830 100644 --- a/test/LibRed.Engine.Tests/CreateTableDefaultTests.cs +++ b/test/LibRed.Engine.Tests/CreateTableDefaultTests.cs @@ -188,6 +188,29 @@ public void Duplicate_table_name_throws() finally { TemporaryDatabase.Delete(path); } } + // A table shares its name space with the queries and linked tables of the Tables container, and with nothing + // else — ACE's rule, and its message. A relationship's name is free for a table. + [Fact] + public void Table_name_colliding_with_a_query_throws_but_a_relationship_name_is_free() + { + string path = Fresh(); + try + { + using var db = JetDatabase.Open(path, readOnly: false); + var e = new QueryEngine(db); + e.ExecuteNonQuery("CREATE VIEW `WidgetView` AS SELECT `ShipperID` FROM `Shippers`"); + var ex = Assert.Throws(() => + e.ExecuteNonQuery("CREATE TABLE `widgetview` (`Id` INTEGER)")); + Assert.Equal("Table 'widgetview' already exists.", ex.Message); + + e.ExecuteNonQuery("CREATE TABLE `Widget` (`Id` INTEGER PRIMARY KEY, `ShipperID` INTEGER, " + + "CONSTRAINT `WidgetShipper` FOREIGN KEY (`ShipperID`) REFERENCES `Shippers` (`ShipperID`))"); + e.ExecuteNonQuery("CREATE TABLE `WidgetShipper` (`Id` INTEGER)"); + Assert.Contains(db.Catalog.Tables, t => t.Name == "WidgetShipper"); + } + finally { TemporaryDatabase.Delete(path); } + } + [Fact] public void Temporary_table_throws_not_supported() { diff --git a/test/LibRed.Engine.Tests/CreateViewTests.cs b/test/LibRed.Engine.Tests/CreateViewTests.cs index 2ef63c86..12a14388 100644 --- a/test/LibRed.Engine.Tests/CreateViewTests.cs +++ b/test/LibRed.Engine.Tests/CreateViewTests.cs @@ -197,8 +197,27 @@ public void View_name_colliding_with_an_object_throws() { using var db = JetDatabase.Open(path, readOnly: false); // Northwind already has a Customers table. - Assert.Throws(() => + var ex = Assert.Throws(() => new QueryEngine(db).ExecuteNonQuery("CREATE VIEW `Customers` AS SELECT `CustomerID` FROM `Customers`")); + Assert.Equal("Object 'Customers' already exists.", ex.Message); + } + finally { TemporaryDatabase.Delete(path); } + } + + // A view collides only with the tables, queries and linked tables of the Tables container, as in ACE — a + // relationship lives in another container, so its name is free for a view. + [Fact] + public void View_may_take_a_relationships_name() + { + string path = Fresh(); + try + { + using var db = JetDatabase.Open(path, readOnly: false); + var e = new QueryEngine(db); + e.ExecuteNonQuery("CREATE TABLE `Widget` (`Id` INTEGER PRIMARY KEY, `ShipperID` INTEGER, " + + "CONSTRAINT `WidgetShipper` FOREIGN KEY (`ShipperID`) REFERENCES `Shippers` (`ShipperID`))"); + e.ExecuteNonQuery("CREATE VIEW `WidgetShipper` AS SELECT `Id` FROM `Widget`"); + Assert.Empty(e.ExecuteQuery("SELECT * FROM `WidgetShipper`").Rows); } finally { TemporaryDatabase.Delete(path); } } diff --git a/test/LibRed.Engine.Tests/SelectIntoTests.cs b/test/LibRed.Engine.Tests/SelectIntoTests.cs index d2b50037..4751cb50 100644 --- a/test/LibRed.Engine.Tests/SelectIntoTests.cs +++ b/test/LibRed.Engine.Tests/SelectIntoTests.cs @@ -102,6 +102,18 @@ public void An_existing_target_is_an_error() Assert.Equal("SiNew", error.ObjectName); } + // A query's name is taken too: tables and queries share the Tables container's name space. + [Fact] + public void A_query_named_like_the_target_is_an_error() + { + QueryEngine engine = WithSource(); + engine.ExecuteNonQuery("CREATE VIEW SiView AS SELECT Id FROM SiSrc"); + + var error = Assert.Throws( + () => engine.ExecuteNonQuery("SELECT Id INTO SiView FROM SiSrc")); + Assert.Equal("Table 'SiView' already exists.", error.Message); + } + // An expression column is typed from the expression, since there is no source column to copy. [Fact] public void An_expression_column_is_typed_from_the_expression() From 93ccf94563285489c1e6a456168e15aae4276d03 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 18:48:40 +0800 Subject: [PATCH 60/88] Read and write the Name AutoCorrect maps, and read the page type as a word Access keeps each object's Name AutoCorrect map twice: in the NameMap column of its MSysNameMap row and in a NameMap property in its LvProp, in two layouts. NameMap decodes and encodes both, carrying every field - the uninitialised bytes Access leaves, a missing terminator, the property's end record - so a map read and written back is byte-identical; every map in the corpus round-trips. JetDatabase reads the rows and a named object's property, and replaces either; a row is never added, its Id not being understood. The engine maintains neither copy, through ACE or LibRed. MSysNameMap.NameMap has an owned-pages map and no free-pages map, as Access writes it (and MSysAccessXML.LValue); every value on such a column takes a page of its own. The long-value writer refused the column outright, so no map over 64 bytes could be stored; it now writes one the way Access does, and ACE reads it back and keeps it through a compact. The page type is the 16-bit word at offset 0, not a type byte followed by a constant flags byte: every page Jet and ACE write carries 0x01 in the high byte, and ACE tests the whole word - it reads a table's owned page as rows only for 0x0101, 0x0103, 0x0104, 0x0106, 0x0107 and 0x0109, and skips everything else, 0x0001 and 0x0201 included. PageType is now that word and every check compares it whole; what LibRed writes is unchanged. The spec gains MSysNameMap, both map layouts, the owned-only long-value column, and the page type as a word, with the field at 0x02 named for what it is: the page's free-space count. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/NameMap.cs | 231 ++++++++++++++++++ src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs | 3 +- .../LibRed.Core/Crypto/JetLegacyEncryption.cs | 2 +- .../LibRed.Core/Formats/JetFormatBase.cs | 6 +- src/LibRed/LibRed.Core/JetDatabase.cs | 29 +++ src/LibRed/LibRed.Core/Pages/DataPage.cs | 4 +- src/LibRed/LibRed.Core/Pages/PageType.cs | 45 ++-- .../LibRed.Core/Pages/TdefChainReader.cs | 10 +- .../LibRed.Core/Storage/DatabaseCreator.cs | 6 +- .../LibRed.Core/Storage/IndexPageReader.cs | 4 +- src/LibRed/LibRed.Core/Storage/IndexWriter.cs | 5 +- .../LibRed.Core/Storage/LongValueWriter.cs | 3 +- .../LibRed.Core/Storage/PageAllocator.cs | 14 +- src/LibRed/LibRed.Core/Storage/RowInserter.cs | 43 ++-- .../LibRed.Core/Storage/TableCreator.cs | 108 +++++++- src/LibRed/LibRed.Core/Storage/UsageMap.cs | 8 +- .../LibRed.Core/Storage/UsageMapWriter.cs | 9 +- src/LibRed/docs/format/README.md | 56 +++-- src/LibRed/docs/format/appendix-structures.md | 48 ++-- src/LibRed/docs/format/long-values.md | 13 +- src/LibRed/docs/format/page-00-database.md | 6 +- .../docs/format/page-01-data-and-rows.md | 9 +- src/LibRed/docs/format/page-02a-tdef.md | 9 +- .../docs/format/page-03-04-index-btree.md | 17 +- src/LibRed/docs/format/page-05-usage-maps.md | 13 +- .../docs/format/page-08-released-tdef.md | 27 +- .../docs/format/page-09-released-data.md | 22 +- src/LibRed/docs/format/system-catalog.md | 55 ++++- .../DeleteAccessTests.cs | 4 +- .../DropTableParityAccessTests.cs | 2 +- .../PackedLongValueReleaseAccessTests.cs | 6 +- .../DropTableReclamationTests.cs | 2 +- .../GlobalReferenceFreeMapTests.cs | 9 +- .../MultiPageTableDefinitionTests.cs | 2 +- test/LibRed.Core.Tests/NameMapTests.cs | 194 +++++++++++++++ .../PageAndRowCorruptionTests.cs | 11 +- .../WholeFileParityProbeTest.cs | 6 +- test/LibRed.Engine.Tests/IndexSplitTests.cs | 2 +- 38 files changed, 834 insertions(+), 209 deletions(-) create mode 100644 src/LibRed/LibRed.Core/Catalog/NameMap.cs create mode 100644 test/LibRed.Core.Tests/NameMapTests.cs diff --git a/src/LibRed/LibRed.Core/Catalog/NameMap.cs b/src/LibRed/LibRed.Core/Catalog/NameMap.cs new file mode 100644 index 00000000..076769ca --- /dev/null +++ b/src/LibRed/LibRed.Core/Catalog/NameMap.cs @@ -0,0 +1,231 @@ +using System.Buffers.Binary; +using System.Text; + +namespace LibRed.Catalog; + +/// +/// Access's Name AutoCorrect map for one object: the names that object, and the objects it depends on, had +/// when Access last wrote the map (docs/format/system-catalog.md §11, MSysNameMap). Access keeps it +/// twice, in two layouts — the NameMap column of the object's MSysNameMap row +/// (/) and the NameMap property in the object's +/// LvProp (/) — which usually agree and sometimes +/// do not, having been written at different times. +/// +/// +/// The engine never maintains either: ACE's DDL leaves both as they were, and so does LibRed's. This type is +/// for a caller who wants to read or edit them deliberately. A map read and written back in the same layout is +/// byte-identical: every field is carried, including those whose meaning is not known. +/// +public sealed record NameMap +{ + /// The MSysNameMap.NameMap version in every file examined. + public const int RowVersion = 5; + + private const int RowFixedLength = 48; + private const int PropertyEndKind = 12; + private static readonly byte[] PropertySignature = [0x0A, 0xCC, 0x0E, 0x55]; + + /// The records, in stored order. + public IReadOnlyList Records { get; init; } = []; + + /// The map's version: the header value of the row layout (), or the value + /// the property layout's closing kind-12 record carries (2 to 5 seen). Null for a property blob that has no + /// closing record. + public int? Version { get; init; } = RowVersion; + + /// The property layout's closing record's 16-byte slot, whose first four bytes are + /// ; carried so the rest is written back as read. + private byte[]? _endSlot; + + /// Bytes after a property-layout map's last record that the layout does not account for, written + /// back as read. The row layout has none: a row blob parses to its exact end or is refused. + private byte[] _trailing = []; + + /// Decodes the NameMap column of an MSysNameMap row: [int32 version][int32 48], + /// then records of [int32 length][48 fixed bytes][UTF-16 name]. + /// The blob is not in this layout. + public static NameMap ReadRow(ReadOnlySpan blob) + { + if (blob.Length < 8) + throw new InvalidDataException($"A name map needs an 8-byte header; this one is {blob.Length} bytes."); + int fixedLength = BinaryPrimitives.ReadInt32LittleEndian(blob[4..]); + if (fixedLength != RowFixedLength) + throw new InvalidDataException($"A name map's records have {RowFixedLength} fixed bytes; this one declares {fixedLength}."); + + var records = new List(); + int pos = 8; + while (pos < blob.Length) + { + int length = pos + 4 <= blob.Length ? BinaryPrimitives.ReadInt32LittleEndian(blob[pos..]) : -1; + if (length < RowFixedLength || length % 2 != 0 || length > blob.Length - pos - 4) + throw new InvalidDataException($"The name map record at offset {pos} overruns the blob."); + ReadOnlySpan record = blob.Slice(pos + 4, length); + ReadOnlySpan name = record[RowFixedLength..]; + bool terminated = name.Length >= 2 && name[^2] == 0 && name[^1] == 0; + records.Add(ReadCommon(record, Encoding.Unicode.GetString(terminated ? name[..^2] : name)) with + { + TypeCode = BinaryPrimitives.ReadInt32LittleEndian(record[40..]), + Unused = BinaryPrimitives.ReadInt32LittleEndian(record[44..]), + NameTerminated = terminated, + }); + pos += 4 + length; + } + return new NameMap { Records = records, Version = BinaryPrimitives.ReadInt32LittleEndian(blob) }; + } + + /// Decodes the NameMap property of an object's LvProp: the signature + /// 0A CC 0E 55, then records of [40 fixed bytes][UTF-16 name, NUL-terminated] up to a closing + /// record of kind 12. A property record has no or + /// ; both read as zero. + /// The blob is not in this layout. + public static NameMap ReadProperty(ReadOnlySpan blob) + { + if (blob.Length < 4 || !blob[..4].SequenceEqual(PropertySignature)) + throw new InvalidDataException("A NameMap property begins with the signature 0A CC 0E 55."); + + var records = new List(); + int pos = 4; + while (pos + 42 <= blob.Length) + { + int end = pos + 40; + while (end + 1 < blob.Length && (blob[end] | blob[end + 1]) != 0) end += 2; + if (end + 1 >= blob.Length) break; // no terminating NUL: not a record, left as trailing bytes + + ReadOnlySpan record = blob[pos..(end + 2)]; + if (BinaryPrimitives.ReadInt32LittleEndian(record[20..]) == PropertyEndKind) + return new NameMap + { + Records = records, + Version = BinaryPrimitives.ReadInt32LittleEndian(record[24..]), + _endSlot = record.Slice(24, 16).ToArray(), + _trailing = blob[(end + 2)..].ToArray(), + }; + records.Add(ReadCommon(record, Encoding.Unicode.GetString(blob[(pos + 40)..end]))); + pos = end + 2; + } + return new NameMap { Records = records, Version = null, _trailing = blob[pos..].ToArray() }; + } + + /// Encodes the map in the MSysNameMap.NameMap layout. is written as + /// the header, when it is null. + public byte[] WriteRow() + { + var blob = new List(); + AppendInt32(blob, Version ?? RowVersion); + AppendInt32(blob, RowFixedLength); + foreach (NameMapRecord record in Records) + { + byte[] name = EncodeName(record); + AppendInt32(blob, RowFixedLength + name.Length); + AppendCommon(blob, record); + AppendInt32(blob, record.TypeCode); + AppendInt32(blob, record.Unused); + blob.AddRange(name); + } + return [.. blob]; + } + + /// Encodes the map in the NameMap property layout, closed by a kind-12 record carrying + /// — none when it is null, as in a map read from a blob that had none. A record's + /// name is always NUL-terminated here, since the NUL is what ends it. + public byte[] WriteProperty() + { + var blob = new List(PropertySignature); + foreach (NameMapRecord record in Records) + { + AppendCommon(blob, record); + blob.AddRange(Encoding.Unicode.GetBytes(record.Name)); + blob.AddRange([0, 0]); + } + if (Version is int version) + { + byte[] slot = _endSlot is null ? new byte[16] : (byte[])_endSlot.Clone(); + BinaryPrimitives.WriteInt32LittleEndian(slot, version); + AppendCommon(blob, new NameMapRecord(Guid.Empty, PropertyEndKind, "") { Slot = slot }); + blob.AddRange([0, 0]); + } + blob.AddRange(_trailing); + return [.. blob]; + } + + /// The 40 bytes both layouts share: [int32 reserved][GUID][int32 kind][16-byte slot]. + private static NameMapRecord ReadCommon(ReadOnlySpan record, string name) => + new(new Guid(record.Slice(4, 16)), BinaryPrimitives.ReadInt32LittleEndian(record[20..]), name) + { + Reserved = BinaryPrimitives.ReadInt32LittleEndian(record), + Slot = record.Slice(24, 16).ToArray(), + }; + + private static void AppendCommon(List blob, NameMapRecord record) + { + if (record.Slot.Length != 16) + throw new ArgumentException($"A name map record's slot is 16 bytes; '{record.Name}' has {record.Slot.Length}.", nameof(record)); + AppendInt32(blob, record.Reserved); + blob.AddRange(record.ItemGuid.ToByteArray()); + AppendInt32(blob, record.Kind); + blob.AddRange(record.Slot); + } + + private static byte[] EncodeName(NameMapRecord record) => + [.. Encoding.Unicode.GetBytes(record.Name), .. record.NameTerminated ? new byte[2] : []]; + + private static void AppendInt32(List blob, int value) + { + Span bytes = stackalloc byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(bytes, value); + blob.AddRange(bytes); + } +} + +/// One MSysNameMap row (docs/format/system-catalog.md §11). +/// The object's GUID property — what identifies the row. +/// The row's Id. Not the object's MSysObjects.Id, and not decoded. +/// The object's name when the row was written, without the trailing NUL it is stored with. +/// The object's MSysObjects.Type read as unsigned: 1 table, 5 query, 32768 form, +/// 32772 report. +/// The row's map; null when its NameMap column is. +public sealed record NameMapRow(Guid ObjectGuid, int Id, string Name, int Type, NameMap? Map); + +/// +/// One record of a : an object, or a field of one (docs/format/system-catalog.md §11). +/// +/// The object's or field's GUID — for an object, its GUID property. +/// The record kind. Not decoded: object records carry 0, 1 or 2, field records almost always 7. +/// The object's or field's name, without the terminating NUL. +public sealed record NameMapRecord(Guid ItemGuid, int Kind, string Name) +{ + /// The 16-byte slot after the kind: in an object record an OLE Automation date then eight zero + /// bytes, in a field record a GUID (in most records the preceding object record's). See + /// and . + public byte[] Slot { get; init; } = new byte[16]; + + /// The record's MSysObjects type (an object record) or data type code (a field record). Only the + /// MSysNameMap layout stores it; a record read from the property layout has zero. + public int TypeCode { get; init; } + + /// The first four bytes of the record, zero in every file examined; carried so they are written back + /// as read. + public int Reserved { get; init; } + + /// The last four fixed bytes of a MSysNameMap record, which Access leaves uninitialised — + /// whatever was in memory. Carried so a map is written back as read; zero for a new record. Not stored in + /// the property layout. + public int Unused { get; init; } + + /// Whether the name is stored with its terminating NUL, as every record but one in the files + /// examined is. Only the MSysNameMap layout can store it without. + public bool NameTerminated { get; init; } = true; + + /// The slot read as an object record's OLE Automation date; null when it holds none. + public DateTime? SlotDate + { + get + { + double days = BinaryPrimitives.ReadDoubleLittleEndian(Slot); + return days != 0 && double.IsFinite(days) && days is > -657435 and < 2958466 ? DateTime.FromOADate(days) : null; + } + } + + /// The slot read as a field record's GUID. + public Guid SlotGuid => new(Slot); +} diff --git a/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs b/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs index e06fab30..e7a06782 100644 --- a/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs +++ b/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs @@ -179,8 +179,7 @@ public static Result Build( int definitionSize = DefinitionSize(format, columns, indexes, logical, longValueColumns); var page = new byte[Math.Max(format.PageSize, definitionSize)]; - page[0] = (byte)PageType.TableDefinition; - page[format.TdefHeaderFlagsOffset] = 0x01; + PageHeader.WriteType(page, PageType.TableDefinition); BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.TdefRecordMarkerOffset, 4), JetFormatBase.TdefRecordMarker); // AutoNumber (COUNTER) config lives in the TDEF header: 0x18 = increment (default 1), and 0x14 = // the last-assigned value initialized to Seed-Increment so the first insert yields Seed. A table has diff --git a/src/LibRed/LibRed.Core/Crypto/JetLegacyEncryption.cs b/src/LibRed/LibRed.Core/Crypto/JetLegacyEncryption.cs index 477a5e8c..f2d0e748 100644 --- a/src/LibRed/LibRed.Core/Crypto/JetLegacyEncryption.cs +++ b/src/LibRed/LibRed.Core/Crypto/JetLegacyEncryption.cs @@ -12,7 +12,7 @@ namespace LibRed.Crypto; /// /// /// Verified against a real System.mdw (databaseKey 0xABBB315C): with this key every page decrypts -/// to a valid page-type byte (page 1 → 0x01 data, page 2/3 → 0x02 TDEF, index pages → 0x04), +/// to a valid page type (page 1 → 0x0101 data, page 2/3 → 0x0102 TDEF, index pages → 0x0104), /// and the XOR (not ADD) page-number mixing is the one that yields valid types on pages where the two differ. /// Same per-page key derivation as (LE32(pageNumber) XOR encodingKey), just /// feeding RC4 directly rather than an AES IV. diff --git a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs index d8929722..5420f3f6 100644 --- a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs +++ b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs @@ -126,10 +126,8 @@ public abstract class JetFormatBase // Defaults below are for Jet 4 / ACE (verified against a real ACCDB). Jet 3 differs // (18-byte column entries, 1-byte ASCII name lengths) and will override these. - /// Offset of the 1-byte TDEF header flags (observed 0x01). - public virtual int TdefHeaderFlagsOffset => 0x01; - - /// Offset of the 2-byte free-space-remaining-in-this-page field. + /// Offset of the 2-byte free-space count: the bytes still free in this page. (Before it, at 0x00, + /// the 2-byte page type — .) public virtual int TdefFreeSpaceOffset => 0x02; /// Offset of the 4-byte pointer to the next TDEF page (0 if the definition fits one page). diff --git a/src/LibRed/LibRed.Core/JetDatabase.cs b/src/LibRed/LibRed.Core/JetDatabase.cs index 6322d2ab..3e72a2b5 100644 --- a/src/LibRed/LibRed.Core/JetDatabase.cs +++ b/src/LibRed/LibRed.Core/JetDatabase.cs @@ -570,6 +570,35 @@ public void CreateActionQuery(string name, ActionQuerySpec spec) Catalog.Invalidate(); } + /// Every MSysNameMap row — Access's Name AutoCorrect map, one row per object it tracks — with + /// its map decoded; empty when the file has no such table. The engine never maintains these: a row reflects + /// the names as Access last saw them (docs/format/system-catalog.md §11). + public IReadOnlyList ReadNameMaps() => + new Storage.TableCreator(_channel, Catalog, Collation).ReadNameMapRows(); + + /// Replaces the map of the MSysNameMap row for the object whose GUID property is + /// — and the row's name when is given. Returns false + /// when there is no such row; one is never added. + public bool UpdateNameMap(Guid objectGuid, NameMap map, string? name = null) + { + ArgumentNullException.ThrowIfNull(map); + return new Storage.TableCreator(_channel, Catalog, Collation).WriteNameMapRow(objectGuid, map, name); + } + + /// The NameMap property — the object's own copy of its Name AutoCorrect map — of the object + /// with this name and MSysObjects.Type (1 table, 6 linked table, -32768 form, -32764 report); null + /// when it has none. Throws when there is no such object. + public NameMap? ReadNameMapProperty(string objectName, short objectType) => + new Storage.TableCreator(_channel, Catalog, Collation).ReadNameMapProperty(objectName, objectType); + + /// Sets the object's NameMap property, or removes it when is null, + /// leaving every other property as it was. Throws when there is no such object. + public void UpdateNameMapProperty(string objectName, short objectType, NameMap? map) + { + new Storage.TableCreator(_channel, Catalog, Collation).WriteNameMapProperty(objectName, objectType, map); + Catalog.Invalidate(); + } + /// Opens a table directly from its TDEF page, bypassing the catalog — used during database /// creation to seed the system tables before they self-register in MSysObjects. public Storage.Table OpenTableAt(int tdefPage, string name, bool isSystem = true) => diff --git a/src/LibRed/LibRed.Core/Pages/DataPage.cs b/src/LibRed/LibRed.Core/Pages/DataPage.cs index 54be39ce..e3b3d7c3 100644 --- a/src/LibRed/LibRed.Core/Pages/DataPage.cs +++ b/src/LibRed/LibRed.Core/Pages/DataPage.cs @@ -111,9 +111,9 @@ private static void ValidateHeader(PageBuffer buffer, JetFormatBase format, out if (buffer.Length != format.PageSize) throw new InvalidDataException( $"Data page {buffer.PageNumber} has {buffer.Length} bytes; expected {format.PageSize}."); - if (buffer.ReadByte(0) != (byte)PageType.DataPage) + if (PageHeader.ReadType(buffer.Span) != PageType.DataPage) throw new InvalidDataException( - $"Page {buffer.PageNumber} is type 0x{buffer.ReadByte(0):X2}, not a data page (0x01)."); + $"Page {buffer.PageNumber} is type 0x{(ushort)PageHeader.ReadType(buffer.Span):X4}, not a data page (0x0101)."); rowCount = buffer.ReadUInt16(format.DataRowCountOffset); // An index addresses a row by a one-byte slot number, so a page can hold at most 255 and ACE stops diff --git a/src/LibRed/LibRed.Core/Pages/PageType.cs b/src/LibRed/LibRed.Core/Pages/PageType.cs index 44ad5139..0d3b85ae 100644 --- a/src/LibRed/LibRed.Core/Pages/PageType.cs +++ b/src/LibRed/LibRed.Core/Pages/PageType.cs @@ -1,31 +1,48 @@ +using System.Buffers.Binary; + namespace LibRed.Pages; /// -/// The page-type marker stored in byte 0 of every page. Values match the on-disk -/// Jet/ACE encoding. +/// The page type: the little-endian 16-bit word at offset 0 of every page. Every type Jet/ACE writes has high +/// byte 0x01, page 0's included — the byte once read as a constant "flags" field is half of the type, and +/// ACE tests the whole word: it reads a table's owned page as rows only when the word is one of the row-bearing +/// types, and skips a page whose low byte is right but whose high byte is not (verified against ACE, every +/// combination of low byte 00–09 and high byte). See docs/format/README.md. /// -public enum PageType : byte +public enum PageType : ushort { - DatabaseDefinition = 0x00, - DataPage = 0x01, - TableDefinition = 0x02, - IntermediateIndexPage = 0x03, - LeafIndexPage = 0x04, - PageUsageBitmap = 0x05, + DatabaseDefinition = 0x0100, + DataPage = 0x0101, + TableDefinition = 0x0102, + IntermediateIndexPage = 0x0103, + LeafIndexPage = 0x0104, + PageUsageBitmap = 0x0105, /// A table-definition page that has been released by DROP TABLE. Access marks it by - /// setting this type byte and changing nothing else — the old definition stays on the page — and it + /// setting this type and changing nothing else — the old definition stays on the page — and it /// leaves the data, long-value and usage-map holder pages it frees at their original types. Measured: /// exactly one byte of the 4,096 differs across an ACE drop. Compact reclaims the page. /// See docs/format/page-08-released-tdef.md. - ReleasedTableDefinition = 0x08, + ReleasedTableDefinition = 0x0108, /// A data page released because its last live row was deleted. It covers both an ordinary /// table's data page emptied by DELETE and a packed long-value page whose last tenant was deleted — /// structurally the same event, and the same one-byte stamp. Every row slot is left a 0-length /// deleted+overflow tombstone and the page is given back. A chained long value owns its pages /// outright and they go back at instead. Nothing needs to handle it on read: - /// allocation selects on the free map, not on this byte. + /// allocation selects on the free map, not on this type. /// See docs/format/page-09-released-data.md. - ReleasedDataPage = 0x09, -} \ No newline at end of file + ReleasedDataPage = 0x0109, +} + +/// Reads and writes a page's — the whole word at offset 0, never its low byte +/// alone. +public static class PageHeader +{ + /// The page's type, as stored. + public static PageType ReadType(ReadOnlySpan page) => (PageType)BinaryPrimitives.ReadUInt16LittleEndian(page); + + /// Stamps at offset 0. + public static void WriteType(Span page, PageType type) => + BinaryPrimitives.WriteUInt16LittleEndian(page, (ushort)type); +} diff --git a/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs b/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs index 3e089d84..27dba5a6 100644 --- a/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs +++ b/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs @@ -17,10 +17,9 @@ internal static (PageBuffer Buffer, IReadOnlyList ContinuationPages) Read( JetFormatBase format = channel.Format; ValidatePageNumber(channel, firstPage, "TDEF root"); PageBuffer first = channel.ReadPage(firstPage); - if (first.ReadByte(0) != (byte)PageType.TableDefinition || first.ReadByte(1) != 0x01) + if (PageHeader.ReadType(first.Span) != PageType.TableDefinition) throw new InvalidDataException( - $"TDEF root page {firstPage} has header " + - $"[{first.ReadByte(0):X2} {first.ReadByte(1):X2}], expected [02 01]."); + $"TDEF root page {firstPage} is type 0x{(ushort)PageHeader.ReadType(first.Span):X4}, expected 0x0102."); int definitionLength = first.ReadInt32(format.TdefLengthOffset); if (definitionLength < format.TdefRealIndexBlockOffset || definitionLength > MaxDefinitionLength) @@ -53,10 +52,9 @@ internal static (PageBuffer Buffer, IReadOnlyList ContinuationPages) Read( throw new InvalidDataException($"TDEF page {firstPage} contains a continuation cycle at page {next}."); PageBuffer continuation = channel.ReadPage(next); - if (continuation.ReadByte(0) != (byte)PageType.TableDefinition || continuation.ReadByte(1) != 0x01) + if (PageHeader.ReadType(continuation.Span) != PageType.TableDefinition) throw new InvalidDataException( - $"TDEF continuation page {next} has header " + - $"[{continuation.ReadByte(0):X2} {continuation.ReadByte(1):X2}], expected [02 01]."); + $"TDEF continuation page {next} is type 0x{(ushort)PageHeader.ReadType(continuation.Span):X4}, expected 0x0102."); continuationPages.Add(next); continuationBuffers.Add(continuation); diff --git a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs index d5768c79..34ecae02 100644 --- a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs @@ -476,8 +476,7 @@ private static byte[] BuildFreeMapPage(JetFormatBase format, int usedPages) { const int mapLen = 1 + 4 + 64; // inline map: type + start page + 64-byte bitmap (512 pages) var page = new byte[format.PageSize]; - page[0] = (byte)PageType.DataPage; - page[1] = 0x01; + PageHeader.WriteType(page, PageType.DataPage); BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.DataOwnerOffset, 4), 1); // global-map owner (observed) int row0 = format.PageSize - mapLen; // free map (highest offset — the one the allocator reads) @@ -520,8 +519,7 @@ private static byte[] BuildUsageMapPage(JetFormatBase format, int mapCount) { const int mapLen = 1 + 4 + 64; // inline map record: type + start page + 64-byte bitmap var page = new byte[format.PageSize]; - page[0] = (byte)PageType.DataPage; - page[1] = 0x01; + PageHeader.WriteType(page, PageType.DataPage); int offset = format.PageSize; for (int row = 0; row < mapCount; row++) { diff --git a/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs b/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs index 0a55e747..de28ae2d 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs @@ -23,10 +23,10 @@ public static CheckedIndexPage Read(PageChannel channel, int pageNumber, int? ex { ValidatePageNumber(channel, pageNumber, "index page"); PageBuffer buffer = channel.ReadPageShared(pageNumber); - var type = (PageType)buffer.ReadByte(0); + var type = PageHeader.ReadType(buffer.Span); if (type is not (PageType.LeafIndexPage or PageType.IntermediateIndexPage)) throw new InvalidDataException( - $"Page {pageNumber} is type 0x{(byte)type:X2}, not an index page (0x03/0x04)."); + $"Page {pageNumber} is type 0x{(ushort)type:X4}, not an index page (0x0103/0x0104)."); int owner = buffer.ReadInt32(OwnerOffset); if (expectedOwner is not null && owner != expectedOwner) diff --git a/src/LibRed/LibRed.Core/Storage/IndexWriter.cs b/src/LibRed/LibRed.Core/Storage/IndexWriter.cs index 71e0e5d1..baf95bfb 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexWriter.cs @@ -652,8 +652,7 @@ private CheckedIndexPage ReadMutationPage(int pageNumber, PageType expectedType) { int pageSize = _channel.PageSize; var page = new byte[pageSize]; - page[0] = (byte)type; - page[1] = 0x01; // page flags (observed constant) + PageHeader.WriteType(page, type); page[LevelOffset] = (byte)level; // 0 on a leaf; the node's height above the leaves otherwise WriteInt32Le(page, OwnerOffset, _table.DefinitionPage); WriteInt32Le(page, PrevPageOffset, prev); @@ -853,7 +852,7 @@ private byte[] ImageOf(int pageNumber) // A leaf turning node — the root, when it splits — still counts as the same page rewritten. ReadOnlySpan existing = _channel.ReadPage(pageNumber).Span; - if (existing[0] is not ((byte)PageType.LeafIndexPage or (byte)PageType.IntermediateIndexPage) + if (PageHeader.ReadType(existing) is not (PageType.LeafIndexPage or PageType.IntermediateIndexPage) || BinaryPrimitives.ReadInt32LittleEndian(existing[OwnerOffset..]) != _table.DefinitionPage) return new byte[_channel.PageSize]; return existing.ToArray(); diff --git a/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs b/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs index 08bd4c57..940d805e 100644 --- a/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs @@ -163,8 +163,7 @@ private void WriteChunkPage(int pageNumber, byte[] row, uint stamp = 0) { JetFormatBase format = _channel.Format; var page = new byte[format.PageSize]; - page[0] = (byte)PageType.DataPage; - page[1] = 0x01; // page flags (observed constant) + PageHeader.WriteType(page, PageType.DataPage); BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.DataOwnerOffset, 4), LongValueFormat.LvalMarker); BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.DataChainStampOffset, 4), stamp); diff --git a/src/LibRed/LibRed.Core/Storage/PageAllocator.cs b/src/LibRed/LibRed.Core/Storage/PageAllocator.cs index bfc2db80..fafaf58e 100644 --- a/src/LibRed/LibRed.Core/Storage/PageAllocator.cs +++ b/src/LibRed/LibRed.Core/Storage/PageAllocator.cs @@ -23,7 +23,7 @@ public sealed class PageAllocator(PageChannel channel) { private const byte InlineMapType = 0x00; private const byte ReferenceMapType = 0x01; - /// Bytes preceding the bitmap on a dedicated usage-bitmap page (type 0x05). + /// Bytes preceding the bitmap on a dedicated usage-bitmap page (type 0x0105). private const int BitmapPageHeaderSize = 4; /// A reference map is a fixed 69-byte record: the type byte + 17 bitmap-page pointers (17 being @@ -272,8 +272,7 @@ private bool AddReleasedBitmapPages(byte[] reference, SortedSet pages) if (BinaryPrimitives.ReadInt32LittleEndian(reference.AsSpan(1 + slot * 4)) != 0) continue; int bitmapPage = Allocate(); var bitmap = new byte[_channel.PageSize]; - bitmap[0] = (byte)PageType.PageUsageBitmap; - bitmap[1] = 1; + PageHeader.WriteType(bitmap, PageType.PageUsageBitmap); _channel.WritePage(bitmapPage, bitmap); BinaryPrimitives.WriteInt32LittleEndian(reference.AsSpan(1 + slot * 4), bitmapPage); added = true; @@ -329,7 +328,7 @@ private void ClearReleasedMap(MapRecord released) public void ValidateGlobalMaps() => ReadGlobalMaps(); /// Allocates from a reference-type global free map (huge databases): the record is a list of - /// pointers to dedicated bitmap pages (type 0x05), pointer k covering the page range starting at + /// pointers to dedicated bitmap pages (type 0x0105), pointer k covering the page range starting at /// k × (pageSize − 4) × 8. A **set bit is a free page** (the global map's sense, opposite of a /// per-table owned map). Finds the first free page, clears its bit on the bitmap page, and returns it; /// grows the file when no bitmap records a free page. @@ -469,8 +468,7 @@ private int AppendUnreleasedPage(ReleasedPages releasedPages) private void WriteNewGlobalBitmap(int number, int range) { var bitmap = new byte[_channel.PageSize]; - bitmap[0] = (byte)PageType.PageUsageBitmap; - bitmap[1] = 1; + PageHeader.WriteType(bitmap, PageType.PageUsageBitmap); int span = (_channel.PageSize - BitmapPageHeaderSize) * 8; int firstFree = Math.Clamp(_channel.PageCount - range * span, 0, span); for (int bit = firstFree; bit < span; bit++) @@ -536,7 +534,7 @@ private MapRecord ReadMapRecord(string name, (int Row, int Page) pointer) throw new InvalidDataException( $"Page 0's global {name} map pointer names page {pointer.Page}, outside the file's pages 1..{_channel.PageCount - 1}."); PageBuffer buffer = _channel.ReadPage(pointer.Page); - if (buffer.Span[0] != (byte)PageType.DataPage) + if (PageHeader.ReadType(buffer.Span) != PageType.DataPage) throw new InvalidDataException( $"Page 0's global {name} map pointer names page {pointer.Page}, which is not a data page."); var data = new DataPage(); @@ -593,7 +591,7 @@ private byte[] ValidateBitmapPage(int pageNumber, MapRecord free, MapRecord rele { ValidateReusablePage(pageNumber, "usage-map bitmap pointer", free, released, _channel.PageCount - 1); byte[] page = _channel.ReadPage(pageNumber).Span.ToArray(); - if (page[0] != (byte)PageType.PageUsageBitmap || page[1] != 0x01 || page[2] != 0 || page[3] != 0) + if (PageHeader.ReadType(page) != PageType.PageUsageBitmap || page[2] != 0 || page[3] != 0) throw new InvalidDataException($"Global map pointer {pageNumber} does not target a valid bitmap page."); return page; } diff --git a/src/LibRed/LibRed.Core/Storage/RowInserter.cs b/src/LibRed/LibRed.Core/Storage/RowInserter.cs index 7ab5ff37..12378e43 100644 --- a/src/LibRed/LibRed.Core/Storage/RowInserter.cs +++ b/src/LibRed/LibRed.Core/Storage/RowInserter.cs @@ -500,15 +500,15 @@ private void ReclaimRelocationTarget(JetFormatBase format, byte[] sourcePage, in /// reclaiming a hidden relocation target is kept, as ACE keeps it (see ). /// /// - /// A page emptied by DELETE is stamped (0x09), taken + /// A page emptied by DELETE is stamped (0x0109), taken /// out of the table's owned and free maps, and released — held until this handle closes, the route ACE /// takes for a delete's pages. Nothing else about the page changes: the row count stands and every slot /// stays the 0-length deleted+overflow tombstone left, which is already what ACE /// writes, so the stamp is the only differing byte. /// Measured against ACE over a 1,200-row table with 699 rows deleted from the low end: 18 data pages - /// emptied, and on each one ACE changed exactly the type byte, cleared the page from both of the table's + /// emptied, and on each one ACE changed exactly the type's low byte, cleared the page from both of the table's /// maps, and set its bit in the global map. This is the same event as an emptied packed long-value page - /// () and carries the same stamp — 0x09 is not confined to + /// () and carries the same stamp — 0x0109 is not confined to /// long-value pages, and these carry the table's own owner at 0x04 rather than an LVAL /// signature. /// "Emptied" is every slot deleted and zero-length, not merely every slot deleted: a hidden @@ -519,7 +519,7 @@ private void ReclaimRelocationTarget(JetFormatBase format, byte[] sourcePage, in private bool WriteReclaimedPage(JetFormatBase format, byte[] page, int pageNumber) { bool release = IsEmptied(format, page) && !IsFirstDataPage(pageNumber); - if (release) page[0] = (byte)PageType.ReleasedDataPage; + if (release) PageHeader.WriteType(page, PageType.ReleasedDataPage); _channel.WritePage(pageNumber, page.AsSpan(0, format.PageSize)); if (!release) return false; @@ -702,7 +702,7 @@ private void FreeLongValue(ColumnDef column, byte[] descriptor, bool releaseAtCl var reader = new LongValueReader(_channel); _ = reader.ResolveWithPages(descriptor, out IReadOnlyList pages); HashSet ownedPages = MapPages(owned.Row, owned.Page).ToHashSet(); - _ = MapPages(free.Row, free.Page); // validate both mutation targets before the first free + if (free.Page != 0) _ = MapPages(free.Row, free.Page); // validate both mutation targets before the first free foreach (int page in pages) if (!ownedPages.Contains(page)) throw new InvalidDataException( @@ -716,7 +716,7 @@ private void FreeLongValue(ColumnDef column, byte[] descriptor, bool releaseAtCl if (releaseAtClose) allocator.Release(page); else allocator.Free(page); _usageMaps.SetBit(owned.Row, owned.Page, page, set: false); - _usageMaps.SetBit(free.Row, free.Page, page, set: false); + if (free.Page != 0) _usageMaps.SetBit(free.Row, free.Page, page, set: false); } } @@ -724,7 +724,7 @@ private void FreeLongValue(ColumnDef column, byte[] descriptor, bool releaseAtCl /// Retires one value from a shared (single-page form) long-value page: its row becomes a 0-length /// deleted+overflow tombstone and the page is re-laid, the surviving records packing from the page end /// in slot order so the freed space is reclaimed. When nothing live is left the page is given back — its - /// type byte set to , its bit cleared from the column's owned + /// type set to , its bit cleared from the column's owned /// and free maps, and the page returned to the global allocator. /// /// @@ -779,18 +779,19 @@ private void ReleasePackedValue(ColumnDef column, int pageNumber, int row, (ushort)(offset - (dir + holder.RowCount * 2))); bool emptied = records.All(r => r.Length == 0); - if (emptied) page[0] = (byte)PageType.ReleasedDataPage; + if (emptied) PageHeader.WriteType(page, PageType.ReleasedDataPage); _channel.WritePage(pageNumber, page); // A page that survives has room again, so it goes back into the column's free-pages map — the same - // map TryAppend consults when looking for somewhere to pack the next small value. + // map TryAppend consults when looking for somewhere to pack the next small value. A column with no + // free-pages map packs nothing, so there it simply stays owned. if (!emptied) { - _usageMaps.SetBit(free.Row, free.Page, pageNumber, set: true); + if (free.Page != 0) _usageMaps.SetBit(free.Row, free.Page, pageNumber, set: true); return; } _usageMaps.SetBit(owned.Row, owned.Page, pageNumber, set: false); - _usageMaps.SetBit(free.Row, free.Page, pageNumber, set: false); + if (free.Page != 0) _usageMaps.SetBit(free.Row, free.Page, pageNumber, set: false); if (releaseAtClose) new PageAllocator(_channel).Release(pageNumber); else new PageAllocator(_channel).Free(pageNumber); } @@ -915,8 +916,7 @@ private static void EnsureRecordFits(JetFormatBase format, byte[] record) int pageNumber = new PageAllocator(_channel).Allocate(); var page = new byte[format.PageSize]; - page[0] = (byte)PageType.DataPage; - page[1] = 0x01; // page flags (observed constant) + PageHeader.WriteType(page, PageType.DataPage); BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.DataOwnerOffset, 4), _table.DefinitionPage); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2), 0); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), @@ -1141,19 +1141,23 @@ public byte[] StorePackedLongValue(int columnId, byte[] payload) /// free-pages map with room), the way Access shares a page across many small values; only when none has /// room is a fresh page allocated (owned + free). A value larger than one page is chained across /// dedicated pages. + /// A column with an owned-pages map and no free-pages map (a free pointer to page 0) never shares + /// a page: each value takes a fresh one, recorded in the owned map alone. Access writes MSysNameMap.NameMap + /// and MSysAccessXML.LValue that way, and packs nothing into them — measured across every such file, + /// one LVAL page per stored value. /// private byte[] StoreLongValue(LongValueWriter writer, byte[] payload, (int Row, int Page) owned, (int Row, int Page) free) { - if (owned.Page == 0 || free.Page == 0) - throw new InvalidDataException("Long-value column has no complete owned/free usage-map pointers."); + if (owned.Page == 0) + throw new InvalidDataException("Long-value column has no owned-pages usage-map pointer."); _ = MapPages(owned.Row, owned.Page); // validate both map targets before allocating or writing LVAL pages - IReadOnlyList freePages = MapPages(free.Row, free.Page); + IReadOnlyList freePages = free.Page == 0 ? [] : MapPages(free.Row, free.Page); if (payload.Length > LongValueFormat.MaxSinglePageValue) { LongValueResult chained = writer.Write(payload); foreach (int page in chained.OwnedPages) _usageMaps.SetBit(owned.Row, owned.Page, page, set: true); - if (chained.FreePage != 0) + if (chained.FreePage != 0 && free.Page != 0) _usageMaps.SetBit(free.Row, free.Page, chained.FreePage, set: true, movableWindow: true); return chained.Descriptor; } @@ -1170,7 +1174,8 @@ private byte[] StoreLongValue(LongValueWriter writer, byte[] payload, (int Row, // No free page had room: a fresh page (owned, and free — it still has spare room). int newPage = writer.WriteNewPage(payload); _usageMaps.SetBit(owned.Row, owned.Page, newPage, set: true); - _usageMaps.SetBit(free.Row, free.Page, newPage, set: true, movableWindow: true); + if (free.Page != 0) + _usageMaps.SetBit(free.Row, free.Page, newPage, set: true, movableWindow: true); return LongValueWriter.SinglePageDescriptor(payload.Length, newPage, 0); } @@ -1213,7 +1218,7 @@ private List MapPages(int mapRow, int mapPage) if (bitmapPage <= 1 || bitmapPage >= _channel.PageCount || !bitmapPages.Add(bitmapPage)) throw new InvalidDataException($"Long-value usage map has invalid bitmap-page pointer {bitmapPage}."); ReadOnlySpan bitmap = _channel.ReadPageShared(bitmapPage).Span; - if (bitmap[0] != (byte)PageType.PageUsageBitmap || bitmap[1] != 0x01 || bitmap[2] != 0 || bitmap[3] != 0) + if (PageHeader.ReadType(bitmap) != PageType.PageUsageBitmap || bitmap[2] != 0 || bitmap[3] != 0) throw new InvalidDataException($"Long-value usage-map pointer {bitmapPage} is not a bitmap page."); UsageMapBits.Append(result, bitmap[4..], i * pagesPerBitmap, _channel.PageCount, "Long-value usage map"); } diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 60717419..de056101 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -1137,12 +1137,12 @@ private bool DropTable(string tableName, bool keepComplexColumns) RetireMapRecords(retire, maps, owned); - // Access marks the released definition page itself: its type byte becomes 0x08 and nothing else on + // Access marks the released definition page itself: its type becomes 0x0108 and nothing else on // the page changes, so the old definition is still sitting there when Compact comes to reclaim it. - // Measured across an ACE DROP TABLE: exactly one byte of the 4,096 differs. Only the TDEF is marked — - // the data, long-value and map-holder pages ACE frees keep their 0x01. + // Measured across an ACE DROP TABLE: exactly one byte of the 4,096 differs, the type's low byte. Only + // the TDEF is marked — the data, long-value and map-holder pages ACE frees keep their 0x0101. byte[] released = _channel.ReadPage(tdefPage).Span.ToArray(); - released[0] = (byte)PageType.ReleasedTableDefinition; + PageHeader.WriteType(released, PageType.ReleasedTableDefinition); _channel.WritePage(tdefPage, released); foreach (int page in owned) @@ -1446,6 +1446,97 @@ private void WriteObjectProperties(int objectId, byte[] properties) } } + /// The LvProp property holding an object's copy of its Name AutoCorrect map. + private const string NameMapProperty = "NameMap"; + + /// Every MSysNameMap row, its map decoded; empty when the file has no such table. + public IReadOnlyList ReadNameMapRows() + { + if (_catalog.FindTable("MSysNameMap") is not { } def) return []; + int guid = def.RequireColumn("GUID").Index, id = def.RequireColumn("Id").Index, + name = def.RequireColumn("Name").Index, type = def.RequireColumn("Type").Index, + map = def.RequireColumn("NameMap").Index; + return [.. new Table(_channel, def).Rows().Select(r => new NameMapRow( + (Guid)r[guid]!, (int)r[id]!, ((string)r[name]!).TrimEnd('\0'), (int)r[type]!, + r[map] is byte[] blob ? NameMap.ReadRow(blob) : null))]; + } + + /// Replaces the map of the MSysNameMap row whose GUID is , + /// and its Name when is given — stored with the trailing NUL Access gives it. + /// Returns false when there is no such row; a row is never added, since its Id is not understood. + public bool WriteNameMapRow(Guid objectGuid, NameMap map, string? name) + { + if (_catalog.FindTable("MSysNameMap") is not { } def) return false; + int guidIndex = def.RequireColumn("GUID").Index, nameIndex = def.RequireColumn("Name").Index; + ColumnDef mapColumn = def.RequireColumn("NameMap"); + var table = new Table(_channel, def); + + foreach ((RowId id, object?[] values) in table.RowsWhere([guidIndex], v => v[guidIndex] is Guid g && g == objectGuid).ToList()) + { + // A long value, like LvProp: stored first, the row given the descriptor (see WriteObjectProperties). + byte[] descriptor = new RowInserter(_channel, def).StorePackedLongValue(mapColumn.ColumnId, map.WriteRow()); + values[mapColumn.Index] = new LongValueDescriptor(descriptor); + var changed = new HashSet { mapColumn.Index }; + if (name is not null) + { + values[nameIndex] = name + "\0"; + changed.Add(nameIndex); + } + table.Update(id, values, changed); + return true; + } + return false; + } + + /// The NameMap property of the object named of + /// MSysObjects.Type ; null when the object has none. + /// There is no such object. + public NameMap? ReadNameMapProperty(string objectName, short objectType) + { + byte[]? blob = ReadObjectProperties(RequireObjectId(objectName, objectType)); + return blob is { Length: > 0 } + && PropertyBlob.Read(blob).FirstOrDefault(p => p.IsOwnedBy("") && p.Name == NameMapProperty) is { RawValue: { } raw } + ? NameMap.ReadProperty(raw) + : null; + } + + /// Sets the object's NameMap property to , or removes it when null. An + /// existing entry keeps its place and flag byte; a new one takes the flag 0x00 Access gives it. Every + /// other property is left as it was. + /// There is no such object. + public void WriteNameMapProperty(string objectName, short objectType, NameMap? map) + { + int objectId = RequireObjectId(objectName, objectType); + byte[] blob = ReadObjectProperties(objectId) ?? []; + var properties = PropertyBlob.Read(blob).ToList(); + int at = properties.FindIndex(p => p.IsOwnedBy("") && p.Name == NameMapProperty); + if (map is null) + { + if (at < 0) return; + properties.RemoveAt(at); + } + else + { + PropertyBlob.Property property = PropertyBlob.Of("", NameMapProperty, map.WriteProperty(), JetDataType.Ole); + if (at >= 0) properties[at] = property with { Flags = properties[at].Flags, Block = properties[at].Block }; + else properties.Add(property with { Flags = 0 }); + } + WriteObjectProperties(objectId, PropertyBlob.Write(properties, blob)); + } + + /// The MSysObjects.Id of the object with this name and type. + private int RequireObjectId(string name, short type) + { + TableDef msys = _catalog.RequireTable("MSysObjects"); + int idIndex = msys.RequireColumn("Id").Index, nameIndex = msys.RequireColumn("Name").Index, + typeIndex = msys.RequireColumn("Type").Index; + var objects = new Table(_channel, msys); + foreach (object?[] row in objects.Rows(objects.DecodeOnly([idIndex, nameIndex, typeIndex]))) + if (row[typeIndex] is short t && t == type && NameMatches(row[nameIndex], name)) + return (int)row[idIndex]!; + throw new InvalidOperationException($"There is no object '{name}' of type {type}."); + } + /// MSysObjects and the two columns every extended-property path works through: the /// Id it matches an object by, and the LvProp holding the blob. private (TableDef Definition, Table Table, int IdIndex, ColumnDef LvProp) ObjectProperties() @@ -3420,8 +3511,7 @@ private void WriteDefinition(int firstPage, byte[] def, IReadOnlyList oldCo { var (offset, length, free) = chunks[i]; var page = new byte[ps]; - page[0] = (byte)PageType.TableDefinition; - page[1] = 0x01; + PageHeader.WriteType(page, PageType.TableDefinition); if (length > 0) // a page holding only the reserve starts past the definition's end Array.Copy(def, offset, page, JetFormatBase.TdefContinuationHeaderSize, length); int next = i + 1 < pageNumbers.Length ? pageNumbers[i + 1] : 0; @@ -3536,8 +3626,7 @@ private void WriteEmptyLeafIndexPage(JetFormatBase format, int pageNumber, int o const int OwnerOffset = 0x04; var page = new byte[format.PageSize]; - page[0] = (byte)PageType.LeafIndexPage; - page[1] = 0x01; // page flags (observed constant) + PageHeader.WriteType(page, PageType.LeafIndexPage); BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(OwnerOffset, 4), owner); // No entries: empty mask, no prefix compression, free space is the whole entry region. BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), @@ -3559,8 +3648,7 @@ private void WriteUsageMaps(JetFormatBase format, int pageNumber, int mapCount) const int MapLength = UsageMapRecordLength; var page = new byte[format.PageSize]; - page[0] = (byte)PageType.DataPage; - page[1] = 0x01; // page flags (observed constant) + PageHeader.WriteType(page, PageType.DataPage); // Owner of a usage-map page is 0 (it belongs to no table). int offset = format.PageSize; diff --git a/src/LibRed/LibRed.Core/Storage/UsageMap.cs b/src/LibRed/LibRed.Core/Storage/UsageMap.cs index fc135178..31beca07 100644 --- a/src/LibRed/LibRed.Core/Storage/UsageMap.cs +++ b/src/LibRed/LibRed.Core/Storage/UsageMap.cs @@ -13,7 +13,7 @@ namespace LibRed.Storage; /// The TDEF holds a pointer (row + page) to the usage-map record. An inline map /// (type 0x00) stores a start page and a bitmap where bit i marks page (startPage + i) /// as owned. A reference map (type 0x01, for very large tables) instead stores a list of -/// pointers to dedicated bitmap pages (type 0x05); pointer k's bitmap covers the page +/// pointers to dedicated bitmap pages (page type 0x0105); pointer k's bitmap covers the page /// range starting at k * (pageSize - 4) * 8. /// public sealed class UsageMap(PageChannel channel, TableDef table) @@ -23,7 +23,7 @@ public sealed class UsageMap(PageChannel channel, TableDef table) private const int ReferenceMapSlots = 17; private const int ReferenceMapRecordSize = 1 + ReferenceMapSlots * 4; - /// Bytes preceding the bitmap on a dedicated usage-bitmap page (type 0x05). + /// Bytes preceding the bitmap on a dedicated usage-bitmap page (type 0x0105). private const int BitmapPageHeaderSize = 4; private readonly PageChannel _channel = channel; @@ -43,7 +43,7 @@ public sealed class UsageMap(PageChannel channel, TableDef table) /// pages holding a table's Memo/OLE content are invisible to . public IEnumerable PagesInMap(int mapRow, int mapPage) => ReadMapAt(mapRow, mapPage); - /// The dedicated bitmap pages (type 0x05) a reference-form map record at the pointer names, each + /// The dedicated bitmap pages (type 0x0105) a reference-form map record at the pointer names, each /// validated; none for an inline record. public IReadOnlyList BitmapPagesOf(int mapRow, int mapPage) { @@ -227,7 +227,7 @@ private ReadOnlySpan ReadBitmapPage(int pageNumber) throw new InvalidDataException($"Usage-map bitmap page {pageNumber} is outside the database."); ReadOnlySpan page = _channel.ReadPage(pageNumber).Span; - if (page[0] != (byte)PageType.PageUsageBitmap || page[1] != 0x01 || page[2] != 0 || page[3] != 0) + if (PageHeader.ReadType(page) != PageType.PageUsageBitmap || page[2] != 0 || page[3] != 0) throw new InvalidDataException($"Usage-map pointer {pageNumber} does not reference a valid bitmap page."); return page[BitmapPageHeaderSize..]; } diff --git a/src/LibRed/LibRed.Core/Storage/UsageMapWriter.cs b/src/LibRed/LibRed.Core/Storage/UsageMapWriter.cs index 62461cb2..45b3b988 100644 --- a/src/LibRed/LibRed.Core/Storage/UsageMapWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/UsageMapWriter.cs @@ -187,7 +187,7 @@ private bool TryRepositionWindow(byte[] page, DataPage holder, JetFormatBase for return true; } - /// Number of pages one dedicated bitmap page (type 0x05) covers. + /// Number of pages one dedicated bitmap page (type 0x0105) covers. private int PagesPerBitmapPage => (_channel.Format.PageSize - BitmapPageHeaderSize) * 8; /// Sets or clears 's bit in a reference map: pointer slot @@ -229,7 +229,7 @@ private void SetBitmapPageBit(int bitmapPage, int bit, bool set) $"Usage map names bitmap page {bitmapPage}, outside the file's 2..{_channel.PageCount - 1} range."); byte[] bitmap = _channel.ReadPage(bitmapPage).Span.ToArray(); - if (bitmap[0] != (byte)PageType.PageUsageBitmap || bitmap[1] != 0x01 || bitmap[2] != 0 || bitmap[3] != 0) + if (PageHeader.ReadType(bitmap) != PageType.PageUsageBitmap || bitmap[2] != 0 || bitmap[3] != 0) throw new InvalidDataException( $"Page {bitmapPage} is named as a usage bitmap but does not carry the [05 01 00 00] header."); @@ -240,13 +240,12 @@ private void SetBitmapPageBit(int bitmapPage, int bit, bool set) _channel.WritePage(bitmapPage, bitmap); } - /// Allocates and initialises an empty dedicated usage-bitmap page (type 0x05). + /// Allocates and initialises an empty dedicated usage-bitmap page (type 0x0105). private int AllocateBitmapPage() { int pageNumber = new PageAllocator(_channel).Allocate(); var bitmap = new byte[_channel.Format.PageSize]; - bitmap[0] = (byte)PageType.PageUsageBitmap; - bitmap[1] = 0x01; // page flags (observed constant, as on data pages) + PageHeader.WriteType(bitmap, PageType.PageUsageBitmap); _channel.WritePage(pageNumber, bitmap); return pageNumber; } diff --git a/src/LibRed/docs/format/README.md b/src/LibRed/docs/format/README.md index 01ee2251..aa2c3c30 100644 --- a/src/LibRed/docs/format/README.md +++ b/src/LibRed/docs/format/README.md @@ -28,16 +28,16 @@ Implemented by `src/LibRed/LibRed.Core/`. The canonical offsets live in | File | Covers | | --- | --- | | [page-00-database.md](page-00-database.md) | Page 0 — the database-definition page (format id, version byte) | -| [page-01-data-and-rows.md](page-01-data-and-rows.md) | Data page (type `0x01`) header + slot directory, and the inline **row record** format | -| [page-02a-tdef.md](page-02a-tdef.md) | Table-definition page (type `0x02`): header, multi-page chaining, body layout, and writing a TDEF Access accepts | +| [page-01-data-and-rows.md](page-01-data-and-rows.md) | Data page (type `0x0101`) header + slot directory, and the inline **row record** format | +| [page-02a-tdef.md](page-02a-tdef.md) | Table-definition page (type `0x0102`): header, multi-page chaining, body layout, and writing a TDEF Access accepts | | [page-02b-columns.md](page-02b-columns.md) | TDEF **columns**: the 25-byte descriptor, and column maintenance incl. the in-place **`ALTER COLUMN`** type/length change | | [page-02c-default-values.md](page-02c-default-values.md) | Column **`DEFAULT`** value *semantics* — what an expression may contain, the DDL-parser-vs-expression-service split (engine behaviour; the on-disk `LvProp` storage is in [system-catalog.md](system-catalog.md)) | | [page-02d-constraints.md](page-02d-constraints.md) | TDEF **indexes / keys / constraints**: index-data, index-info and stats blocks (PK / unique / FK metadata) | | [page-02e-calculated-columns.md](page-02e-calculated-columns.md) | **Calculated column** *semantics* — the expression language ACE accepts, when the cached result is recomputed, and what DDL may do to one (engine behaviour; the descriptor and value envelope are in [page-02b](page-02b-columns.md)) | -| [page-03-04-index-btree.md](page-03-04-index-btree.md) | Index B-tree pages (types `0x03` node / `0x04` leaf): header, entries, prefix compression, key encoding, splitting | -| [page-05-usage-maps.md](page-05-usage-maps.md) | Per-table owned/free usage maps, `0x05` bitmap pages, and the global free-pages map (allocation) | -| [page-08-released-tdef.md](page-08-released-tdef.md) | Released table-definition page (type `0x08`): what `DROP TABLE` leaves behind | -| [page-09-released-data.md](page-09-released-data.md) | Released data page (type `0x09`): a table data page emptied by DELETE, or a packed LVAL page emptied of its values | +| [page-03-04-index-btree.md](page-03-04-index-btree.md) | Index B-tree pages (types `0x0103` node / `0x0104` leaf): header, entries, prefix compression, key encoding, splitting | +| [page-05-usage-maps.md](page-05-usage-maps.md) | Per-table owned/free usage maps, `0x0105` bitmap pages, and the global free-pages map (allocation) | +| [page-08-released-tdef.md](page-08-released-tdef.md) | Released table-definition page (type `0x0108`): what `DROP TABLE` leaves behind | +| [page-09-released-data.md](page-09-released-data.md) | Released data page (type `0x0109`): a table data page emptied by DELETE, or a packed LVAL page emptied of its values | | [long-values.md](long-values.md) | Memo / OLE long values, LVAL pages, and the per-column usage-map list | | [data-types.md](data-types.md) | Data-type codes and their decode, plus compressed Unicode | | [system-catalog.md](system-catalog.md) | `MSysObjects` / `MSysACEs` / `MSysQueries` / `MSysRelationships`, the `LvProp` property blob, views & procedures, relationships | @@ -54,22 +54,34 @@ catalogued one level up in [`../functions.md`](../functions.md). which are big-endian (noted in the index B-tree file). - **Page size:** 4096 bytes (Jet 4 / ACE). Jet 3 is 2048. - **Pages** are numbered from 0; a page's byte offset in the file is `pageNumber * pageSize`. -- **Page type** is the first byte of every page: - - | Byte | Page type | LibRed | File | - | --- | --- | --- | --- | - | `0x00` | Database definition (page 0 only) | `DatabaseDefinitionPage` | [page-00](page-00-database.md) | - | `0x01` | Data page (also long-value/LVAL pages) | `DataPage` | [page-01](page-01-data-and-rows.md) / [long-values](long-values.md) | - | `0x02` | Table definition (TDEF) | `TableDefinitionPage` | [page-02a](page-02a-tdef.md) | - | `0x03` | Index B-tree node (intermediate) | `IndexCursor` | [page-03-04](page-03-04-index-btree.md) | - | `0x04` | Index B-tree leaf | `IndexCursor` | [page-03-04](page-03-04-index-btree.md) | - | `0x05` | Page-usage bitmap | `UsageMap` | [page-05](page-05-usage-maps.md) | - | `0x08` | Released table definition (a dropped table's TDEF) | `PageType.ReleasedTableDefinition` | [page-08](page-08-released-tdef.md) | - | `0x09` | Released data page (emptied by DELETE, or of its packed long values) | `PageType.ReleasedDataPage` | [page-09](page-09-released-data.md) | - - `0x08` and `0x09` mark pages that have been **given back**. Neither needs handling on read — allocation - selects on the global free map, not on this byte — but both are written, so a file LibRed produces carries - the same markers Access would. +- **Page type** is the **16-bit** little-endian word at offset `0x00` of every page. Its high byte (file + offset `0x01`) is `0x01` on every page Jet and ACE write, page 0 included — it is not a separate "flags" + byte but half of the type, and ACE compares the whole word (verified: a data page whose `0x01` byte is + anything else is not read as one). + + | Word | Bytes | Page type | LibRed | File | + | --- | --- | --- | --- | --- | + | `0x0100` | `00 01` | Database definition (page 0 only) | `DatabaseDefinitionPage` | [page-00](page-00-database.md) | + | `0x0101` | `01 01` | Data page (also long-value/LVAL pages) | `DataPage` | [page-01](page-01-data-and-rows.md) / [long-values](long-values.md) | + | `0x0102` | `02 01` | Table definition (TDEF) | `TableDefinitionPage` | [page-02a](page-02a-tdef.md) | + | `0x0103` | `03 01` | Index B-tree node (intermediate) | `IndexCursor` | [page-03-04](page-03-04-index-btree.md) | + | `0x0104` | `04 01` | Index B-tree leaf | `IndexCursor` | [page-03-04](page-03-04-index-btree.md) | + | `0x0105` | `05 01` | Page-usage bitmap | `UsageMap` | [page-05](page-05-usage-maps.md) | + | `0x0106` | `06 01` | *Unknown* — never seen in a file, but ACE treats it as row-bearing (below) | — | — | + | `0x0107` | `07 01` | *Unknown* — likewise | — | — | + | `0x0108` | `08 01` | Released table definition (a dropped table's TDEF) | `PageType.ReleasedTableDefinition` | [page-08](page-08-released-tdef.md) | + | `0x0109` | `09 01` | Released data page (emptied by DELETE, or of its packed long values) | `PageType.ReleasedDataPage` | [page-09](page-09-released-data.md) | + + `0x0108` and `0x0109` mark pages that have been **given back**. Neither needs handling on read — allocation + selects on the global free map, not on the type — but both are written, so a file LibRed produces carries + the same markers Access would. A page never written reads as all zeros, word `0x0000`. + + **Which types ACE reads as rows (verified).** Scanning a table, ACE parses a page its owned-pages map names + as a data page when the word is `0x0101`, `0x0103`, `0x0104`, `0x0106`, `0x0107` or `0x0109`, and skips + it — silently, rows and all — for every other word: `0x0100`, `0x0102`, `0x0105`, `0x0108`, `0x0000`, any + word whose high byte is not `0x01`, and a page past the end of the file. Nothing else about the page is + checked: not its owner field (`0x04`), which may name another table or `LVAL`, and not the free-space + count (`0x02`). A page the map names but the file no longer holds is therefore lost without an error. --- diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index 901b320e..f417f4e2 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -6,18 +6,19 @@ All integers little-endian unless noted; offsets are hex, relative to the struct --- -## Page header (first byte = page type) +## Page header (first word = page type) -| Byte `0x00` | Page type | +| Word at `0x00` (LE) | Page type | | --- | --- | -| `0x00` | Database definition (page 0 only) | -| `0x01` | Data page (also LVAL long-value pages) | -| `0x02` | Table definition (TDEF) | -| `0x03` | Index B-tree node | -| `0x04` | Index B-tree leaf | -| `0x05` | Page-usage bitmap | -| `0x08` | Released table definition — a dropped table's TDEF, otherwise unchanged | -| `0x09` | Released long-value page — a packed LVAL page emptied of its values | +| `0x0100` | Database definition (page 0 only) | +| `0x0101` | Data page (also LVAL long-value pages) | +| `0x0102` | Table definition (TDEF) | +| `0x0103` | Index B-tree node | +| `0x0104` | Index B-tree leaf | +| `0x0105` | Page-usage bitmap | +| `0x0106`, `0x0107` | Unknown — never seen; ACE reads them as row-bearing | +| `0x0108` | Released table definition — a dropped table's TDEF, otherwise unchanged | +| `0x0109` | Released data page — emptied by DELETE, or a packed LVAL page emptied of its values | --- @@ -25,8 +26,8 @@ All integers little-endian unless noted; offsets are hex, relative to the struct | Offset | Size | Meaning | | --- | --- | --- | -| `0x00` | 1 | Page type `0x00` | -| `0x01` | 3 | Unknown (observed `01 00 00`, constant) | +| `0x00` | 2 | Page type `0x0100` | +| `0x02` | 2 | Unknown (zero) | | `0x04` | 15 | Format id ASCII: `Standard Jet DB` (`0x00`/`0x01`) / `Jet System DB` (`0x01`) / `Standard ACE DB` (`0x02`+) | | `0x13` | 1 | NUL terminator of the id string | | `0x14` | 1 | Version byte (`0x00` Jet3, `0x01` Jet4, `0x02` ACE12, `0x03` ACE14, `0x04` ACE15/2013 never emitted and refused by ACE, `0x05` ACE16, `0x06` ACE17) | @@ -53,13 +54,12 @@ All integers little-endian unless noted; offsets are hex, relative to the struct --- -## Data page — type `0x01` → [page-01](page-01-data-and-rows.md) +## Data page — type `0x0101` → [page-01](page-01-data-and-rows.md) | Offset | Size | Meaning | | --- | --- | --- | -| `0x00` | 1 | Page type `0x01` | -| `0x01` | 1 | Flags (`0x01`) | -| `0x02` | 2 | Free space | +| `0x00` | 2 | Page type `0x0101` | +| `0x02` | 2 | Free-space count — bytes still free on the page | | `0x04` | 4 | Owning TDEF page — or ASCII `LVAL` (`0x4C41564C`) for long-value pages | | `0x08` | 4 | Jet4-only; zero except on the **first page of a long-value chain**, where it is the chain stamp matching the pointing descriptor's `0x08` | | `0x0C` | 2 | Row count | @@ -75,13 +75,12 @@ Variable section (`varOffsetTable`+`numVar`) omitted only when the table has nev --- -## TDEF header — type `0x02` → [page-02a](page-02a-tdef.md) +## TDEF header — type `0x0102` → [page-02a](page-02a-tdef.md) | Offset | Size | Meaning | | --- | --- | --- | -| `0x00` | 1 | Page type `0x02` | -| `0x01` | 1 | Flags (`0x01`) | -| `0x02` | 2 | Free space | +| `0x00` | 2 | Page type `0x0102` | +| `0x02` | 2 | Free-space count — bytes still free on the page | | `0x04` | 4 | Next TDEF page (0 = single page) | | `0x08` | 4 | TDEF length (total logical bytes) | | `0x0C` | 4 | Constant marker `0x00000659` | @@ -209,19 +208,18 @@ descriptor. **Inline (type `0x00`):** `[0x00][startPage:4][bitmap…]` — bit `i` ⇒ page `startPage+i` owned. **Reference (type `0x01`, 69 bytes):** `[0x01][17 × 4-byte bitmap-page pointers]`. -**Bitmap page (type `0x05`):** header `[0x05][0x01][0][0]`, bitmap from offset 4. +**Bitmap page (page type `0x0105`):** header `[05 01][00 00]` — the type word, then two zero bytes — bitmap from offset 4. Global maps, located by page 0: free pages at `0x18` (page 1 row 0 as ACE writes it) — set bit = **free** (opposite of a table map); released pages at `0x1C` (page 1 row 1) — set bit = freed, not reusable until close. --- -## Index B-tree page header — types `0x03` / `0x04` → [page-03-04](page-03-04-index-btree.md) +## Index B-tree page header — types `0x0103` / `0x0104` → [page-03-04](page-03-04-index-btree.md) | Offset | Size | Meaning | | --- | --- | --- | -| `0x00` | 1 | Page type `0x03` node / `0x04` leaf | -| `0x01` | 1 | Flags (`0x01`) | -| `0x02` | 2 | Free space | +| `0x00` | 2 | Page type `0x0103` node / `0x0104` leaf | +| `0x02` | 2 | Free-space count — bytes still free on the page | | `0x04` | 4 | Owning TDEF page | | `0x08` | 4 | Jet4-inserted field (zero); shifts the following fields +4 vs Jet3 | | `0x0C` | 4 | Previous leaf page (0 = leftmost); a split node's left sibling | diff --git a/src/LibRed/docs/format/long-values.md b/src/LibRed/docs/format/long-values.md index 8a6a37e7..22ade93d 100644 --- a/src/LibRed/docs/format/long-values.md +++ b/src/LibRed/docs/format/long-values.md @@ -17,7 +17,7 @@ Flags (byte `0x03` masked with `0xC0`; its low six bits belong to the length): - `0x80` **inline** — the payload follows the descriptor in the row. - `0x40` **single LVAL page** — the row at (page, row) *is* the whole payload. Several such values **share** a page; deleting one retires its row to a 0-length deleted + overflow tombstone and re-lays the page, and - the page is released as type `0x09` once the last of them is gone + the page is released as type `0x0109` once the last of them is gone ([page-05 §9](page-05-usage-maps.md)). - `0x00` **multi-page** — the payload is chained across LVAL pages; each chunk's row begins with a 4-byte pointer (`[row:1][page:3]`) to the next chunk (zero on the last), followed by chunk @@ -116,7 +116,7 @@ character at both ends, 536,870,911 accepted and 536,870,912 refused. > chaining. What fixes the boundary at 3816, rather than the 4076 a row can actually hold, is **not > established**; the ~260-byte margin is unexplained. -LVAL pages are data pages (type `0x01`) whose owner field (`0x04`) is the ASCII marker `LVAL`. +LVAL pages are data pages (type `0x0101`) whose owner field (`0x04`) is the ASCII marker `LVAL`. > **Reader and reclamation guardrails.** LibRed requires the complete 12-byte descriptor before reading > its fields, accepts only the three flags above, and bounds inline data against the bytes actually present. @@ -245,6 +245,15 @@ and `MapPages` reads either form back. > additionally allocate the usage-map records and emit a real `{col_num, used, free}` entry per long-value > column — verified against ACE-authored tables. > +> **An entry can carry an owned-pages map and no free-pages map** — its free pointer is page 0. Access writes +> exactly two columns that way, `MSysNameMap.NameMap` and `MSysAccessXML.LValue`; every other long-value +> column examined has both. Such a column **never shares a page**: each value over 64 bytes is on an LVAL +> page of its own, recorded in the owned map alone (measured: one page per stored value in every such file). +> LibRed writes to one the same way — a fresh page per value, owned bit only, nothing packed — and ACE reads +> the result back and keeps it through a compact (verified, single-page and chained values). LibRed retires a +> value from one by clearing its owned bit and releasing the page, there being no free map to update — not +> measured against ACE. +> > The §3.3.2 entry is only strictly *required* once a value spills to LVAL pages — an entry-less table > still round-trips inline values through both LibRed and Access, but Access fails *"Not a valid bookmark"* > writing a 6000-char value into one (nowhere to record the LVAL page). LibRed writes it regardless, so diff --git a/src/LibRed/docs/format/page-00-database.md b/src/LibRed/docs/format/page-00-database.md index 6e09fd42..3a7e916d 100644 --- a/src/LibRed/docs/format/page-00-database.md +++ b/src/LibRed/docs/format/page-00-database.md @@ -6,8 +6,8 @@ | Offset | Size | Meaning | | --- | --- | --- | -| `0x00` | 1 | Page type, `0x00` | -| `0x01` | 3 | Unknown (observed `01 00 00`, constant across Jet 4 and ACE; not decoded) | +| `0x00` | 2 | Page type `0x0100` (bytes `00 01`) — the 16-bit type every page starts with ([README](README.md)) | +| `0x02` | 2 | Unknown (zero on every file examined; not decoded) | | `0x04` | 15 | Format identifier ASCII: `Standard Jet DB` or `Standard ACE DB` | | `0x13` | 1 | NUL terminator of the identifier string | | `0x14` | 1 | Version byte (see below). mdbtools reads `jet_version` as a 4-byte word at `0x14`; the version is its low byte | @@ -409,7 +409,7 @@ key = LE32(pageNumber XOR databaseKey) and the page bytes are the RC4 keystream XOR'd over the plaintext. This is the same per-page key mixing ACE Agile uses (`LE32(pageNumber) XOR encodingKey`), just feeding RC4 directly instead of deriving an AES IV. Verified against a real `System.mdw`: the page-number mixing is XOR, not ADD; every page decrypts to a valid -page-type byte (page 1 → `01` data, pages 2/3 → `02` TDEF, index pages → `04`), `MSysObjects`/`MSysACEs` +page type (page 1 → `0x0101` data, pages 2/3 → `0x0102` TDEF, index pages → `0x0104`), `MSysObjects`/`MSysACEs` parse, and `MSysAccounts` yields the account SIDs in §2.3. Implemented as `LibRed.Crypto.JetLegacyEncryption`; `PageChannel` selects it for non-ACE (`!IsAccdb`) files with a nonzero database key. diff --git a/src/LibRed/docs/format/page-01-data-and-rows.md b/src/LibRed/docs/format/page-01-data-and-rows.md index f87fa07a..e8301ffa 100644 --- a/src/LibRed/docs/format/page-01-data-and-rows.md +++ b/src/LibRed/docs/format/page-01-data-and-rows.md @@ -1,14 +1,13 @@ -# Data pages (type 0x01) and the row record +# Data pages (type 0x0101) and the row record > Part of the [LibRed Jet / ACE file-format reference](README.md). Cross-references use the original **§-numbers**; the [section map](README.md#section-map) says which file each lives in. -## 4. Data page — type `0x01` +## 4. Data page — type `0x0101` | Offset | Size | Meaning | | --- | --- | --- | -| `0x00` | 1 | Page type `0x01` | -| `0x01` | 1 | Flags (observed constant `0x01`; the same byte appears on TDEF and index pages — verified) | -| `0x02` | 2 | Free space | +| `0x00` | 2 | Page type `0x0101` (bytes `01 01`). The whole word is the type — the second byte is not a separate flags field ([README](README.md)) | +| `0x02` | 2 | Free-space count — bytes still free on the page | | `0x04` | 4 | Owning table's TDEF page — **or** the ASCII marker `LVAL` (`0x4C41564C`) for long-value pages | | `0x08` | 4 | Jet4-only. **Zero on every page except the FIRST page of a multi-page long-value chain**, where it carries a stamp that must equal the pointing descriptor's own `0x08` — see [long-values](long-values.md). Jet3 has the row count here instead (which is why Jet4's row count sits 4 bytes later). LibRed writes it and checks it. | | `0x0C` | 2 | Row count on this page. **ACE writes at most 255, whatever the free space** — see the slot-count limit below. | diff --git a/src/LibRed/docs/format/page-02a-tdef.md b/src/LibRed/docs/format/page-02a-tdef.md index 31cdf364..7ba0eba2 100644 --- a/src/LibRed/docs/format/page-02a-tdef.md +++ b/src/LibRed/docs/format/page-02a-tdef.md @@ -1,4 +1,4 @@ -# Table-definition (TDEF) page — type 0x02 +# Table-definition (TDEF) page — type 0x0102 > Part of the [LibRed Jet / ACE file-format reference](README.md). Cross-references use the original **§-numbers**; the [section map](README.md#section-map) says which file each lives in. @@ -6,9 +6,8 @@ | Offset | Size | Meaning | | --- | --- | --- | -| `0x00` | 1 | Page type `0x02` | -| `0x01` | 1 | Flags (observed `0x01`) | -| `0x02` | 2 | Free space remaining in this page | +| `0x00` | 2 | Page type `0x0102` (bytes `02 01`), on the first page and every continuation page | +| `0x02` | 2 | Free-space count — bytes still free on this page | | `0x04` | 4 | Next TDEF page (0 if the definition fits one page) | | `0x08` | 4 | TDEF length (total logical bytes) | | `0x0C` | 4 | Unknown — a constant `0x00000659` (1625) observed in every file | @@ -211,7 +210,7 @@ the documented 255-column, 32-index, and 64-character-name limits. > **Rewriting a multi-page TDEF (verified vs ACE: `ADD COLUMN`, `DROP COLUMN`, `CREATE INDEX`).** Growing or > shrinking, ACE rewrites the **first page in place** but writes the continuation data to **newly allocated -> pages**, and releases the old continuation pages without touching a byte of them (they keep type `0x02`); +> pages**, and releases the old continuation pages without touching a byte of them (they keep type `0x0102`); > they are back in the global free-pages map once the session closes. The new last page is zero past its > data. The **last continuation is allocated first**, each allocation taking the lowest free page: from free > pages `354…` a two-continuation chain became `first → 355 → 354`, and from the end of a file diff --git a/src/LibRed/docs/format/page-03-04-index-btree.md b/src/LibRed/docs/format/page-03-04-index-btree.md index 7c6246ce..a575398f 100644 --- a/src/LibRed/docs/format/page-03-04-index-btree.md +++ b/src/LibRed/docs/format/page-03-04-index-btree.md @@ -1,23 +1,22 @@ -# Index B-tree pages — types 0x03 / 0x04 +# Index B-tree pages — types 0x0103 / 0x0104 > Part of the [LibRed Jet / ACE file-format reference](README.md). Cross-references use the original **§-numbers**; the [section map](README.md#section-map) says which file each lives in. -## 10. Index B-tree pages — types `0x03` (node) and `0x04` (leaf) +## 10. Index B-tree pages — types `0x0103` (node) and `0x0104` (leaf) ### 10.1 Header | Offset | Size | Meaning | | --- | --- | --- | -| `0x00` | 1 | Page type (`0x03` node / `0x04` leaf) | -| `0x01` | 1 | Flags (observed constant `0x01` — verified) | -| `0x02` | 2 | Free space | +| `0x00` | 2 | Page type (`0x0103` node / `0x0104` leaf; bytes `03 01` / `04 01`) | +| `0x02` | 2 | Free-space count — bytes still free on the page | | `0x04` | 4 | Owning table TDEF page | | `0x08` | 4 | **The 4-byte field Jet4 inserted** right after the owner — purpose unknown, **`0` observed** on every ACE- and LibRed-written index page. Inserting it here is what pushes prev/next/tail/compress down by 4 vs Jet3 (see the Jet3→Jet4 note under `0x1B`). | | `0x0C` | 4 | **Previous leaf page** (`0` on the first/leftmost leaf), little-endian. **Verified against ACE:** on an ACE-built split index the higher-key leaf's `0x0C` points back at the lower-key leaf. A node that has split carries the same link to its left sibling at the same level (§10.5); the root never has one. | | `0x10` | 4 | **Next leaf page** (`0` on the last/rightmost leaf), little-endian; on a split node, its right sibling. **Verified against ACE — and load-bearing:** Access's full-table `COUNT(*)`/scan descends to the leftmost leaf and walks this forward chain. If it is wrong (e.g. `next` written at `0x0C`), Access stops after the first leaf and **silently sees only those rows** — a data-loss/corruption hazard, since it then treats the rest of the table's space as free. LibRed maintains `0x0C`/`0x10` across splits (§10.5). (This is **Jet3's `0x0C` next-pointer shifted +4**; the child-tail that mdbtools lists at `0x10` is the *Jet3* tail position — in Jet4 it too shifted to `0x14`.) | | `0x14` | 4 | **Child-tail** page (node pages: the rightmost child, referenced by no entry). **Verified** for Jet4/ACE: the tail pointer read here drives correct multi-level traversal. This is **Jet3's `0x10` tail shifted +4** by the `0x08` insertion, which is exactly why mdbtools (Jet3) documents the tail at `0x10`. | | `0x18` | 2 | Compressed-byte count (shared key prefix length, §10.3). Jet3's `0x14`, shifted +4. | -| `0x1A` | 1 | The **1-byte field Jet4 inserted** just before the bitmask. ACE writes `0` on leaves and `1` on the root of a two-level split index, consistent with a **B-tree level/height** — but **only `0` and `1` have been observed** (no 3-level tree was built against ACE, so `2`+ is a guess). **Required only for leaves (verified):** writing `0x01` on a *leaf* makes ACE fail to open the whole database (`"could not find the object 'Databases'"`). **Node value is cosmetic (verified):** with correct leaf-chain offsets, nodes written with `0x1A=0` *and* prefix-compressed still give ACE the right `COUNT`/`SUM`, so Access reads a node's tail child regardless; leaf vs node is told by the page-type byte at `0x00`. LibRed still writes the height to match ACE byte-for-byte, but the only hard requirements are the leaf-chain offsets and a *leaf's* `0x1A=0`. | +| `0x1A` | 1 | The **1-byte field Jet4 inserted** just before the bitmask. ACE writes `0` on leaves and `1` on the root of a two-level split index, consistent with a **B-tree level/height** — but **only `0` and `1` have been observed** (no 3-level tree was built against ACE, so `2`+ is a guess). **Required only for leaves (verified):** writing `0x01` on a *leaf* makes ACE fail to open the whole database (`"could not find the object 'Databases'"`). **Node value is cosmetic (verified):** with correct leaf-chain offsets, nodes written with `0x1A=0` *and* prefix-compressed still give ACE the right `COUNT`/`SUM`, so Access reads a node's tail child regardless; leaf vs node is told by the page type at `0x00`. LibRed still writes the height to match ACE byte-for-byte, but the only hard requirements are the leaf-chain offsets and a *leaf's* `0x1A=0`. | | `0x1B` | … | Entry-position bitmask. mdbtools **version-labels** this: bitmask at `0x16` (Jet3) / **`0x1B` (Jet4)**. The `+5` Jet3→Jet4 shift is **fully decomposed**: a **4-byte field inserted at `0x08`** (right after the owner) plus the **1-byte B-tree level at `0x1A`** = `+5`. Everything between is Jet3's field shifted by 4 (Jet3 → Jet4): prev `0x08`→`0x0C`, next `0x0C`→`0x10`, child-tail `0x10`→`0x14`, compressed count `0x14`→`0x18`, and the mask `0x16`→`0x1B`, the level accounting for its extra `+1`. No unexplained bytes remain in this header. (The Jet4 *positions* are ACE-verified; that these are exactly the bytes Jet3 lacks is not yet confirmed against a real Jet3 index page.) | | `0x1E0` | — | Start of entry data | @@ -42,7 +41,7 @@ Each entry ends with a **4-byte big-endian** trailing pointer: also recurse into the header's child-tail page (`0x14`). > **Reader/traversal guardrails.** LibRed validates every page number before I/O, requires page type -> `0x03`/`0x04` and a consistent owning TDEF, bounds every bitmask-derived entry before reading its +> `0x0103`/`0x0104` and a consistent owning TDEF, bounds every bitmask-derived entry before reading its > 4-byte trailer *after reconstruction* (§10.3), and requires the compressed prefix to fit the first > entry. Node child/tail, leaf > previous/next, and indexed-row page pointers are checked against the file's page range; optional leaf @@ -99,7 +98,7 @@ omits. Reconstruct: `fullEntry = prefix ++ stored`. > **Compression is optional on leaves.** A `compressedByteCount` of 0 (every entry stored in full) > is a valid *leaf* that Access reads without complaint — verified by rewriting a leaf uncompressed -> and re-seeking it. **Node (`0x03`) pages follow the same cycle as leaves** (below): a node is written +> and re-seeking it. **Node (`0x0103`) pages follow the same cycle as leaves** (below): a node is written > uncompressed until it fills, compressed in place when it does, and split when that is not enough, both halves > then written at the largest prefix they share. So the node of a small two-level tree reads `0x18 = 0` — it has > never filled — while the halves of a split node carry a prefix (verified: 16 separators at prefix 0 with 176 @@ -987,7 +986,7 @@ rather than rewritten empty. Three things change together: the tail and that entry goes. A node is left with **no entries and only its child-tail** rather than collapsed into its remaining child: `entryCount = 0` over a live `0x14` is a valid node. - **The page is released** — cleared from the index's own pages map (`+0x22` of its data block) and returned - to the global map. Its **type byte stays `0x04`**: unlike a released data page ([page-09](page-09-released-data.md)) + to the global map. Its **type stays `0x0104`**: unlike a released data page ([page-09](page-09-released-data.md)) an index page carries no released-page marker, and none of its bytes change. Two shapes keep an empty leaf, having nowhere to go: a leaf that **is** the root — an index with no rows is diff --git a/src/LibRed/docs/format/page-05-usage-maps.md b/src/LibRed/docs/format/page-05-usage-maps.md index 7f5c91cc..1a1f46d7 100644 --- a/src/LibRed/docs/format/page-05-usage-maps.md +++ b/src/LibRed/docs/format/page-05-usage-maps.md @@ -16,7 +16,7 @@ page) pointing at a row on a data page. The first byte of that row is the map ty | `0x05` | … | Bitmap; bit `i` ⇒ page `startPage + i` is owned | **Reference map (type `0x01`, for very large tables):** the row is a list of 4-byte pointers -to dedicated **bitmap pages** (type `0x05`). Pointer `k` (zero ⇒ none) points at a bitmap page +to dedicated **bitmap pages** (page type `0x0105`). Pointer `k` (zero ⇒ none) points at a bitmap page covering the page range starting at `k × (pageSize − 4) × 8`; on a bitmap page the bitmap data begins at **offset 4**. @@ -27,7 +27,8 @@ begins at **offset 4**. The record is exactly **69 bytes** (`1 + 17 × 4`). Seventeen slots is not arbitrary: each bitmap page covers `(4096 − 4) × 8 = 32,736` pages ≈ 134 MB, so 17 slots span ≈ 2.28 GB — just past Jet's 2 GB -file ceiling. A bitmap page's header is `[0]=0x05`, `[1]=0x01`, `[2..3]=0`, bitmap from offset 4. +file ceiling. A bitmap page's header is the page type `0x0105` (bytes `05 01`) then `[2..3]=0`, bitmap from +offset 4. Bitmap pages are allocated **lazily**, only when a bit in their range is first set, and are *not* themselves marked as owned by the table. @@ -240,7 +241,7 @@ An inline record that already covers them stays as it is. Otherwise, in order of A map already in reference form gains a bitmap page, allocated the same way, for each range holding a released page that it has none for — a released table-definition page among the free pages is taken like any other, its -type byte becoming `0x05`. The merge then clears every bitmap page, each keeping its `05 01 00 00` header. +type becoming `0x0105`. The merge then clears every bitmap page, each keeping its `05 01 00 00` header. So at rest the released-pages map has no bits set, though it may have grown, moved its start page or converted to reference form. A non-empty one is *inferred* to be a release interrupted before close. @@ -259,7 +260,7 @@ usage map, etc.), and the only change to page 1 is one cleared bit per page take > allocations can therefore consume an ACE-authored run of future bits without creating a sparse file. > **Both map forms are handled.** For an inline (`0x00`) map it scans the record's > bitmap directly; for a **reference (`0x01`)** map — as a very large pre-existing ACE file carries — -> it scans each slot's dedicated bitmap page (type `0x05`), where a **set bit is a free page** (the +> it scans each slot's dedicated bitmap page (type `0x0105`), where a **set bit is a free page** (the > global map's sense), clears the bit on that bitmap page, and returns `slot × (pageSize−4)×8 + bit`. > `Free` is the inverse (sets the bit). > @@ -346,9 +347,9 @@ zeroed and is freed, its `05 01 00 00` header left in place. > live page, which is corruption rather than a leak. It is also the case that *only* clearing the bits is > not enough on a shared holder: the row has to go, or the dropped table's map records outlive it. -**The released definition page is marked.** Access sets the dropped table's TDEF page type to **`0x08`** +**The released definition page is marked.** Access sets the dropped table's TDEF page type to **`0x0108`** and changes nothing else on it; the other pages a drop frees (data, long-value, map holders, bitmap pages, and -a wide definition's continuation pages) keep their original type bytes. The marker, what survives on the page +a wide definition's continuation pages) keep their original types. The marker, what survives on the page and how close a LibRed drop lands to an ACE one are in [page-08](page-08-released-tdef.md). #### A page emptied of its contents is released on its own terms diff --git a/src/LibRed/docs/format/page-08-released-tdef.md b/src/LibRed/docs/format/page-08-released-tdef.md index c1e9e742..6118899a 100644 --- a/src/LibRed/docs/format/page-08-released-tdef.md +++ b/src/LibRed/docs/format/page-08-released-tdef.md @@ -1,26 +1,25 @@ -# Page type `0x08` — a released table-definition page +# Page type `0x0108` — a released table-definition page -A TDEF page that `DROP TABLE` has given back. Access marks it by **setting this one byte and changing -nothing else**: the dropped table's definition — header, column descriptors, names, index blocks — stays on -the page exactly as it was until a Compact reclaims it. +A TDEF page that `DROP TABLE` has given back. Access marks it by **changing the type's low byte and nothing +else**: the dropped table's definition — header, column descriptors, names, index blocks — stays on the page +exactly as it was until a Compact reclaims it. > Measured across an ACE `DROP TABLE`: **exactly one byte of the TDEF page's 4,096 changes**, offset `0x000`, -> `0x02` → `0x08`. +> `0x02` → `0x08` — the type word `0x0102` becomes `0x0108`. -That is what the `0x08` pages in real-world files are, and it is why they read as *structured* rather than -blank — a reader walking raw pages finds a whole table definition sitting behind a type byte that says the -page is no longer in use. It is also the reason a released TDEF cannot be mistaken for a live one by type -alone. +That is what the `0x0108` pages in real-world files are, and it is why they read as *structured* rather than +blank — a reader walking raw pages finds a whole table definition sitting behind a type that says the page is +no longer in use. It is also the reason a released TDEF cannot be mistaken for a live one by type alone. -The other pages a drop frees — data pages, long-value pages, usage-map holders — **keep their original type -bytes**; only the definition page is marked. A definition that runs onto continuation pages has only its first -page marked: the continuations are freed with every byte, their `0x02` type included, left as it was. (A data +The other pages a drop frees — data pages, long-value pages, usage-map holders — **keep their original +types**; only the definition page is marked. A definition that runs onto continuation pages has only its first +page marked: the continuations are freed with every byte, their `0x0102` type included, left as it was. (A data or long-value page released for a different reason — emptied of its rows or its packed values — does get its -own marker, [`0x09`](page-09-released-data.md), but not as part of a drop.) +own marker, [`0x0109`](page-09-released-data.md), but not as part of a drop.) ## Reading -**Nothing needs to handle it.** Allocation selects on the global free map, not on this byte, so a released +**Nothing needs to handle it.** Allocation selects on the global free map, not on the type, so a released TDEF is handed out and overwritten like any other free page. LibRed names it `PageType.ReleasedTableDefinition` so that a page walk can report it, and treats it no further. diff --git a/src/LibRed/docs/format/page-09-released-data.md b/src/LibRed/docs/format/page-09-released-data.md index ae999e27..98d15918 100644 --- a/src/LibRed/docs/format/page-09-released-data.md +++ b/src/LibRed/docs/format/page-09-released-data.md @@ -1,4 +1,4 @@ -# Page type `0x09` — a released data page +# Page type `0x0109` — a released data page A data page whose last live row has gone. Two events produce one: @@ -8,7 +8,7 @@ A data page whose last live row has gone. Two events produce one: (≤ 3,816 bytes — see [long-values](long-values.md)) are packed several to an LVAL page, and the page is released when no live record is left on it. -Both are stamped the same way, so the type byte alone does not say which produced a given page. The page's +Both are stamped the same way, so the type alone does not say which produced a given page. The page's **owner** at `0x04` does: a released LVAL page still carries the `LVAL` signature, a released ordinary data page still carries its table's TDEF page number. @@ -19,9 +19,8 @@ was. | Offset | Size | Value on a released page | | --- | --- | --- | -| `0x00` | 1 | `0x09` | -| `0x01` | 1 | `0x01` (page flags, unchanged) | -| `0x02` | 2 | Free space — `PageSize − 14 − 2N` for `N` slots, i.e. everything below the directory | +| `0x00` | 2 | Page type `0x0109` (bytes `09 01`) — only the low byte changes from a data page's `0x0101` | +| `0x02` | 2 | Free-space count — `PageSize − 14 − 2N` for `N` slots, i.e. everything below the directory | | `0x04` | 4 | Unchanged: `LVAL` (`0x4C41564C`) for a long-value page, the owning TDEF for a data page | | `0x08` | 4 | Zero — the chain stamp is only set on a chain's first page ([long-values](long-values.md)) | | `0x0C` | 2 | Row count `N` — how many rows the page had; they all remain, as tombstones | @@ -51,7 +50,7 @@ the survivors packing from the page end in slot order; the type changes only onc page is taken out of the **column's** owned and free maps and released. Exactly one page is released per page emptied. -**A chained value never produces one.** Those own their pages outright and are freed at `0x01`. +**A chained value never produces one.** Those own their pages outright and are freed at `0x0101`. Released pages exist at **every format version** — Jet 4, ACE 12, ACE 14, ACE 16 — so the mechanism predates ACE. Their presence tracks a file's *history* rather than its format. @@ -59,20 +58,21 @@ ACE. Their presence tracks a file's *history* rather than its format. ### What does not produce one An **index** page that leaves its tree. An index leaf a delete empties is unlinked and released like any -other page, but keeps its `0x04` type byte and is not stamped — see -[page-03-04 §10.4d](page-03-04-index-btree.md). A released `0x09` page is therefore always a data page, never +other page, but keeps its `0x0104` type and is not stamped — see +[page-03-04 §10.4d](page-03-04-index-btree.md). A released `0x0109` page is therefore always a data page, never an index one. **Reclaiming a hidden relocation target**, either. A relocated row's target ([page-01](page-01-data-and-rows.md)) is already flagged deleted, and taking its bytes back when its owner is deleted leaves the page an ordinary -data page: type `0x01`, its one slot a tombstone, free space back to `4080`, still in both of the table's +data page: type `0x0101`, its one slot a tombstone, free space back to `4080`, still in both of the table's maps. Only the deletion of a **live** row releases the page it empties. ## Reading **Nothing needs to handle it.** Reading is unaffected — no live row or descriptor points at a released page -— and allocation selects on the free map without consulting the type byte, so one can be handed out and -overwritten normally. LibRed names it `PageType.ReleasedDataPage` so a page walk can report it. +— and allocation selects on the free map without consulting the type, so one can be handed out and +overwritten normally. LibRed names it `PageType.ReleasedDataPage` so a page walk can report it. ACE's table +scan, meeting one in a table's owned map, reads it as rows like a live data page (README). ## Writing diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index c1c2d65f..9aa0d421 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -302,7 +302,7 @@ > LibRed also **enforces the LONG-only restriction at CREATE/ADD-COLUMN time** (`GenUniqueID()` on any other > type raises "Cannot place this validation expression on this field"), matching ACE. -- **LVAL (long-value) page** — a data page (type `0x01`) whose owner field (`0x04`) is the ASCII marker +- **LVAL (long-value) page** — a data page (type `0x0101`) whose owner field (`0x04`) is the ASCII marker `"LVAL"` instead of a TDEF page number. A single-page long value stores the whole payload in the referenced row (row 0 on a fresh page); the in-row reference descriptor is `[length-and-flags:4][row:1][page:3][4 reserved]`. The first word is little-endian, with a 30-bit byte @@ -663,6 +663,59 @@ > case-only change (both verified). The self-rename case is not hypothetical — EF models "move a table to > another schema" as a rename, and on a schema-less engine that degrades to `RENAME TO` the *same* name. +- **MSysNameMap** is Access's **Name AutoCorrect** map: for each object Access tracks, the names it and the + objects it depends on had when the map was last written. Five columns, no indexes: `GUID` (GUID), `Id` + (Long), `Name` (Text), `NameMap` (OLE), `Type` (Long). One row per object: + - **`Type`** is the object's `MSysObjects.Type` read as an unsigned 16-bit value — `1` table, `5` query, + `32768` form, `32772` report. + - **`Name`** is the object's name as it was when the row was written, stored **with a trailing NUL**. + - **`GUID`** is the object's own `GUID` property (the 16-byte Binary property in its `LvProp`) — in all but a + few rows, which Access has left stale. + - **`Id`** is **not** the `MSysObjects` Id: it is distinct within a file, not contiguous, and not decoded. + - **`NameMap`** is a long-value column with an owned-pages map and no free-pages map, so each map over 64 + bytes has an LVAL page to itself ([long-values.md](long-values.md) §3.3.2). + + **`NameMap` blob.** `[int32 version = 5][int32 fixedLen = 48]`, then records back to back, each + `[int32 length, excluding itself][48 fixed bytes][UTF-16 name, NUL-terminated]`; a map with no records is + the 8-byte header alone. Every blob in the corpus parses to its exact end under this rule. The fixed part: + + | Offset | Size | Object record | Field record | + |---|---|---|---| + | `+0` | 4 | `0` | `0` | + | `+4` | 16 | the object's GUID | the field's GUID | + | `+20` | 4 | a record kind, not decoded (`0`, `1` or `2`) | a record kind, not decoded (`7` on almost every field; `4`, `6`, `13` seen) | + | `+24` | 16 | an OLE Automation date (`double`, zero in a few), then 8 zero bytes | a GUID — in most records the preceding object record's; not decoded | + | `+40` | 4 | the object's `MSysObjects.Type` (`1` table, `5` query, `6` linked table, …) | the field's data type code (`4` Long, `10` Text, `12` Memo, …) | + | `+44` | 4 | **uninitialised** — whatever was in memory; a reader must not interpret it | the same | + + A **table's** blob begins with the table's own object record; where Access has refreshed it since the + table last changed, a field record per column follows, and nothing else. A **query's** blob never begins + with the query itself but with an object it reads; forms and reports mostly the same. + + **The engine never maintains any of it (verified).** Through ACE, `CREATE TABLE`, `DROP TABLE`, + `ADD COLUMN` and `DROP COLUMN`, and a table or field rename through DAO, leave `MSysNameMap` untouched — a + dropped table's row stays, and a renamed one keeps its old `Name` — and leave the table's `GUID` and + `NameMap` properties as they were; a table created through SQL or DAO gets neither property. The two + per-column pieces ACE does keep in step are the column's own property block: `DROP COLUMN` removes it, a + field rename renames its owner. Updating the map is the Access application's work, done when it + next runs Name AutoCorrect; a writer below Access leaves it alone, as LibRed does. + + > **The `NameMap` property holds the same records in a second layout.** Tables, linked tables, forms and + > reports carry a property named `NameMap` in their `LvProp` (OLE, `0x0B`, entry flag `0x00`). After the + > signature `0A CC 0E 55`, records follow back to back, each the first 40 bytes of an `MSysNameMap` fixed + > part — `[int32 0][GUID][int32 kind][16-byte slot]` — followed at once by the NUL-terminated UTF-16 name: + > no length prefix, no type field, no uninitialised field. The list ends with a record of **kind `12`** + > (`0x0C`) whose GUID is null, whose slot holds an int32 of `2` to `5` then zeros (`5` on every form, + > report and linked table), and whose name is empty; the blob ends with it. Every blob in the corpus + > parses to its exact end this way, bar one table in an MDB file that has no kind-12 record and ends after + > its last name with two zero bytes. Records with a **null GUID** occur inside the list too, so the end is + > the kind, never the GUID. + > + > Where an object has both, the property and its `MSysNameMap` blob usually list identical records; where + > they differ, it is in an object record's date or in a name — the two were written at different times. A + > table's property usually begins with the table itself (its `GUID` property), occasionally with another + > object. LibRed keeps the property byte for byte like any other; nothing in the engine reads it. + --- diff --git a/test/LibRed.Core.AccessTests/DeleteAccessTests.cs b/test/LibRed.Core.AccessTests/DeleteAccessTests.cs index 8abdb085..6bb86977 100644 --- a/test/LibRed.Core.AccessTests/DeleteAccessTests.cs +++ b/test/LibRed.Core.AccessTests/DeleteAccessTests.cs @@ -122,9 +122,9 @@ public void Deleting_every_row_keeps_only_the_tables_first_page() using var reopened = JetDatabase.Open(path); int[] left = [.. new UsageMap(reopened.Channel, reopened.OpenTable("Shape").Definition).DataPages()]; Assert.Equal([owned[0]], left); - Assert.Equal((byte)PageType.DataPage, reopened.Channel.ReadPage(owned[0]).ReadByte(0)); + Assert.Equal(PageType.DataPage, PageHeader.ReadType(reopened.Channel.ReadPage(owned[0]).Span)); foreach (int page in owned[1..]) - Assert.Equal((byte)PageType.ReleasedDataPage, reopened.Channel.ReadPage(page).ReadByte(0)); + Assert.Equal(PageType.ReleasedDataPage, PageHeader.ReadType(reopened.Channel.ReadPage(page).Span)); } private static void Exec(OleDbConnection connection, string sql) diff --git a/test/LibRed.Core.AccessTests/DropTableParityAccessTests.cs b/test/LibRed.Core.AccessTests/DropTableParityAccessTests.cs index ee7c7c28..fdd492b1 100644 --- a/test/LibRed.Core.AccessTests/DropTableParityAccessTests.cs +++ b/test/LibRed.Core.AccessTests/DropTableParityAccessTests.cs @@ -197,7 +197,7 @@ internal static string Difference(string acePath, string libredPath) continue; } if (BitConverter.ToInt32(ace, at + 4) == 2) continue; - if (ace[at] is (byte)LibRed.Pages.PageType.IntermediateIndexPage or (byte)LibRed.Pages.PageType.LeafIndexPage) continue; + if (LibRed.Pages.PageHeader.ReadType(ace.AsSpan(at)) is LibRed.Pages.PageType.IntermediateIndexPage or LibRed.Pages.PageType.LeafIndexPage) continue; for (int i = 0, shown = 0; i < PageSize && shown < 8; i++) if (ace[at + i] != libred[at + i]) diff --git a/test/LibRed.Core.AccessTests/PackedLongValueReleaseAccessTests.cs b/test/LibRed.Core.AccessTests/PackedLongValueReleaseAccessTests.cs index 830d65f8..e51276b4 100644 --- a/test/LibRed.Core.AccessTests/PackedLongValueReleaseAccessTests.cs +++ b/test/LibRed.Core.AccessTests/PackedLongValueReleaseAccessTests.cs @@ -88,8 +88,8 @@ private static string Census(string path) { channel.ReadPage(page, buffer); if (BitConverter.ToUInt32(buffer, channel.Format.DataOwnerOffset) != 0x4C41564C) continue; - if (buffer[0] == (byte)PageType.DataPage) live++; - else if (buffer[0] == (byte)PageType.ReleasedDataPage) released++; + if (PageHeader.ReadType(buffer) == PageType.DataPage) live++; + else if (PageHeader.ReadType(buffer) == PageType.ReleasedDataPage) released++; } return $"lval live={live} released={released}"; } @@ -113,7 +113,7 @@ private static string Difference(string acePath, string libredPath) continue; } if (BitConverter.ToInt32(ace, at + 4) == 2) continue; - if (ace[at] is (byte)PageType.IntermediateIndexPage or (byte)PageType.LeafIndexPage) continue; + if (PageHeader.ReadType(ace.AsSpan(at)) is PageType.IntermediateIndexPage or PageType.LeafIndexPage) continue; for (int i = 0, shown = 0; i < PageSize && shown < 8; i++) if (ace[at + i] != libred[at + i]) diff --git a/test/LibRed.Core.Tests/DropTableReclamationTests.cs b/test/LibRed.Core.Tests/DropTableReclamationTests.cs index 4e949b6a..0c894e11 100644 --- a/test/LibRed.Core.Tests/DropTableReclamationTests.cs +++ b/test/LibRed.Core.Tests/DropTableReclamationTests.cs @@ -53,7 +53,7 @@ public void Dropping_a_memo_table_frees_its_long_value_pages() // Access marks the released definition page and leaves the rest of it alone — measured, exactly // one byte of the 4,096 changes across an ACE drop. The pages it frees alongside keep their types. - Assert.Equal((byte)LibRed.Pages.PageType.ReleasedTableDefinition, releasedTdef[0]); + Assert.Equal(LibRed.Pages.PageType.ReleasedTableDefinition, LibRed.Pages.PageHeader.ReadType(releasedTdef)); // 60 values of 3,000 characters is ~360 KB of UTF-16, well over a hundred 4 KB pages. Before the // fix this was 2 — the single data page plus the TDEF. diff --git a/test/LibRed.Core.Tests/GlobalReferenceFreeMapTests.cs b/test/LibRed.Core.Tests/GlobalReferenceFreeMapTests.cs index 1696c165..5bf48e58 100644 --- a/test/LibRed.Core.Tests/GlobalReferenceFreeMapTests.cs +++ b/test/LibRed.Core.Tests/GlobalReferenceFreeMapTests.cs @@ -7,7 +7,7 @@ namespace LibRed.Core.Tests; // PageAllocator handles the reference-type (0x01) global free-pages map on page 1: it finds a free page by -// scanning the dedicated bitmap pages (type 0x05), clears its bit, and returns it — and Free() sets the bit +// scanning the dedicated bitmap pages (type 0x0105), clears its bit, and returns it — and Free() sets the bit // back. A SET bit is a FREE page (the global map's sense, opposite of a per-table owned map). Verified here // against a hand-crafted two-slot reference map: a real reference-type global map only appears in a large // (>~130 MB) pre-existing ACE file, impractical to grow in a unit test, and the bit↔page math is the same @@ -32,7 +32,7 @@ public void Allocate_and_free_through_a_reference_type_global_map() // slot 1 → bitmap page 3. (The 69-byte record is packed at the page end, as ACE packs rows.) Row 1 is the // released-pages map page 0 points at (0x1C), empty, as every real file carries it. int p1 = pageSize; - file[p1] = 0x01; // page type: data page + LibRed.Pages.PageHeader.WriteType(file.AsSpan(p1), LibRed.Pages.PageType.DataPage); BinaryPrimitives.WriteUInt16LittleEndian(file.AsSpan(p1 + format.DataRowCountOffset, 2), 2); int mapOffset = pageSize - 69; BinaryPrimitives.WriteUInt16LittleEndian(file.AsSpan(p1 + format.DataRowDirectoryOffset, 2), (ushort)mapOffset); @@ -64,12 +64,11 @@ public void Allocate_and_free_through_a_reference_type_global_map() } } - /// Writes a type-0x05 usage-bitmap page at with one free bit set (the + /// Writes a type-0x0105 usage-bitmap page at with one free bit set (the /// bitmap starts 4 bytes past the page header; a set bit marks a free page). private static void WriteBitmapPage(byte[] file, int offset, int? inRangeBit) { - file[offset] = 0x05; - file[offset + 1] = 0x01; + LibRed.Pages.PageHeader.WriteType(file.AsSpan(offset), LibRed.Pages.PageType.PageUsageBitmap); if (inRangeBit is int bit) file[offset + 4 + bit / 8] |= (byte)(1 << (bit % 8)); } diff --git a/test/LibRed.Core.Tests/MultiPageTableDefinitionTests.cs b/test/LibRed.Core.Tests/MultiPageTableDefinitionTests.cs index 92b9704e..af30c2ee 100644 --- a/test/LibRed.Core.Tests/MultiPageTableDefinitionTests.cs +++ b/test/LibRed.Core.Tests/MultiPageTableDefinitionTests.cs @@ -69,7 +69,7 @@ public void Rejects_an_invalid_continuation_chain_before_assembly(string corrupt { case "wrong-type": byte[] wrongType = channel.ReadPage(continuation).Span.ToArray(); - wrongType[0] = (byte)PageType.DataPage; + PageHeader.WriteType(wrongType, PageType.DataPage); channel.WritePage(continuation, wrongType); break; case "outside-file": diff --git a/test/LibRed.Core.Tests/NameMapTests.cs b/test/LibRed.Core.Tests/NameMapTests.cs new file mode 100644 index 00000000..bd49aa30 --- /dev/null +++ b/test/LibRed.Core.Tests/NameMapTests.cs @@ -0,0 +1,194 @@ +using LibRed; +using LibRed.Catalog; +using Xunit; + +namespace LibRed.Core.Tests; + +// Access's Name AutoCorrect map, in both of the layouts Access keeps it in: the NameMap column of an MSysNameMap +// row and the NameMap property in the object's LvProp. The engine never maintains either; these pin that a caller +// can read and edit them, and that a map read and written back is unchanged down to the bytes nobody understands. +public class NameMapTests +{ + // An Access-written table's MSysNameMap blob: Table1, then its columns ID (Long) and Field1 (Memo). The last + // four fixed bytes of each record are uninitialised memory — 0 on Table1's, A6 on the others. + private static readonly byte[] Row = Convert.FromHexString( + "05000000300000003E0000000000000085276F677377BE40AC361424EE65776D000000004BDB0DFF959AE6400000000000000000" + + "01000000000000005400610062006C006500310000003600000000000000E51D15FE90C25B49920D5E7939D2700807000000" + + "85276F677377BE40AC361424EE65776D04000000A60000004900440000003E000000000000009BFDAC9F26E6A3458870B1AF" + + "8834DED60700000085276F677377BE40AC361424EE65776D0C000000A60000004600690065006C00640031000000"); + + // The same table's NameMap property: the same three records in the property layout, closed by a kind-12 + // record carrying version 5. + private static readonly byte[] Property = Convert.FromHexString( + "0ACC0E550000000085276F677377BE40AC361424EE65776D000000004BDB0DFF959AE64000000000000000005400610062006C00" + + "65003100000000000000E51D15FE90C25B49920D5E7939D270080700000085276F677377BE40AC361424EE65776D4900440000" + + "00000000009BFDAC9F26E6A3458870B1AF8834DED60700000085276F677377BE40AC361424EE65776D4600690065006C006400" + + "3100000000000000000000000000000000000000000000000C000000050000000000000000000000000000000000"); + + private static readonly Guid Table1 = Guid.Parse("676f2785-7773-40be-ac36-1424ee65776d"); + + // An Access-written file with one MSysNameMap row and a NameMap property, both for its Table1. Its NameMap + // column, like every Access-written one, has an owned-pages map and no free-pages map. + private static readonly string Fixture = Path.Combine(AppContext.BaseDirectory, "Data", "Hungarian.accdb"); + + [Fact] + public void A_row_map_reads_as_its_records_and_writes_back_unchanged() + { + NameMap map = NameMap.ReadRow(Row); + + Assert.Equal(5, map.Version); + Assert.Equal(new[] { "Table1", "ID", "Field1" }, map.Records.Select(r => r.Name)); + Assert.Equal(new[] { 0, 7, 7 }, map.Records.Select(r => r.Kind)); + Assert.Equal(new[] { 1, 4, 12 }, map.Records.Select(r => r.TypeCode)); + Assert.Equal(new[] { 0, 0xA6, 0xA6 }, map.Records.Select(r => r.Unused)); + Assert.Equal(Table1, map.Records[0].ItemGuid); + Assert.NotNull(map.Records[0].SlotDate); + Assert.All(map.Records.Skip(1), r => Assert.Equal(Table1, r.SlotGuid)); + Assert.Equal(Row, map.WriteRow()); + } + + [Fact] + public void A_property_map_reads_as_the_same_records_and_writes_back_unchanged() + { + NameMap property = NameMap.ReadProperty(Property); + NameMap row = NameMap.ReadRow(Row); + + Assert.Equal(5, property.Version); + Assert.Equal( + row.Records.Select(r => (r.ItemGuid, r.Kind, Convert.ToHexString(r.Slot), r.Name)), + property.Records.Select(r => (r.ItemGuid, r.Kind, Convert.ToHexString(r.Slot), r.Name))); + Assert.All(property.Records, r => Assert.Equal(0, r.TypeCode)); + Assert.Equal(Property, property.WriteProperty()); + } + + // A null GUID is an ordinary record in the property layout; only the kind closes the list. + [Fact] + public void A_null_guid_record_does_not_end_a_property_map() + { + NameMap map = new() + { + Records = [new NameMapRecord(Guid.Empty, 0, "Unresolved"), new NameMapRecord(Table1, 0, "Table1")], + }; + + NameMap read = NameMap.ReadProperty(map.WriteProperty()); + + Assert.Equal(new[] { "Unresolved", "Table1" }, read.Records.Select(r => r.Name)); + Assert.Equal(NameMap.RowVersion, read.Version); + } + + [Fact] + public void An_edited_map_keeps_every_record_it_did_not_touch() + { + NameMap map = NameMap.ReadRow(Row); + NameMap renamed = map with + { + Records = [.. map.Records.Select(r => r.Name == "Field1" ? r with { Name = "Notes" } : r)], + }; + + NameMap read = NameMap.ReadRow(renamed.WriteRow()); + + Assert.Equal(new[] { "Table1", "ID", "Notes" }, read.Records.Select(r => r.Name)); + Assert.Equal(map.Records.Take(2), read.Records.Take(2), NameMapRecordComparer.Instance); + Assert.Equal(0xA6, read.Records[2].Unused); + NameMap property = NameMap.ReadProperty(renamed.WriteProperty()); + Assert.Equal("Notes", property.Records[2].Name); + } + + [Fact] + public void A_blob_in_neither_layout_is_refused() + { + Assert.Throws(() => NameMap.ReadRow(Property)); + Assert.Throws(() => NameMap.ReadProperty(Row)); + } + + [Fact] + public void A_database_s_name_map_row_can_be_read_and_updated() + { + string path = TemporaryDatabase.CopyPath(Fixture, "namemap-row-"); + try + { + NameMapRow row; + using (var db = JetDatabase.Open(path, readOnly: false)) + { + row = Assert.Single(db.ReadNameMaps()); + Assert.Equal("Table1", row.Name); + Assert.Equal(1, row.Type); + NameMap map = Assert.IsType(row.Map); + Assert.Equal("Table1", map.Records[0].Name); + + NameMap renamed = map with + { + Records = [map.Records[0] with { Name = "Renamed" }, .. map.Records.Skip(1)], + }; + Assert.True(db.UpdateNameMap(row.ObjectGuid, renamed, "Renamed")); + Assert.False(db.UpdateNameMap(Guid.NewGuid(), renamed)); + } + + using (var db = JetDatabase.Open(path, readOnly: true)) + { + NameMapRow after = Assert.Single(db.ReadNameMaps()); + Assert.Equal("Renamed", after.Name); + Assert.Equal(row.Id, after.Id); + Assert.Equal("Renamed", after.Map!.Records[0].Name); + Assert.Equal(row.Map!.Records.Skip(1), after.Map.Records.Skip(1), NameMapRecordComparer.Instance); + } + } + finally { TemporaryDatabase.Delete(path); } + } + + [Fact] + public void A_table_s_name_map_property_can_be_read_updated_and_removed_without_touching_the_others() + { + string path = TemporaryDatabase.CopyPath(Fixture, "namemap-prop-"); + try + { + NameMap original; + using (var db = JetDatabase.Open(path, readOnly: false)) + { + original = Assert.IsType(db.ReadNameMapProperty("Table1", 1)); + Assert.Equal("Table1", original.Records[0].Name); + NameMap renamed = original with + { + Records = [original.Records[0] with { Name = "Renamed" }, .. original.Records.Skip(1)], + }; + db.UpdateNameMapProperty("Table1", 1, renamed); + Assert.Throws(() => db.ReadNameMapProperty("NoSuchTable", 1)); + } + + using (var db = JetDatabase.Open(path, readOnly: false)) + { + NameMap after = Assert.IsType(db.ReadNameMapProperty("Table1", 1)); + Assert.Equal("Renamed", after.Records[0].Name); + Assert.Equal(original.Version, after.Version); + Assert.NotNull(TableProperty(db, "GUID")); + + db.UpdateNameMapProperty("Table1", 1, null); + Assert.Null(db.ReadNameMapProperty("Table1", 1)); + Assert.NotNull(TableProperty(db, "GUID")); + } + } + finally { TemporaryDatabase.Delete(path); } + } + + private static PropertyBlob.Property? TableProperty(JetDatabase db, string name) + { + var objects = db.OpenTable("MSysObjects"); + var d = objects.Definition; + object?[] row = objects.Rows().Single(r => (string)r[d.FindColumn("Name")!.Index]! == "Table1"); + return PropertyBlob.Read((byte[])row[d.FindColumn("LvProp")!.Index]!) + .Cast().FirstOrDefault(p => p!.Value.IsOwnedBy("") && p.Value.Name == name); + } + + // Records hold their slot as an array, so record equality compares it by reference. + private sealed class NameMapRecordComparer : IEqualityComparer + { + public static readonly NameMapRecordComparer Instance = new(); + + public bool Equals(NameMapRecord? x, NameMapRecord? y) => + x is not null && y is not null && x with { Slot = NoSlot } == y with { Slot = NoSlot } && x.Slot.AsSpan().SequenceEqual(y.Slot); + + private static readonly byte[] NoSlot = []; + + public int GetHashCode(NameMapRecord obj) => obj.ItemGuid.GetHashCode(); + } +} diff --git a/test/LibRed.Core.Tests/PageAndRowCorruptionTests.cs b/test/LibRed.Core.Tests/PageAndRowCorruptionTests.cs index 1bfe4924..ff3075a1 100644 --- a/test/LibRed.Core.Tests/PageAndRowCorruptionTests.cs +++ b/test/LibRed.Core.Tests/PageAndRowCorruptionTests.cs @@ -15,6 +15,7 @@ public class PageAndRowCorruptionTests [Theory] [InlineData("wrong-page-type")] + [InlineData("wrong-page-type-high-byte")] [InlineData("short-page")] [InlineData("directory-past-page")] [InlineData("row-overlaps-directory")] @@ -26,7 +27,12 @@ public void Malformed_data_page_slots_are_rejected_as_corruption(string corrupti switch (corruption) { case "wrong-page-type": - page[0] = (byte)PageType.TableDefinition; + PageHeader.WriteType(page, PageType.TableDefinition); + break; + case "wrong-page-type-high-byte": + // The type is the whole word: 01 02 is not a data page, though its low byte is. ACE skips such a + // page rather than reading it as rows. + page[1] = 0x02; break; case "short-page": page = page[..100]; @@ -145,8 +151,7 @@ public void First_variable_column_added_after_an_old_all_fixed_row_decodes_as_nu private static byte[] NewDataPage(int rowCount, int firstOffset, int secondOffset) { var page = new byte[Format.PageSize]; - page[0] = (byte)PageType.DataPage; - page[1] = 1; + PageHeader.WriteType(page, PageType.DataPage); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowCountOffset, 2), (ushort)rowCount); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowDirectoryOffset, 2), (ushort)firstOffset); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowDirectoryOffset + 2, 2), (ushort)secondOffset); diff --git a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs index 430f5a78..d8eb06fc 100644 --- a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs @@ -353,9 +353,9 @@ private static void Add(SortedDictionary> groups, string ki /// page its own table, where either file's catalog knows the name. private static string Owner(Dictionary owners, byte[] file, int at) { - if ((PageType)file[at] is PageType.TableDefinition or PageType.ReleasedTableDefinition) + if (PageHeader.ReadType(file.AsSpan(at)) is PageType.TableDefinition or PageType.ReleasedTableDefinition) return owners.TryGetValue(at / PageSize, out string? self) ? $"({self})" : ""; - if ((PageType)file[at] is not (PageType.DataPage or PageType.IntermediateIndexPage or PageType.LeafIndexPage)) + if (PageHeader.ReadType(file.AsSpan(at)) is not (PageType.DataPage or PageType.IntermediateIndexPage or PageType.LeafIndexPage)) return ""; if (file.AsSpan(at + 4, 4).SequenceEqual("LVAL"u8)) return "(long values)"; int owner = BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(at + 4, 4)); @@ -388,7 +388,7 @@ private static void Mask(string path, byte[] file, HashSet[] masks) for (int page = 1; page < file.Length / PageSize; page++) { int at = page * PageSize; - if ((PageType)file[at] != PageType.DataPage) continue; + if (PageHeader.ReadType(file.AsSpan(at)) != PageType.DataPage) continue; for (int i = 0x08; i < 0x0C; i++) masks[page].Add(i); if (BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(at + 4, 4)) != objects.DefinitionPage) continue; diff --git a/test/LibRed.Engine.Tests/IndexSplitTests.cs b/test/LibRed.Engine.Tests/IndexSplitTests.cs index 22f751ab..995b2915 100644 --- a/test/LibRed.Engine.Tests/IndexSplitTests.cs +++ b/test/LibRed.Engine.Tests/IndexSplitTests.cs @@ -43,7 +43,7 @@ public void Leaf_splitting_keeps_every_key_in_order_and_findable() var pk = new LibRed.Catalog.JetCatalog(ch).FindTable("Big")!.Indexes.Single(x => x.IsPrimaryKey); // The root grew into a node — the tree is genuinely multi-level, not a single fat leaf. - Assert.Equal(PageType.IntermediateIndexPage, (PageType)ch.ReadPage(pk.RootPage).ReadByte(0)); + Assert.Equal(PageType.IntermediateIndexPage, PageHeader.ReadType(ch.ReadPage(pk.RootPage).Span)); var cursor = new IndexCursor(ch, pk.RootPage); Assert.Equal(N, cursor.RowIds().Count()); From 37be2705109165714d4dd71400e090224e55d3b1 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 20:13:33 +0800 Subject: [PATCH 61/88] Record what ACE does with a relocation pointer to a missing page A row whose relocation pointer names a page the file does not hold still counts - COUNT(*) counts slots - but reading it fails "Unrecognized database format", and ACE marks the reading user's commit slot 01 00, after which every open fails until a repair. Unlike a page missing from a table's owned-pages map, which the scan skips silently. LibRed refuses the read too, and leaves the commit slot alone, having none of its own. Page 0's commit-byte section now lists the two measured occasions for 01 00, and three page types the move to 16-bit types missed are corrected. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/docs/format/long-values.md | 2 +- src/LibRed/docs/format/page-00-database.md | 5 +++++ src/LibRed/docs/format/page-01-data-and-rows.md | 15 ++++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/LibRed/docs/format/long-values.md b/src/LibRed/docs/format/long-values.md index 22ade93d..081dd089 100644 --- a/src/LibRed/docs/format/long-values.md +++ b/src/LibRed/docs/format/long-values.md @@ -120,7 +120,7 @@ LVAL pages are data pages (type `0x0101`) whose owner field (`0x04`) is the ASCI > **Reader and reclamation guardrails.** LibRed requires the complete 12-byte descriptor before reading > its fields, accepts only the three flags above, and bounds inline data against the bytes actually present. -> Every external pointer must name an in-file type-`0x01` page with the `LVAL` owner marker and a live, +> Every external pointer must name an in-file type-`0x0101` page with the `LVAL` owner marker and a live, > ordinary row slot. Chained rows must contain their 4-byte next pointer, make payload progress, never repeat > a `(page,row)`, terminate at zero exactly when the declared length is reached, and neither underfill nor > overrun that length. Before reclaiming a replaced chain, LibRed validates the complete chain and requires diff --git a/src/LibRed/docs/format/page-00-database.md b/src/LibRed/docs/format/page-00-database.md index 3a7e916d..bcce89c6 100644 --- a/src/LibRed/docs/format/page-00-database.md +++ b/src/LibRed/docs/format/page-00-database.md @@ -285,6 +285,11 @@ per-user *overall status*; the lock file holds the actual page-level read/write "accessed a corrupted page" — either one *without a matching user lock* makes Jet declare the database suspect and demand a repair before it will open. +ACE writes `01 00` to the user's slot on two measured occasions: a global usage-map pointer at `0x18`/`0x1C` +naming a page past the end of the file ([page-05 §9.1](page-05-usage-maps.md)), and a read of a row whose +relocation pointer names a page the file does not hold ([page-01](page-01-data-and-rows.md), *Relocated +rows*). Either way every later open fails with *"Unrecognized database format"*. + #### The slot is a little-endian commit counter (verified) Above those low reserved values, a slot is **one 16-bit little-endian counter of that user's committed diff --git a/src/LibRed/docs/format/page-01-data-and-rows.md b/src/LibRed/docs/format/page-01-data-and-rows.md index e8301ffa..d5dc669b 100644 --- a/src/LibRed/docs/format/page-01-data-and-rows.md +++ b/src/LibRed/docs/format/page-01-data-and-rows.md @@ -116,7 +116,7 @@ hundreds of bytes against the pointer's four. > does anything complain locally. A writer must therefore enforce 4060 rather than the page geometry; > LibRed does so in `RowInserter` from `JetFormatBase.MaxRecordSize`. -> **Reader guardrails.** LibRed requires an exact format-sized type-`0x01` page before either a full +> **Reader guardrails.** LibRed requires an exact format-sized type-`0x0101` page before either a full > scan or the O(1) index-seek slot path. The declared slot directory must fit before the heap; every > masked row offset must lie between the directory end and page end and must not increase relative to > the previous slot. Equal offsets remain valid because ALTER/relocation can deliberately leave a @@ -126,7 +126,7 @@ hundreds of bytes against the pointer's four. ### Relocated rows A live slot with `0x4000` set **begins with** a 4-byte little-endian forward pointer, -`(targetPage << 8) | targetRow`. The target is a nonempty inline row on a type-`0x01` page owned by +`(targetPage << 8) | targetRow`. The target is a nonempty inline row on a type-`0x0101` page owned by the same table. Its target slot has `0x8000` (deleted/hidden) set and `0x4000` clear: ordinary scans skip the hidden physical row, while the original row id and its index entries continue to resolve through the live source slot. A zero-length slot with both flags set is a tombstone, not a relocation @@ -162,10 +162,19 @@ the OLE-column transition the bytes themselves record; Access's own maintenance which is not reachable through SQL DML, is unexamined. Readers must therefore take the pointer from the leading 4 bytes and ignore any remainder rather than requiring a width. +**A pointer to a page the file does not hold is corruption, and ACE treats it as such (verified).** The +source slot still counts as a row — `COUNT(*)` includes it, counting slots without following them — but +any read of the row's data fails with *"Unrecognized database format"*, and ACE sets the reading user's +commit slot to `01 00` ("accessed a corrupted page", [page-00 §2.2](page-00-database.md)), after which +every open of the file fails until it is repaired. This is unlike a page missing from a table's owned-pages +map, which ACE's scan skips silently ([README](README.md)): the owned map only lists where rows may be, +while a relocation pointer asserts that one is there. + LibRed follows relocations through one shared resolver used by scans, index seeks, and raw-row mutation helpers. It validates that the source begins with a 4-byte pointer, plus the in-file page number, target row, page owner, and source/target flag shapes before exposing target bytes; -malformed pointers fail with `InvalidDataException`. +malformed pointers — a missing target page among them — fail with `InvalidDataException`. LibRed does +not set the commit slot: it keeps no user slot of its own (§2.2). ## 5. Row record format From 9280d3ec723e34d2013da174a3b3bb3ab0198944 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 20:48:20 +0800 Subject: [PATCH 62/88] Take an append-only memo's version history with it on DROP COLUMN A table keeps the history of all its append-only memos in one hidden complex column, VersionHistory_F5F8918F-0A3F-4DA9-AE71-184EE5012880, whose template and flat table carry a value column per memo. LibRed dropped the memo and left all of it behind. It now does what ACE does: while another append-only memo remains, only the dropped memo's value column goes, from the template and the flat table; when it was the last, the hidden column and its index, its MSysComplexColumns row, the flat table and the template go too - their MSysACEs rows left behind, as ACE leaves them - with the table-level AppendOnly property, and the table's complex-column flag unless another complex column remains. The table-level property leaves in the same rewrite as the memo's own, as ACE writes it once. The spec gains the version-history structure and both drop rules. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Storage/TableCreator.cs | 85 +++++++++++- src/LibRed/docs/format/system-catalog.md | 27 ++++ .../AppendOnlyDropAccessTests.cs | 126 ++++++++++++++++++ 3 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 test/LibRed.Core.AccessTests/AppendOnlyDropAccessTests.cs diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index de056101..8c4e0dbe 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -1044,7 +1044,10 @@ public bool DropConstraint(string childTable, string name) /// are, for the one caller that drops a table in order to put it back: . Taking /// them would destroy the values it is rebuilding around, and it re-points each row at the new /// definition page instead. - private bool DropTable(string tableName, bool keepComplexColumns) + /// Leaves the table's MSysACEs rows behind, as ACE does for the flat table + /// and template of a version history it drops with the last append-only memo (measured) — see + /// . + private bool DropTable(string tableName, bool keepComplexColumns, bool keepPermissions = false) { TableDef? table = _catalog.FindTable(tableName); if (table is null) return false; @@ -1149,7 +1152,7 @@ private bool DropTable(string tableName, bool keepComplexColumns) allocator.Release(page); // reusable only after this handle closes, as ACE holds them DeleteCatalogRows("MSysObjects", "Id", tdefPage); - DeleteCatalogRows("MSysACEs", "ObjectId", tdefPage); + if (!keepPermissions) DeleteCatalogRows("MSysACEs", "ObjectId", tdefPage); _catalog.Invalidate(); return true; } @@ -3177,16 +3180,85 @@ public bool DropColumn(string tableName, string columnName) foreach (int page in owned) allocator.Release(page); // reusable only after this handle closes, as ACE holds them - RemoveColumnProperties(table.DefinitionPage, columnName); // drop its DefaultValue/Required from LvProp (ACE does) + // An append-only memo's version history goes with it (DropVersionHistory). Found before the catalog moves on, + // and when this is the table's last append-only memo its table-level AppendOnly leaves in the same property + // rewrite as the memo's own block — one rewrite, as ACE makes it. + VersionHistory? history = VersionHistoryOf(tableName, columnName); + RemoveColumnProperties(table.DefinitionPage, columnName, history is { IsLast: true }); // ACE drops its properties _catalog.Invalidate(); + if (history is { } h) DropVersionHistory(tableName, columnName, h); return true; } + /// The element type of a table's version-history column: a per-table template named + /// MSysComplexTypeVH_<GUID>, holding one value column per append-only memo, named for the memo, and + /// a Modified_<GUID> timestamp. + private const string VersionHistoryTemplatePrefix = "MSysComplexTypeVH_"; + + private const string VersionHistoryTimestampPrefix = "Modified_"; + + /// The table-level property Access sets while a table has an append-only memo. + private const string AppendOnlyProperty = "AppendOnly"; + + /// A table's version-history column as it stands for one of its append-only memos: the complex column, + /// its template's name, and whether that memo is the last whose history it keeps. + private sealed record VersionHistory(string ColumnName, int ComplexId, string FlatTable, string Template, bool IsLast); + + /// The version history keeping 's history, or null when the column is not an + /// append-only memo. A table has at most one version-history column; its template carries a value column named + /// for each append-only memo. + private VersionHistory? VersionHistoryOf(string tableName, string memoName) + { + if (_catalog.ComplexColumns.FirstOrDefault(c => + string.Equals(c.OwnerTable.Name, tableName, StringComparison.OrdinalIgnoreCase) + && c.ElementTypeName?.StartsWith(VersionHistoryTemplatePrefix, StringComparison.Ordinal) == true) is not { } history) + return null; + var values = _catalog.RequireTable(history.ElementTypeName!).Columns + .Where(c => !c.Name.StartsWith(VersionHistoryTimestampPrefix, StringComparison.Ordinal)).ToList(); + if (!values.Any(c => string.Equals(c.Name, memoName, StringComparison.OrdinalIgnoreCase))) return null; + return new VersionHistory(history.ColumnName, history.ComplexId, history.FlatTable.Name, history.ElementTypeName!, + IsLast: values.Count == 1); + } + + /// + /// Takes a dropped append-only memo's version history with it, as ACE does (measured). A table keeps the history + /// of all its append-only memos in one hidden complex column, whose template and flat table carry a value column + /// per memo. While another append-only memo remains, only the dropped memo's value column goes, from both. When it + /// was the last, the history goes entirely: the hidden column and its index, its MSysComplexColumns row, the + /// flat table and the template — each released as a dropped table's is, but with its MSysACEs rows left + /// behind, as ACE leaves them — and, when no other complex column is left on the table, its complex-column flag. + /// (The table-level AppendOnly property has already gone, with the memo's own properties.) Unlike dropping + /// an attachment or multi-value column directly, which leaves its registration and flat table behind. + /// + private void DropVersionHistory(string tableName, string memoName, VersionHistory history) + { + if (!history.IsLast) + { + DropColumn(history.FlatTable, memoName); + DropColumn(history.Template, memoName); + return; + } + + int tdefPage = _catalog.RequireTable(tableName).DefinitionPage; + DropColumn(tableName, history.ColumnName); // the complex-column branch takes its index with it + DropTable(history.FlatTable, keepComplexColumns: false, keepPermissions: true); + DeleteCatalogRows("MSysComplexColumns", "ComplexID", history.ComplexId); + DropTable(history.Template, keepComplexColumns: false, keepPermissions: true); + + if (!_catalog.ComplexColumns.Any(c => string.Equals(c.OwnerTable.Name, tableName, StringComparison.OrdinalIgnoreCase)) + && ReadObjectFlags(tdefPage) is int flags && (flags & CatalogFormat.ObjectFlagOwnsComplexColumns) != 0) + UpdateCatalogRows("MSysObjects", "Id", tdefPage, required: true, + ("Flags", flags & ~CatalogFormat.ObjectFlagOwnsComplexColumns)); + _catalog.Invalidate(); + } + /// Removes a dropped column's extended-property block (DefaultValue, Required, …) from its /// table's MSysObjects.LvProp blob — what ACE does on DROP COLUMN (verified). Surgically removes /// just that column's block (keeps the name pool + other columns' blocks), re-stores the smaller blob on - /// an LvProp page and updates the row. No-op when the column had no properties. - private void RemoveColumnProperties(int tdefPage, string columnName) + /// an LvProp page and updates the row. No-op when the column had no properties. With + /// the table-level AppendOnly property leaves in the same rewrite — + /// for the table's last append-only memo. + private void RemoveColumnProperties(int tdefPage, string columnName, bool alsoTableAppendOnly = false) { (TableDef msys, Table table, int idIdx, ColumnDef lvProp) = ObjectProperties(); @@ -3195,7 +3267,8 @@ private void RemoveColumnProperties(int tdefPage, string columnName) if (values[lvProp.Index] is not byte[] { Length: > 0 } blob) return; byte[] cleaned = PropertyBlob.RemoveOwner(blob, columnName); - if (cleaned.Length == blob.Length) return; // the column had no property block — nothing to remove + if (alsoTableAppendOnly) cleaned = ReplaceTableProperty(cleaned, AppendOnlyProperty, null); + if (cleaned.Length == blob.Length) return; // nothing to remove byte[] descriptor = new RowInserter(_channel, msys).StorePackedLongValue(lvProp.ColumnId, cleaned); values[lvProp.Index] = new LongValueDescriptor(descriptor); diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index 9aa0d421..ec479caf 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -812,6 +812,33 @@ set on every table with a complex column and on no table without one, system tab the template tables, which hold the values rather than the column (verified across Access-written ACE files). A flat table can carry its own `LvProp`. +### Version history — the append-only memo + +A memo with the `AppendOnly` property (column-owned Boolean, DDL flag `0x01`; DAO's `Field2.AppendOnly`) +keeps its earlier values in a complex column Access adds for it (verified — DAO setting the property creates +all of the below). While a table has one, the table itself carries `AppendOnly` = 1 too, in its own block. + +- **One history per table, whatever its number of append-only memos.** The table gains a hidden `Complex` + column named **`VersionHistory_F5F8918F-0A3F-4DA9-AE71-184EE5012880`** — the same name in every table — + with its own complex index (flags `0x0289`), registered in `MSysComplexColumns` like any other. +- Its element type is not one of the nine shared templates but a **template of the table's own**, + **`MSysComplexTypeVH_`** (`Flags` `0x80030000`), holding **one value column per append-only memo, + named for the memo** (and of its type), plus `Modified_F9B5E312-4155-4c59-9AAE-391C1B295827` (DateTime). +- Its flat table, `f__VersionHistory_F5F8918F-0A3F-` (the name is cut short), has the two bookkeeping + columns `_VersionHistory_…` and `
_VersionHistory_…` and the template's columns. + +**`DROP COLUMN` of an append-only memo takes its history with it (verified).** + +- While **another** append-only memo remains, only the dropped memo's value column goes — from the template + and from the flat table. The history column, its registration and the table's `AppendOnly` stay. +- When it was the **last**, the history goes entirely: the hidden column and its index, its + `MSysComplexColumns` row, and the flat table and the template, each released as a dropped table is + (`0x0108`) — **but their `MSysACEs` rows are left behind**. The table's `AppendOnly` goes, and so does its + `0x00040000` flag unless another complex column is left on it. + +That is unlike dropping an attachment or multi-value column itself, which leaves its `MSysComplexColumns` row +and flat table behind. LibRed's `DROP COLUMN` does the same as ACE on both counts. + ### Attachment payload — `FileData` An attachment's `FileData` is an OLE long value wrapping the file, with an 8-byte outer header, optional diff --git a/test/LibRed.Core.AccessTests/AppendOnlyDropAccessTests.cs b/test/LibRed.Core.AccessTests/AppendOnlyDropAccessTests.cs new file mode 100644 index 00000000..aeafbdcd --- /dev/null +++ b/test/LibRed.Core.AccessTests/AppendOnlyDropAccessTests.cs @@ -0,0 +1,126 @@ +using System.Data.OleDb; +using System.Reflection; +using LibRed; +using LibRed.Catalog; +using LibRed.Formats; +using Xunit; + +namespace LibRed.Core.Tests; + +// DROP COLUMN of an append-only memo takes the memo's version history with it (system-catalog, "Version history"). +// A table keeps the history of all its append-only memos in one hidden complex column, whose per-table template and +// flat table carry a value column per memo: dropping one memo of several takes only its value columns; dropping the +// last takes the whole history, clears the table's AppendOnly, and clears its complex-column flag unless another +// complex column remains — while leaving the dropped tables' MSysACEs rows, as ACE leaves them. Only DAO can make an +// append-only memo, so the file is built through it, then ACE and LibRed drop the same columns on their own copies. +[Collection(AceCollection.Name)] +public class AppendOnlyDropAccessTests +{ + [Fact] + public void Dropping_append_only_memos_leaves_the_catalog_ACE_leaves() + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not available in this process."); + string acePath = TemporaryDatabase.CreatePath("append-only-ace-"); + string libredPath = TemporaryDatabase.CreatePath("append-only-libred-"); + try + { + Build(engine!, acePath); + File.Copy(acePath, libredPath); + + foreach ((string table, string column) in new[] { ("T", "M1"), ("T", "M2"), ("A", "M") }) + { + using (OleDbConnection connection = AceTestDatabase.Open(acePath)) + using (OleDbCommand command = connection.CreateCommand()) + { + command.CommandText = $"ALTER TABLE {table} DROP COLUMN {column}"; + command.ExecuteNonQuery(); + } + OleDbConnection.ReleaseObjectPool(); + using (var db = JetDatabase.Open(libredPath, readOnly: false)) + Assert.True(db.DropColumn(table, column)); + + Assert.Equal(Catalog(acePath), Catalog(libredPath)); + } + + using var result = JetDatabase.Open(libredPath, readOnly: true); + TableDef t = result.Catalog.FindTable("T")!; + TableDef a = result.Catalog.FindTable("A")!; + Assert.Equal(["ID"], t.Columns.Select(c => c.Name)); + Assert.Equal(0u, t.ObjectFlags & (uint)CatalogFormat.ObjectFlagOwnsComplexColumns); + Assert.Equal(["ID", "Files"], a.Columns.Select(c => c.Name)); + Assert.NotEqual(0u, a.ObjectFlags & (uint)CatalogFormat.ObjectFlagOwnsComplexColumns); // the attachment remains + Assert.DoesNotContain(result.Catalog.Tables, x => x.Name.StartsWith("MSysComplexTypeVH_", StringComparison.Ordinal)); + } + finally + { + TemporaryDatabase.Delete(acePath); + TemporaryDatabase.Delete(libredPath); + } + } + + // T: two append-only memos, sharing one history. A: one append-only memo beside an attachment. + private static void Build(object engine, string path) + { + object db = Invoke(engine, "CreateDatabase", path, ";LANGID=0x0409;CP=1252;COUNTRY=0")!; + try + { + foreach ((string table, (string Name, int Type)[] fields) in new[] + { + ("T", new[] { ("ID", 4), ("M1", 12), ("M2", 12) }), + ("A", new[] { ("ID", 4), ("M", 12), ("Files", 101) }), // 12 dbMemo, 101 dbAttachment + }) + { + object td = Invoke(db, "CreateTableDef", table)!; + foreach ((string name, int type) in fields) + Invoke(Get(td, "Fields"), "Append", Invoke(td, "CreateField", name, type)); + Invoke(Get(db, "TableDefs"), "Append", td); + } + foreach ((string table, string field) in new[] { ("T", "M1"), ("T", "M2"), ("A", "M") }) + { + object f = Item(Get(Item(Get(db, "TableDefs"), table), "Fields"), field); + f.GetType().InvokeMember("AppendOnly", BindingFlags.SetProperty, null, f, [true]); + } + } + finally { Invoke(db, "Close"); } + AceTestDatabase.ReleaseAbandonedComObjects(); + } + + // What the drop is expected to change: each table's columns, indexes, complex-column flag and AppendOnly + // properties; every complex column with its template's and flat table's columns; the hidden tables that exist; + // and how many MSysACEs rows name each object. + private static string Catalog(string path) + { + var lines = new List(); + using var db = JetDatabase.Open(path, readOnly: true); + var objects = db.OpenTable("MSysObjects"); + var od = objects.Definition; + var rows = objects.Rows().ToList(); + foreach (string table in (string[])["T", "A"]) + { + TableDef t = db.Catalog.FindTable(table)!; + object?[] row = rows.Single(r => (string)r[od.FindColumn("Name")!.Index]! == table); + var props = row[od.FindColumn("LvProp")!.Index] is byte[] { Length: > 0 } lv ? PropertyBlob.Read(lv) : []; + lines.Add($"{table}: [{string.Join(",", t.Columns.Select(c => c.Name))}] indexes [{string.Join(",", t.Indexes.Select(i => i.Name))}] " + + $"flags 0x{t.ObjectFlags:X8} AppendOnly [{string.Join(",", props.Where(p => p.Name == "AppendOnly").Select(p => p.Owner))}]"); + } + foreach (ComplexColumn c in db.Catalog.ComplexColumns) + lines.Add($"complex {c.OwnerTable.Name}.{c.ColumnName} [{string.Join(",", db.Catalog.FindTable(c.ElementTypeName!)!.Columns.Select(x => x.Name))}] " + + $"flat [{string.Join(",", c.FlatTable.Columns.Select(x => x.Name))}]"); + lines.AddRange(db.Catalog.Tables.Select(t => t.Name) + .Where(n => n.StartsWith("f_", StringComparison.Ordinal) || n.StartsWith("MSysComplexTypeVH_", StringComparison.Ordinal)).Order()); + var aces = db.OpenTable("MSysACEs"); + int objectId = aces.Definition.FindColumn("ObjectId")!.Index; + lines.Add("MSysACEs rows per object: " + string.Join(",", aces.Rows().GroupBy(r => (int)r[objectId]!).OrderBy(g => g.Key).Select(g => $"{g.Key}:{g.Count()}"))); + return string.Join("\n", lines); + } + + private static object Get(object target, string member) => + target.GetType().InvokeMember(member, BindingFlags.GetProperty, null, target, null)!; + + private static object Item(object collection, string name) => + collection.GetType().InvokeMember("Item", BindingFlags.GetProperty, null, collection, [name])!; + + private static object? Invoke(object target, string member, params object?[] args) => + target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); +} From 4782a4e88f40fe70973dea9e9ddd91873f4d7240 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 21:27:55 +0800 Subject: [PATCH 63/88] Retype a Memo/OLE column in place, as ACE does ALTER COLUMN to or from Memo/OLE fell back to a logical rebuild: the table dropped and recreated, every row re-inserted, every index rebuilt - the primary key moved to the front - where ACE edits the column in place and rebuilds only the indexes over it. It now takes the same in-place edit and re-lay as every other retype, plus the long-value side of ADD and DROP COLUMN: a column becoming a long value gets its map entry and records as ADD COLUMN places them, and each value is stored as an insert stores it; a column ceasing to be one has its maps retired and its pages released as DROP COLUMN does. The rebuild and the helpers only it used are gone. The re-lay shared by every retype also differed from ACE on rows whose old value was NULL: the dead id's null bit is now carried over from the old row rather than set, and a NULL fixed target's new slot keeps the old record's bytes rather than zeros. Text to Memo, Memo to Text, Memo to Memo and a fixed retype over a NULL now leave the file ACE leaves, bar MSysObjects' DateUpdate and, for the Memo re-declaration, stale bytes below the live records on the usage-map page. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Storage/RowEncoder.cs | 24 +- src/LibRed/LibRed.Core/Storage/RowInserter.cs | 68 +-- .../LibRed.Core/Storage/TableCreator.cs | 450 +++++------------- src/LibRed/docs/format/appendix-structures.md | 2 +- .../docs/format/page-01-data-and-rows.md | 7 +- src/LibRed/docs/format/page-02b-columns.md | 33 +- .../ColumnRetypeParityAccessTests.cs | 110 +++++ test/LibRed.Engine.Tests/AlterColumnTests.cs | 24 + 8 files changed, 325 insertions(+), 393 deletions(-) create mode 100644 test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs diff --git a/src/LibRed/LibRed.Core/Storage/RowEncoder.cs b/src/LibRed/LibRed.Core/Storage/RowEncoder.cs index 21f9a1d2..004efa5f 100644 --- a/src/LibRed/LibRed.Core/Storage/RowEncoder.cs +++ b/src/LibRed/LibRed.Core/Storage/RowEncoder.cs @@ -120,7 +120,7 @@ public byte[] Encode(object?[] values, IReadOnlyDictionary? preserv varChunks[column.VariableIndex] = v is null ? [] : JetTypeCodec.Encode(column, v); } - return AssembleRow(maxColumnId, fixedRegion, varChunks, _columns, values, deadIdsPresent: false); + return AssembleRow(maxColumnId, fixedRegion, varChunks, _columns, values); } /// The slot for a calculated column: the envelope holding the result, wrapped in a long-value @@ -197,9 +197,10 @@ private static void EnsureFitsDeclaredLength(ColumnDef column, byte[] encoded) /// Assembles the on-disk row bytes from a prepared fixed region and the ordered variable chunks: /// [count][fixed][var data][var-offset table][numVar] (the variable section is omitted entirely when /// there are none) then [null bitmap]. The count and bitmap width are maxColumnId + 1; a - /// column's bit is set when present (Boolean = its truthy value), and dead ids (gaps below the max, from a - /// burned/dropped id) are set present too — all verified vs ACE (§5). Shared by Encode and - /// the ALTER COLUMN row re-lay so the two can never drift. + /// column's bit is set when present (Boolean = its truthy value), and a dead id's (a gap below the max, from a + /// burned/dropped id) is taken from — the row's bitmap before an ALTER COLUMN + /// re-lay — or left clear for a new row — all verified vs ACE (§5). Shared by Encode and the ALTER + /// COLUMN row re-lay so the two can never drift. /// /// The declared-width check runs HERE rather than in Encode. It used to sit above this call, /// which meant the ALTER COLUMN re-lay — the other caller — never got it, and a narrowing retype could @@ -207,7 +208,7 @@ private static void EnsureFitsDeclaredLength(ColumnDef column, byte[] encoded) /// internal static byte[] AssembleRow(int maxColumnId, ReadOnlySpan fixedRegion, IReadOnlyList varChunks, IReadOnlyList columns, object?[] values, - bool deadIdsPresent = true) + ReadOnlySpan priorBitmap = default) { // A calculated column is exempt from the declared-width check: its length field is a constant ACE // writes (39 for a value type, 509 for any Text, whatever size was asked for), not a limit — a @@ -268,13 +269,12 @@ internal static byte[] AssembleRow(int maxColumnId, ReadOnlySpan fixedRegi if (present) row[bitmapPos + (column.ColumnId >> 3)] |= (byte)(1 << (column.ColumnId & 7)); } // A dead id's bit depends on which route wrote the row, and both are measured against ACE: the ALTER - // COLUMN re-lay carries the old row's bit forward (a retype's burned id reads present, §5), while a - // row INSERTED afterwards leaves it clear — the same statement pair gives ACE 0x0F for the re-laid row - // and 0x0D for the next insert. - if (deadIdsPresent) - for (int id = 0; id <= maxColumnId; id++) - if (!liveIds.Contains(id)) - row[bitmapPos + (id >> 3)] |= (byte)(1 << (id & 7)); + // COLUMN re-lay carries the old row's bit forward — set where the retyped column held a value, clear + // where it was NULL — while a row INSERTED afterwards leaves it clear: the same statement pair gives + // ACE 0x0F for a re-laid row with a value and 0x0D for the next insert. + for (int id = 0; id <= maxColumnId && id < priorBitmap.Length * 8; id++) + if (!liveIds.Contains(id) && (priorBitmap[id >> 3] & (1 << (id & 7))) != 0) + row[bitmapPos + (id >> 3)] |= (byte)(1 << (id & 7)); return row; } diff --git a/src/LibRed/LibRed.Core/Storage/RowInserter.cs b/src/LibRed/LibRed.Core/Storage/RowInserter.cs index 12378e43..ccfb1d1c 100644 --- a/src/LibRed/LibRed.Core/Storage/RowInserter.cs +++ b/src/LibRed/LibRed.Core/Storage/RowInserter.cs @@ -1056,34 +1056,44 @@ private static int LowestRowOffset(byte[] page, JetFormatBase format, int rowCou /// private void MaterializeLongValues(object?[] values) { - const int maxInline = LongValueFormat.MaxInlineValue; LongValueWriter? writer = null; - foreach (ColumnDef column in _table.Columns) + if (column.Type is JetDataType.Memo or JetDataType.Ole) + values[column.Index] = MaterializeLongValue(column, values[column.Index], ref writer); + } + + /// The storage form of one Memo/OLE value: a value over 64 bytes is written to an LVAL page and + /// becomes its ; anything else — a short value, null, a descriptor already + /// built — comes back as it was, for the row codec to inline. For a caller re-laying a single column. + internal object? MaterializeLongValue(ColumnDef column, object? value) + { + LongValueWriter? writer = null; + return MaterializeLongValue(column, value, ref writer); + } + + private object? MaterializeLongValue(ColumnDef column, object? value, ref LongValueWriter? writer) + { + byte[]? payload = value switch { - if (column.Type is not (JetDataType.Memo or JetDataType.Ole)) continue; - byte[]? payload = values[column.Index] switch - { - string s => Encoding.Unicode.GetBytes(s), // memo: UTF-16LE - byte[] b => b, // OLE: raw bytes - _ => null, // null, or an already-built LongValueDescriptor - }; - if (payload is null || payload.Length <= maxInline) continue; - - // Compress before storing, but AFTER the inline test above and using the uncompressed length for - // the single-page test below: ACE decides the storage form on the uncompressed size and applies - // compression to whatever form results, never to a chained value (LongTextStorageAccessTests). - if (payload.Length <= LongValueFormat.MaxSinglePageValue - && values[column.Index] is string text - && Types.JetTypeCodec.TryCompressText(column, text) is { } compressed) - payload = compressed; - - writer ??= new LongValueWriter(_channel); - TableDefinitionPage definition = LongValueMaps; - definition.LongValueOwnedMaps.TryGetValue(column.ColumnId, out (int Row, int Page) owned); - definition.LongValueFreeMaps.TryGetValue(column.ColumnId, out (int Row, int Page) free); - values[column.Index] = new LongValueDescriptor(StoreLongValue(writer, payload, owned, free)); - } + string s => Encoding.Unicode.GetBytes(s), // memo: UTF-16LE + byte[] b => b, // OLE: raw bytes + _ => null, // null, or an already-built LongValueDescriptor + }; + if (payload is null || payload.Length <= LongValueFormat.MaxInlineValue) return value; + + // Compress before storing, but AFTER the inline test above and using the uncompressed length for + // the single-page test below: ACE decides the storage form on the uncompressed size and applies + // compression to whatever form results, never to a chained value (LongTextStorageAccessTests). + if (payload.Length <= LongValueFormat.MaxSinglePageValue + && value is string text + && Types.JetTypeCodec.TryCompressText(column, text) is { } compressed) + payload = compressed; + + writer ??= new LongValueWriter(_channel); + TableDefinitionPage definition = LongValueMaps; + definition.LongValueOwnedMaps.TryGetValue(column.ColumnId, out (int Row, int Page) owned); + definition.LongValueFreeMaps.TryGetValue(column.ColumnId, out (int Row, int Page) free); + return new LongValueDescriptor(StoreLongValue(writer, payload, owned, free)); } // A page is dropped from the free-pages map once it cannot hold a 256-byte value and its 2-byte row-directory @@ -1398,10 +1408,10 @@ private void UpdateTdefCounters(JetFormatBase format, object?[] values, bool[]? if (advances) BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(format.TdefLastAutoNumberOffset, 4), assigned); // Keep the cached catalog seed in sync with the on-disk 0x14. The insert path itself reads the - // high-water from disk (AssignAutoNumbers), so this isn't needed for assigning ids — but RewriteColumn - // (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. + // high-water from disk (AssignAutoNumbers), so this isn't needed for assigning ids — but anything that + // reconstructs the counter from the cached ColumnDef.Seed would, left stale, reset it to its + // create-time value after the high rows were deleted (seen once: next id dropped to 1 instead of + // continuing past 6). Seed = next id. column.Seed = unchecked(newHighWater + column.Increment); } diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 8c4e0dbe..3b2b721e 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -823,21 +823,6 @@ private void BackfillIndex(string tableName, string indexName, bool ignoreNulls, private void SetBuiltStatistics(TableDef table, IndexDef index, List<(byte[] Key, int Pointer, bool NullKey)> entries) => WriteIndexStatistics(table, index, entries.Count, entries.Select(e => Convert.ToHexString(e.Key)).Distinct().Count()); - /// Every logical index's statistics — its real index's total and unique entry counts — by index name. - /// The blocks sit on the definition's first page. - private Dictionary IndexStatistics(TableDef table) - { - byte[] tdef = _channel.ReadPage(table.DefinitionPage).Span.ToArray(); - var statistics = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (IndexDef index in table.Indexes) - { - int at = _channel.Format.TdefRealIndexBlockOffset + index.RealIndexOrdinal * _channel.Format.RealIndexEntrySize; - statistics[index.Name] = (BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(at, 4)), - BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(at + 4, 4))); - } - return statistics; - } - private void WriteIndexStatistics(TableDef table, IndexDef index, int total, int unique) { byte[] tdef = _channel.ReadPage(table.DefinitionPage).Span.ToArray(); @@ -1036,18 +1021,14 @@ public bool DropConstraint(string childTable, string name) /// child cannot be dropped — drop the referencing table (or the relationship) first. EF drops FKs before /// tables, but database-first scaffolding cleanup drops child tables directly, which must work. /// - public bool DropTable(string tableName) => DropTable(tableName, keepComplexColumns: false); + public bool DropTable(string tableName) => DropTable(tableName, keepPermissions: false); /// /// The table to drop. - /// Leaves the complex columns' catalog rows and flat tables where they - /// are, for the one caller that drops a table in order to put it back: . Taking - /// them would destroy the values it is rebuilding around, and it re-points each row at the new - /// definition page instead. /// Leaves the table's MSysACEs rows behind, as ACE does for the flat table /// and template of a version history it drops with the last append-only memo (measured) — see /// . - private bool DropTable(string tableName, bool keepComplexColumns, bool keepPermissions = false) + private bool DropTable(string tableName, bool keepPermissions) { TableDef? table = _catalog.FindTable(tableName); if (table is null) return false; @@ -1070,12 +1051,10 @@ private bool DropTable(string tableName, bool keepComplexColumns, bool keepPermi // alone leaves the flat tables orphaned and catalog rows naming a table that is gone. ACE takes both // with it (measured: dropping complex1.accdb's Table1 through ACE leaves neither, where LibRed left // four of each). Each flat table goes the ordinary way, which is why this runs before the drop proper. - var complexColumns = keepComplexColumns - ? [] - : _catalog.ComplexColumns - .Where(c => string.Equals(c.OwnerTable.Name, tableName, StringComparison.OrdinalIgnoreCase)) - .Select(c => (c.FlatTable.Name, c.ComplexId)) - .ToList(); + var complexColumns = _catalog.ComplexColumns + .Where(c => string.Equals(c.OwnerTable.Name, tableName, StringComparison.OrdinalIgnoreCase)) + .Select(c => (c.FlatTable.Name, c.ComplexId)) + .ToList(); foreach ((string flatTable, int complexId) in complexColumns) { DropTable(flatTable); @@ -1724,13 +1703,6 @@ private void DeleteCatalogRows(string catalogTable, string keyColumn, int keyVal } } - /// Points a complex column's MSysComplexColumns row at — - /// the owner is named by definition page, so a rebuild that moves the table has to follow it there or the - /// column stops resolving and its values become unreachable. - private void SetComplexColumnOwner(int complexId, int definitionPage) => - UpdateCatalogRows("MSysComplexColumns", "ComplexID", complexId, required: true, - ("ConceptualTableID", definitionPage)); - /// Follows a renamed complex column in its MSysComplexColumns row. The catalog matches the /// row to a column by name, so a stale one stops resolving altogether — the column keeps its values on /// disk and nothing can reach them. @@ -1939,47 +1911,11 @@ public bool AddColumn(string tableName, ColumnSpec spec, string? defaultValue = if (!spec.IsFixedLength) BinaryPrimitives.WriteUInt16LittleEndian(parts.Header.AsSpan(format.TdefVariableColumnsOffset, 2), (ushort)(varCount + 1)); - // A memo/OLE column needs a §3.3.2 usage-map entry (its owned + free page maps). ACE appends the two - // maps to the table's existing usage-map page right after the data/index maps (verified), and adds the - // 10-byte entry before the list's 0xFFFF terminator. - int lvMapPage = 0, lvUsedRow = 0, lvFreeRow = 0; - bool lvDedicated = false; - if (isLongValue) - { - int o = format.TdefOwnedPagesOffset + 1; - int primaryPage = parts.Header[o] | (parts.Header[o + 1] << 8) | (parts.Header[o + 2] << 16); - byte[] primaryBytes = _channel.ReadPage(primaryPage).Span.ToArray(); - int primaryFree = BinaryPrimitives.ReadUInt16LittleEndian(primaryBytes.AsSpan(format.DataFreeSpaceOffset, 2)); - - if (primaryFree >= 2 * (UsageMapRecordLength + 2)) - { - // Room on the table's usage-map page — append the two maps there (as ACE does). - lvMapPage = primaryPage; - lvUsedRow = BinaryPrimitives.ReadUInt16LittleEndian(primaryBytes.AsSpan(format.DataRowCountOffset, 2)); - lvFreeRow = lvUsedRow + 1; - } - else - { - // Full — give the column its own usage-map page (owned = row 0, free = row 1), the same - // fallback CREATE TABLE uses once its primary map page fills on a wide table. - lvMapPage = _allocator.Allocate(); - lvUsedRow = 0; lvFreeRow = 1; lvDedicated = true; - } - AddLongValueMapEntry(parts, maxCols, lvUsedRow, lvFreeRow, lvMapPage); - } + LongValueMapPlacement? lvMaps = isLongValue ? PlaceLongValueMaps(parts, maxCols) : null; WriteTdef(table.DefinitionPage, parts); - if (isLongValue) - { - if (lvDedicated) - WriteUsageMaps(format, lvMapPage, mapCount: 2); // owned = row 0, free = row 1, both empty - else - { - AppendEmptyUsageMapRow(format, lvMapPage, lvUsedRow); - AppendEmptyUsageMapRow(format, lvMapPage, lvFreeRow); - } - } + if (lvMaps is { } placed) WriteLongValueMaps(placed); // NOT NULL / DEFAULT go in the table's LvProp blob (DefaultValue before Required, matching ACE), the // same properties CREATE TABLE writes — appended to the existing blob without disturbing other columns'. @@ -1994,6 +1930,55 @@ public bool AddColumn(string tableName, ColumnSpec spec, string? defaultValue = return true; } + /// Where a column's two long-value usage-map records go: a page and its owned and free rows, on a page + /// of their own when the table's map page is full. + private readonly record struct LongValueMapPlacement(int Page, int UsedRow, int FreeRow, bool Dedicated); + + /// + /// Gives a column that has become Memo/OLE its §3.3.2 usage-map entry in and says where + /// its owned and free map records go — for to write once the TDEF is. ACE + /// appends the two maps to the table's existing usage-map page right after the maps already there (verified, + /// for ADD COLUMN and for an ALTER COLUMN to Memo alike), and adds the 10-byte entry before the list's 0xFFFF + /// terminator. + /// + private LongValueMapPlacement PlaceLongValueMaps(TdefParts parts, int columnId) + { + JetFormatBase format = _channel.Format; + int o = format.TdefOwnedPagesOffset + 1; + int primaryPage = parts.Header[o] | (parts.Header[o + 1] << 8) | (parts.Header[o + 2] << 16); + byte[] primaryBytes = _channel.ReadPage(primaryPage).Span.ToArray(); + int primaryFree = BinaryPrimitives.ReadUInt16LittleEndian(primaryBytes.AsSpan(format.DataFreeSpaceOffset, 2)); + + LongValueMapPlacement placement; + if (primaryFree >= 2 * (UsageMapRecordLength + 2)) + { + // Room on the table's usage-map page — append the two maps there (as ACE does). + int used = BinaryPrimitives.ReadUInt16LittleEndian(primaryBytes.AsSpan(format.DataRowCountOffset, 2)); + placement = new LongValueMapPlacement(primaryPage, used, used + 1, Dedicated: false); + } + else + { + // Full — give the column its own usage-map page (owned = row 0, free = row 1), the same + // fallback CREATE TABLE uses once its primary map page fills on a wide table. + placement = new LongValueMapPlacement(_allocator.Allocate(), 0, 1, Dedicated: true); + } + AddLongValueMapEntry(parts, columnId, placement.UsedRow, placement.FreeRow, placement.Page); + return placement; + } + + /// Writes the two empty map records placed. + private void WriteLongValueMaps(LongValueMapPlacement placement) + { + JetFormatBase format = _channel.Format; + if (placement.Dedicated) + WriteUsageMaps(format, placement.Page, mapCount: 2); // owned = row 0, free = row 1, both empty + else + { + AppendEmptyUsageMapRow(format, placement.Page, placement.UsedRow); + AppendEmptyUsageMapRow(format, placement.Page, placement.FreeRow); + } + } + /// /// Gives the rows already in a table values in an AutoNumber column just added to it, as ACE does rather than /// leaving them NULL, and sets where the counter carries on (all verified). @@ -2211,8 +2196,8 @@ public bool DropCheckConstraint(string tableName, string checkName) /// column's max length** is a descriptor-length edit at ColumnLengthOffset: variable columns store /// each row's actual length, so widening rewrites no rows, and narrowing only scans them to check they /// still fit. Every other change — numeric type, a fixed column's size, fixed↔variable — is a full column - /// rewrite, handled by (byte-faithful with ACE) or, for a Memo/OLE - /// target, by . Nothing here throws NotSupported for a storage-type change. + /// rewrite, handled in place by (byte-faithful with ACE), Memo/OLE + /// included. Nothing here throws NotSupported for a storage-type change. public void AlterColumn(string tableName, string columnName, ColumnSpec newSpec) { TableDef table = _catalog.FindTable(tableName) @@ -2241,8 +2226,8 @@ public void AlterColumn(string tableName, string columnName, ColumnSpec newSpec) // A pure reseed of an existing counter — ALTER COLUMN c COUNTER(seed, increment) where c is already an // AutoNumber of the same storage type — changes only the next id, not the data or layout. It's an - // in-place TDEF header edit (0x14/0x18), exactly what ACE does; RewriteColumn would needlessly rebuild - // the whole table. (Changing the numeric type still rebuilds.) + // in-place TDEF header edit (0x14/0x18), exactly what ACE does; a retype would needlessly re-lay every + // row. (Changing the numeric type is a retype.) if (col.IsAutoNumber && newSpec.IsAutoNumber && col.Type == newSpec.Type) { ReseedCounter(table, col, newSpec.Seed, newSpec.Increment); @@ -2272,8 +2257,8 @@ public void AlterColumn(string tableName, string columnName, ColumnSpec newSpec) // identical declaration, and so fall through. // // Nullability is deliberately NOT compared: no ALTER path carries it. The SQL layer always builds - // the spec with NotNull false and applies Required separately afterwards, and RewriteColumn discards - // newSpec.IsNullable outright in favour of the target's. Comparing it here would make the check fail + // the spec with NotNull false and applies Required separately afterwards, and the in-place retype keeps + // the target's own. Comparing it here would make the check fail // for every NOT NULL column, so the same statement would burn an id — and throw at 255 — purely // because the column was required. if (col.Type is JetDataType.Boolean or JetDataType.Byte or JetDataType.Int16 or JetDataType.Int32 @@ -2473,219 +2458,6 @@ private void DemoteCounterToInt(TableDef table, ColumnDef col) _catalog.Invalidate(); } - /// Changes a column's storage type, matching ACE's column-modify semantics (verified): the column - /// keeps its position but is internally a new column — it gets a fresh id burned from the 0x29 - /// high-water, while every other column keeps its id and its original descriptor bytes (so fields - /// LibRed doesn't model are preserved, per the faithful round-trip rule). All target values are converted - /// in memory first (an unconvertible value fails before anything is written), then the rebuild — drop, - /// recreate with the new type, re-insert, recreate secondary indexes, re-add relationships — runs inside a - /// page-level transaction that rolls back atomically on any later failure. Column order, the primary - /// key, unique/secondary indexes, CHECK constraints, defaults, and AutoNumber values are preserved. Rejects a - /// table whose target column is in a relationship (drop the FK first). This is a logical rebuild (a fresh TDEF - /// page, not ACE's byte-exact in-place edit), but the resulting column layout — position, burned id, and the - /// untouched columns' bytes — matches what ACE produces. - private void RewriteColumn(string tableName, string columnName, ColumnSpec newColumnSpec) - { - TableDef def = _catalog.FindTable(tableName) - ?? throw new InvalidOperationException($"Table '{tableName}' does not exist."); - ColumnDef target = def.FindColumn(columnName) - ?? throw new InvalidOperationException($"Column '{columnName}' does not exist in '{tableName}'."); - - EnsureColumnIsNotInRelationship(def, target); - - // A complex (multi-value / attachment) column is held to its values by three links, and the rebuild - // threatens all three: its values live in an f_ flat table that DropTable would take with the - // table, its descriptor carries the MSysComplexColumns key at 0x0B (kept by the RawDescriptor - // passthrough, now that the builder leaves a complex column's union alone), and its catalog row names - // the owner by definition page — which the rebuild moves. So the flat tables and rows stay put, and - // the rows are re-pointed at the new page below. ACE performs this ALTER with every link intact. - var complexColumns = _catalog.ComplexColumns - .Where(c => string.Equals(c.OwnerTable.Name, tableName, StringComparison.OrdinalIgnoreCase)) - .Select(c => c.ComplexId) - .ToList(); - - // Read straight off the definition page: 0x29 and 0x1C are in the header, so this needs none of the - // block slicing (or continuation-page stitching) a full ParseTdef would do for two header fields. - PageBuffer priorHeader = _channel.ReadPage(def.DefinitionPage); - int priorHighWater = priorHeader.ReadUInt16(_channel.Format.TdefMaxColumnsOffset); - // The complex-column counter. A fresh table starts it at zero, so without this the next insert would - // hand out a complex id that already names another row's values. - int priorComplexAutoNumber = priorHeader.ReadInt32(_channel.Format.TdefComplexAutoNumberOffset); - if (priorHighWater >= MaxColumnsPerTable) - throw new NotSupportedException($"Cannot alter '{columnName}': too many fields defined — {MaxColumnsPerTable} column ids have been used."); - - // The rebuild drops + recreates the table, so every relationship it touches is captured and restored - // afterwards. OUTGOING FKs (this table is the child) are cascaded away by DropTable and re-added; their - // backing indexes are recreated with them, so they're excluded from `secondary`. INCOMING FKs (this table - // is the referenced parent) are dropped up front so the parent can be dropped, then re-added — this is - // what makes a parent-side rewrite work. Self-references count as outgoing only. - const StringComparison oic = StringComparison.OrdinalIgnoreCase; - var foreignKeys = _catalog.ForeignKeysOf(tableName).ToList(); - var incoming = _catalog.Relationships - .Where(r => string.Equals(r.ReferencedTable, tableName, oic) && !string.Equals(r.Table, tableName, oic)) - .ToList(); - var fkColumnSets = foreignKeys.Select(fk => fk.Columns.Select(c => c.Column).ToArray()).ToList(); - - // 1. Materialise all rows (values indexed by column position) before dropping the table. - var rows = new Table(_channel, def).Rows().Select(r => (object?[])r.Clone()).ToList(); - - // Every index's statistics, here and on each table referencing this one: the rebuild re-inserts the rows and - // re-creates the indexes, which would recount them all, where ACE's ALTER leaves every index alone but the - // ones over the column it changes (verified, for Memo/OLE retypes as for the rest). - var statistics = new[] { tableName }.Concat(incoming.Select(r => r.Table)).Distinct(StringComparer.OrdinalIgnoreCase) - .ToDictionary(t => t, t => IndexStatistics(_catalog.FindTable(t)!), StringComparer.OrdinalIgnoreCase); - - // 2. Reconstruct the schema — column order preserved, the target re-typed. Every OTHER column keeps its - // original descriptor bytes (RawDescriptor passthrough), so fields LibRed doesn't model survive the - // rewrite (the faithful round-trip rule); the target builds fresh (RawDescriptor null). Column ids stay - // contiguous by position — NOT burned like ACE — because the row codec's null bitmap is currently - // keyed by column id, which only agrees with ACE's (position-keyed) bitmap when id == position. Burning - // the id needs the codec switched to position-keying first (verified vs an ACE-modified file). TODO. - int targetIndex = target.Index; - var specs = def.Columns.Select(c => c.Index == targetIndex - ? newColumnSpec with { IsNullable = target.IsNullable, RawDescriptor = null } - : new ColumnSpec(c.Name, c.Type, c.Length, c.IsFixedLength, c.IsAutoNumber, c.Precision, c.Scale, - c.IsNullable, c.Seed, c.Increment, RawDescriptor: c.RawDescriptor, - CalculatedExpression: c.CalculatedExpression, CalculatedResultType: c.CalculatedResultType)).ToList(); - - IndexDef? pk = def.Indexes.FirstOrDefault(i => i.IsPrimaryKey); - IReadOnlyList? primaryKey = pk?.Columns.Select(c => c.Column.Name).ToList(); - // Secondary indexes to recreate — excluding the primary key and any FK-backing index (re-added with its FK). - var secondary = def.Indexes - .Where(i => !i.IsPrimaryKey) - .Where(i => !fkColumnSets.Any(cols => - cols.SequenceEqual(i.Columns.Select(c => c.Column.Name), StringComparer.OrdinalIgnoreCase))) - .ToList(); - var checks = def.CheckConstraints.ToList(); - var defaults = def.Columns.Where(c => c.DefaultValue is not null) - .Select(c => (Column: c.Name, DefaultSql: c.DefaultValue!)).ToList(); - - // The whole extended-property blob, kept and put back verbatim. Recreating the table writes a blob - // built from the specs — DefaultValue, Required, the calculated triple and the CHECK constraints — - // which is everything LibRed models and nothing else, so ValidationRule, Format, Description, - // AllowZeroLength, the table's own properties and anything Access wrote that LibRed has no name for - // would be lost. ACE's own ALTER leaves the blob alone; carrying it across is the same outcome. The - // properties it holds are owned by column NAME, which the rebuild preserves. - byte[]? properties = ReadObjectProperties(def.DefinitionPage); - - // And the table's permission rows, for the same reason: recreating it writes the pair a new table gets, - // which is not necessarily the set of grants this one carried. - var permissions = ReadPermissionRows(def.DefinitionPage); - - // And its MSysObjects.Flags: recreating it writes a new table's, dropping anything else it carried — the - // hidden bit, or the complex-column bit Access sets on the table that owns one. - int? objectFlags = ReadObjectFlags(def.DefinitionPage); - - // 3. Pre-check: convert every target value in memory BEFORE touching disk. An unconvertible value - // (e.g. non-numeric text → INT) throws here, with nothing written — the caller sees a clean failure. - foreach (object?[] row in rows) - row[targetIndex] = ConvertValue(row[targetIndex], newColumnSpec.Type, newColumnSpec.Name); - - // 4. Apply the rebuild atomically: wrap it in a page-level transaction so any failure that slips past the - // pre-check (a unique-index collision after narrowing, NOT NULL, an I/O or allocation error) rolls the - // whole operation back and leaves the table byte-unchanged — never a half-converted table. - bool ownTransaction = !_channel.InTransaction; - if (ownTransaction) _channel.BeginTransaction(); - try - { - // Drop incoming relationships (so the parent becomes unreferenced) → drop → recreate (PK only) → - // re-insert → recreate secondary indexes → re-add outgoing then incoming relationships. - foreach (ForeignKey r in incoming) { DropConstraint(r.Table, r.Name); _catalog.Invalidate(); } - DropTable(tableName, keepComplexColumns: true); - _catalog.Invalidate(); - Create(tableName, specs, primaryKey, relationships: null, uniqueConstraints: null, - columnDefaults: defaults, checkConstraints: checks, primaryKeyName: pk?.Name); - _catalog.Invalidate(); - - // The logical rebuild still lays live columns out contiguously, but must not reset - // lifetime id consumption. ACE consumes one even for identity Memo/OLE ALTER. - int rebuiltPage = _catalog.FindTable(tableName)!.DefinitionPage; - TdefParts rebuilt = ParseTdef(rebuiltPage); - BinaryPrimitives.WriteUInt16LittleEndian( - rebuilt.Header.AsSpan(_channel.Format.TdefMaxColumnsOffset, 2), (ushort)(priorHighWater + 1)); - BinaryPrimitives.WriteInt32LittleEndian( - rebuilt.Header.AsSpan(_channel.Format.TdefComplexAutoNumberOffset, 4), priorComplexAutoNumber); - WriteTdef(rebuiltPage, rebuilt); - _catalog.Invalidate(); - - // The table is a new object at a new page, so every complex column's catalog row has to follow it; - // the ids in the rows themselves are carried through the re-insert below, which is what keeps each - // record pointing at its own values. - foreach (int complexId in complexColumns) - SetComplexColumnOwner(complexId, rebuiltPage); - if (properties is { Length: > 0 }) WriteObjectProperties(rebuiltPage, properties); - if (objectFlags is int flags) UpdateCatalogRows("MSysObjects", "Id", rebuiltPage, required: true, ("Flags", flags)); - RestorePermissionRows(rebuiltPage, permissions); - _catalog.Invalidate(); - - var dest = new Table(_channel, _catalog.FindTable(tableName)!); - int[] calculatedColumns = dest.Definition.Columns - .Where(c => c.IsCalculated) - .Select(c => c.Index) - .ToArray(); - foreach (object?[] row in rows) - { - // A calculated value is a cache, not caller-supplied data. Recompute it from its expression - // while rebuilding, as an ordinary insert does; reinserting the old cache is rejected. - foreach (int index in calculatedColumns) row[index] = null; - dest.Insert(row); - } - - // Restore each index AS IT WAS. IgnoreNulls and Required are read off the 0x2E flags word and are - // right here on the IndexDef; hard-coding them false made an ALTER COLUMN on an unrelated column - // silently turn a WITH IGNORE NULL index into a plain one — changing which rows are in the B-tree - // — and stop ACE enforcing DISALLOW NULL. - foreach (IndexDef ix in secondary) - { - AddIndex(tableName, ix.Name, ix.Columns.Select(c => (c.Column.Name, !c.Ascending)).ToList(), - ix.IsUnique, isPrimary: false, disallowNull: ix.Required, ignoreNulls: ix.IgnoreNulls); - _catalog.Invalidate(); - } - - // Re-add the outgoing foreign keys (recreates their backing index + linkage + MSysRelationships rows). - foreach (ForeignKey fk in foreignKeys) - { - AddForeignKey(tableName, new RelationshipSpec(fk.Name, fk.ReferencedTable, fk.Columns.ToList(), - fk.IsEnforced, fk.CascadeUpdate, fk.CascadeDelete, NoIndex: false, - DeleteSetNull: fk.DeleteSetNull, UpdateSetNull: fk.UpdateSetNull)); - _catalog.Invalidate(); - } - - // Re-add the incoming relationships — each child's FK back to the rebuilt parent. - foreach (ForeignKey r in incoming) - { - AddForeignKey(r.Table, new RelationshipSpec(r.Name, r.ReferencedTable, r.Columns.ToList(), - r.IsEnforced, r.CascadeUpdate, r.CascadeDelete, NoIndex: false, - DeleteSetNull: r.DeleteSetNull, UpdateSetNull: r.UpdateSetNull)); - _catalog.Invalidate(); - } - - // Put the statistics back as ACE leaves them: every index as it was, except one over the changed column, - // which ACE rebuilds — counted from the rows it now holds, the primary key too (which the re-insert - // counted as inserts). - foreach ((string name, Dictionary before) in statistics) - { - TableDef table = _catalog.FindTable(name)!; - foreach (IndexDef index in table.Indexes) - { - if (string.Equals(name, tableName, oic) - && index.Columns.Any(c => string.Equals(c.Column.Name, columnName, oic))) - SetBuiltStatistics(table, index, IndexEntries(table, index, index.IgnoreNulls)); - else if (before.TryGetValue(index.Name, out (int Total, int Unique) kept)) - WriteIndexStatistics(table, index, kept.Total, kept.Unique); - } - } - - if (ownTransaction) _channel.CommitTransaction(); - } - catch when (ownTransaction) - { - _channel.RollbackTransaction(); - _catalog.Invalidate(); // the in-memory catalog cache is stale after the pages are restored - throw; - } - } - /// Applies ACE's in-place column retype to the target descriptor within /// (no page write — the caller writes the TDEF once): the target becomes a NEW column with a fresh id from the /// 0x29 high-water and its fixed data appended to the END of the current fixed region (its old slot left @@ -2730,8 +2502,8 @@ private static int EditTargetDescriptor(TdefParts parts, ColumnDef target, Colum } /// Full in-place column type change, byte-for-byte like ACE for fixed and variable columns and - /// targets, fixed↔variable, and indexed targets; a Memo/OLE source or target falls back to - /// . Edits the TDEF in place () and re-lays every row — the target's + /// targets, fixed↔variable, indexed targets, and a Memo/OLE source or target (whose long-value maps are placed + /// and retired as ADD and DROP COLUMN do). Edits the TDEF in place () and re-lays every row — the target's /// OLD fixed slot is kept as dead space, its converted value appended at the new offset, count + null bitmap /// updated. Converts values in memory first (throws on bad data before any write); runs in a transaction. public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSpec newSpec) @@ -2750,14 +2522,13 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp RecordLayout.ValidateFieldWidth(newSpec.Name, newSpec.Type, newSpec.Length); JetDataTypeVersions.EnsureStorable(newSpec.Type, _channel.Format.Version, newSpec.Name); - // A long-value (Memo/OLE) target — or converting one away — needs long-value column mechanics (a §3.3.2 - // usage-map entry, LVAL pages, freeing the old value). That's out of scope for the byte-faithful in-place - // edit; the logical rebuild (Create handles long-value columns) does it correctly, if not byte-exactly. - if (newSpec.Type is JetDataType.Memo or JetDataType.Ole || oldTarget.Type is JetDataType.Memo or JetDataType.Ole) - { - RewriteColumn(tableName, columnName, newSpec); - return; - } + // A column becoming Memo/OLE, or ceasing to be one, takes the same in-place edit as any other retype plus the + // long-value side of ADD and DROP COLUMN (verified vs ACE, both directions): the new column gets its §3.3.2 + // entry and two empty map records appended to the table's map page, and each converted value is stored as an + // insert stores it — inline up to 64 bytes, else on an LVAL page; the old column's entry leaves the TDEF, its + // map records are retired and its pages go back at close, their bytes untouched. + bool fromLongValue = oldTarget.Type is JetDataType.Memo or JetDataType.Ole; + bool toLongValue = newSpec.Type is JetDataType.Memo or JetDataType.Ole; // Indexes that include the target column must be rebuilt (their keys change type) — captured now. var affectedIndexes = oldDef.Indexes @@ -2817,9 +2588,31 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp pending.Add((ixName, index.RootPage, newRoot, index.IgnoreNulls)); } + // The long-value side: the old column's maps, read before the TDEF loses them, as DROP COLUMN reads them; + // the new column's entry, placed as ADD COLUMN places it. + UsageMap? oldMaps = null; + var released = new HashSet(); + var retire = new List(); + if (fromLongValue) + { + var oldDefinition = new TableDefinitionPage(); + oldDefinition.Read(_channel, oldDef.DefinitionPage); + oldMaps = new UsageMap(_channel, oldDef); + QueueLongValueMaps(oldDefinition, oldTarget, oldMaps, released, retire); + RemoveLongValueMapEntry(parts, oldTargetId); + } + LongValueMapPlacement? newMaps = toLongValue ? PlaceLongValueMaps(parts, newTargetId) : null; + WriteTdef(oldDef.DefinitionPage, parts); _catalog.Invalidate(); + if (newMaps is { } placed) WriteLongValueMaps(placed); + if (oldMaps is not null) + { + RetireMapRecords(retire, oldMaps, released); + foreach (int page in released) _allocator.Release(page); // reusable only after this handle closes + } + TableDef newDef = _catalog.FindTable(tableName)!; ColumnDef newTarget = newDef.FindColumn(columnName)!; int newMaxId = newDef.Columns.Max(c => c.ColumnId); @@ -2836,9 +2629,14 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp // 3. Re-lay each row: old fixed region + old var chunks verbatim (incl. the dead old slot), target // appended (a new fixed slot, or a new variable chunk); count/var-table/null-bitmap rebuilt. + // A long value is stored first, as an insert would store it, so the chunk appended is its descriptor. foreach (var r in rows) + { + if (toLongValue) + r.Values[newTarget.Index] = writer.MaterializeLongValue(newTarget, r.Values[newTarget.Index]); writer.RewriteRowRaw(r.Id, BuildRelaidRecord( r.Raw, oldFixedLen, oldDef.Columns, newTarget, r.Values, newDef.Columns, newMaxId, newFixedLen, newVarCount)); + } // 4. Finish each index rebuild: backfill the fresh B-tree with new-type keys, then free the old root // (last, so the new root got the appended page rather than reusing this one) — as ACE does. @@ -2858,7 +2656,7 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp /// region and OLD variable chunks are kept verbatim (the dead old-target slot/chunk keeps its stale bytes), /// the converted target is appended (a new fixed slot if it is fixed, else a new variable chunk), and the /// leading count (= max id + 1), variable-offset table + numVar (omitted if none), and null bitmap - /// (dead-id bits set present) are rebuilt. + /// (each dead id's bit carried over from the old row) are rebuilt. private static byte[] BuildRelaidRecord(byte[] oldRow, int oldFixedLen, IReadOnlyList oldCols, ColumnDef newTarget, object?[] values, IReadOnlyList newCols, int newMaxId, int newFixedLen, int newVarCount) @@ -2879,6 +2677,14 @@ private static byte[] BuildRelaidRecord(byte[] oldRow, int oldFixedLen, IReadOnl Array.Copy(oldRow, 2, newFixed, 0, rowFixedLen); if (newTarget.IsFixedLength && tv is not null) Array.Copy(targetBytes, 0, newFixed, newTarget.FixedOffset, newTarget.Length); + else if (newTarget.IsFixedLength && rowFixedLen == oldFixedLen) + { + // ACE writes nothing into the new slot of a NULL: it holds whatever the old record had at those + // offsets — the start of its variable data, say — rather than zeros (verified on full-width rows). + int from = 2 + newTarget.FixedOffset; + int length = Math.Min(newTarget.Length, oldRow.Length - from); + if (length > 0) Array.Copy(oldRow, from, newFixed, newTarget.FixedOffset, length); + } // Variable chunks: old chunks verbatim (incl. a dead variable chunk), padded out to the table's slot // count so the target lands on the index its descriptor names, then the target placed at that index. @@ -2886,8 +2692,11 @@ private static byte[] BuildRelaidRecord(byte[] oldRow, int oldFixedLen, IReadOnl while (chunks.Count < newVarCount) chunks.Add([]); if (!newTarget.IsFixedLength) chunks[newTarget.VariableIndex] = targetBytes; - // Assemble via the shared row layout (count + var table + null bitmap identical to a fresh encode). - return RowEncoder.AssembleRow(newMaxId, newFixed, chunks, newCols, values); + // Assemble via the shared row layout (count + var table + null bitmap identical to a fresh encode), each + // dead id's bit carried over from the old row's bitmap. + int oldCount = BinaryPrimitives.ReadUInt16LittleEndian(oldRow); + return RowEncoder.AssembleRow(newMaxId, newFixed, chunks, newCols, values, + priorBitmap: oldRow.AsSpan(oldRow.Length - (oldCount + 7) / 8)); } /// The length of a row's fixed-data region (bytes between the leading count and the variable data), @@ -3241,9 +3050,9 @@ private void DropVersionHistory(string tableName, string memoName, VersionHistor int tdefPage = _catalog.RequireTable(tableName).DefinitionPage; DropColumn(tableName, history.ColumnName); // the complex-column branch takes its index with it - DropTable(history.FlatTable, keepComplexColumns: false, keepPermissions: true); + DropTable(history.FlatTable, keepPermissions: true); DeleteCatalogRows("MSysComplexColumns", "ComplexID", history.ComplexId); - DropTable(history.Template, keepComplexColumns: false, keepPermissions: true); + DropTable(history.Template, keepPermissions: true); if (!_catalog.ComplexColumns.Any(c => string.Equals(c.OwnerTable.Name, tableName, StringComparison.OrdinalIgnoreCase)) && ReadObjectFlags(tdefPage) is int flags && (flags & CatalogFormat.ObjectFlagOwnsComplexColumns) != 0) @@ -3779,35 +3588,6 @@ private void AddCatalogRow(string name, int tdefPage, private void AddPermissionRows(int objectId) => new CatalogWriter(_channel, _catalog).AddPermissionRows(objectId, CatalogFormat.ObjectContainerParentId); - /// An object's MSysACEs rows, whole. A table need not carry only the pair - /// writes: a workgroup-secured database grants to as many accounts as it - /// likes, and those rows are the grants themselves, not a description of them. - private List ReadPermissionRows(int objectId) - { - TableDef msysAces = _catalog.RequireTable("MSysACEs"); - int idIndex = msysAces.RequireColumn("ObjectId").Index; - return [.. RowsKeyed(new Table(_channel, msysAces), idIndex, objectId).Select(r => r.Values)]; - } - - /// Makes the object's only MSysACEs rows, re-pointed at it — for a - /// rebuild, which has just been given the standard pair for a brand-new table and needs the grants the - /// table actually had. - private void RestorePermissionRows(int objectId, List rows) - { - if (rows.Count == 0) return; - TableDef msysAces = _catalog.RequireTable("MSysACEs"); - int idIndex = msysAces.RequireColumn("ObjectId").Index; - - DeleteCatalogRows("MSysACEs", "ObjectId", objectId); - var inserter = new RowInserter(_channel, msysAces); - foreach (object?[] row in rows) - { - var values = (object?[])row.Clone(); - values[idIndex] = objectId; - inserter.Insert(values, updateIndexes: true); - } - } - private static void WriteInt24(byte[] buffer, int offset, int value) { buffer[offset] = (byte)value; diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index f417f4e2..53147ce1 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -71,7 +71,7 @@ All integers little-endian unless noted; offsets are hex, relative to the struct ``` [colCount:2 = TDEF 0x29 high-water] [fixed data] [var data] [varOffsetTable:(numVar+1)×2] [numVar:2] [nullBitmap:ceil(colCount/8)] ``` -Variable section (`varOffsetTable`+`numVar`) omitted only when the table has never had a variable column (TDEF `0x2B` = 0); `numVar` is that high-water. Null bitmap keyed by column id (set = present); a dead id's bit is set in a row the ALTER COLUMN re-lay rewrote and clear in one inserted afterwards. Booleans carry no data (the bit *is* the value). With the variable section omitted, `fixed data` is padded to a **minimum of 2 bytes** (a floor, not an alignment — an odd 3-byte region stays 3), making 5 the shortest record; ACE misreads anything shorter. +Variable section (`varOffsetTable`+`numVar`) omitted only when the table has never had a variable column (TDEF `0x2B` = 0); `numVar` is that high-water. Null bitmap keyed by column id (set = present); a dead id's bit keeps what the row had in a row the ALTER COLUMN re-lay rewrote (set where the column held a value, clear where it was NULL) and is clear in one inserted afterwards. Booleans carry no data (the bit *is* the value). With the variable section omitted, `fixed data` is padded to a **minimum of 2 bytes** (a floor, not an alignment — an odd 3-byte region stays 3), making 5 the shortest record; ACE misreads anything shorter. --- diff --git a/src/LibRed/docs/format/page-01-data-and-rows.md b/src/LibRed/docs/format/page-01-data-and-rows.md index d5dc669b..9d8d54d0 100644 --- a/src/LibRed/docs/format/page-01-data-and-rows.md +++ b/src/LibRed/docs/format/page-01-data-and-rows.md @@ -231,9 +231,10 @@ not set the commit slot: it keeps no user slot of its own (§2.2). the high-water, not the highest live id, which would have given 1. A writer that sizes these by the live count writes a bit ACE cannot find for any id ≥ live count → ACE reads that column null. - **A dead id's bit depends on which route wrote the row** (verified). The **ALTER COLUMN re-lay** carries the - old row's bit forward, so a retype's burned id reads **present** (`0x0F` above). A row **inserted - afterwards** leaves it **clear** (`0x0D` on the same table), as do the dropped ids after a `DROP COLUMN` - (`01` for a live column 0 with ids 1 and 2 dropped). + old row's bit forward unchanged: the retyped column's old id stays **present** where the row held a value + (`0x0F` above) and **clear** where it was NULL. A row **inserted afterwards** leaves it **clear** (`0x0D` on + the same table), as do the dropped ids after a `DROP COLUMN` (`01` for a live column 0 with ids 1 and 2 + dropped). - **Null bitmap** is indexed by **column id**; a **set bit = the value is present** (non-null). - **Fixed** column value is at `rowStart + 2 + fixedOffset`, `length` bytes. - A **fixed-length text** column (`CHAR`/`NCHAR`, not `TEXT`/`VARCHAR`) fills its whole `length`: the value is diff --git a/src/LibRed/docs/format/page-02b-columns.md b/src/LibRed/docs/format/page-02b-columns.md index c2470299..e6609f86 100644 --- a/src/LibRed/docs/format/page-02b-columns.md +++ b/src/LibRed/docs/format/page-02b-columns.md @@ -372,13 +372,12 @@ fixed↔variable, PK, indexed, multi-page and decimal shapes, including repeated This is **the same mechanism for every type/length change** — including a *widening* `TEXT(n)→TEXT(m)`; there is no cheap "just bump the length" path, ACE burns the id there too. -LibRed's Memo/OLE logical rebuild has a different layout but enforces the same id high-water limit; -see [§3.1](page-02a-tdef.md#31-header). +The same holds for a Memo/OLE source or target (below); the id high-water limit is [§3.1](page-02a-tdef.md#31-header). > **Relationship columns cannot be altered.** ACE rejects a type or length change when the target is either > a referencing FK column or its referenced parent column: *"Cannot change field 'X'. It is part of one or -> more relationships."* This is verified for both sides. LibRed performs this check before choosing an -> in-place edit or logical rebuild, so no descriptor, row, or index page is changed on rejection. +> more relationships."* This is verified for both sides. LibRed performs this check before the edit, so no +> descriptor, row, or index page is changed on rejection. **TDEF header:** the max-column-id high-water (`0x29`, §3.1) bumps **+1** (this is the burned id). For a change **to a variable type**, the variable-column count (`0x2B`) also bumps **+1**. Every field burn is @@ -395,14 +394,11 @@ null bitmap is keyed by **id** and not by position (§5). > one and is rejected at 255. ACE accepts the identity ALTER at 255 for a `NOT NULL` column as readily as a > nullable one, so > LibRed must not compare nullability when deciding an ALTER is a no-op — and no ALTER path carries it -> anyway, since `Required` is applied separately and `RewriteColumn` discards the spec's value. - -> **Where LibRed's Memo/OLE rebuild diverges.** `RewriteColumn` preserves each untouched column's original -> descriptor bytes except the fields LibRed models (the `RawDescriptor` passthrough) and keeps column order, -> but it does **not** give the target the burned id: it rebuilds with **contiguous** ids and re-encodes rows -> with null bits keyed by those, rather than retaining ACE's old ids and dead storage. The `0x29` high-water -> is still preserved and incremented and an ALTER at 255 still rejected, so the lifetime cap matches ACE -> even though the layout does not. The in-place path above retains the ids and dead storage as ACE does. +> anyway, since `Required` is applied separately and the in-place edit keeps the column's own. +> +> Because the edit is in place, everything the ALTER does not touch stays where it was — every other column's +> descriptor and id, and every index, the primary key included, in its place among the logical and index-data +> blocks (verified). **Target column descriptor (§3.4)** — the *only* descriptor that changes; all others stay byte-identical: @@ -426,7 +422,18 @@ from the schema (§5). the **old fixed region and old variable chunks are kept verbatim** (the dead old-target slot / chunk keeps its stale bytes), and the converted target is **appended** — a new fixed slot at the offset above, or a new variable chunk at variable-index = the old var count. The leading count, variable-offset table + `numVar`, -and null bitmap are then rebuilt per §5 (count and bitmap width = max id + 1, dead ids' bits set present). +and null bitmap are then rebuilt per §5 (count and bitmap width = max id + 1; each dead id's bit as the old +row had it — so the retyped column's old id is present where it held a value and clear where it was NULL). +A fixed target whose converted value is NULL gets no bytes written: its new slot holds whatever the old record +had at those offsets (the start of its variable data, on a row with some), not zeros. + +**A Memo/OLE source or target** takes the same edit and re-lay, plus the long-value side of ADD and DROP +COLUMN (verified, both directions and a Memo re-declared as Memo). A column becoming Memo/OLE gets its §3.3.2 +entry, and its owned and free map records appended to the table's usage-map page; each converted value is then +stored as an insert stores it — inline (a memo compressed) up to 64 bytes, else on an LVAL page — and the row +carries its descriptor as the appended chunk. A column ceasing to be one has its entry removed and its map +records retired as a dropped column's are; its LVAL pages go back to the global free map with their bytes +untouched, and each row keeps the old descriptor as its dead chunk. **Indexed target — full index rebuild.** When the modified column is in an index, ACE reconstructs that index (its keys change type). Verified: diff --git a/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs b/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs new file mode 100644 index 00000000..244af9c7 --- /dev/null +++ b/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs @@ -0,0 +1,110 @@ +using System.Buffers.Binary; +using System.Data.OleDb; +using LibRed; +using LibRed.Catalog; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Engine.Tests; + +// ALTER COLUMN retypes a column in place, Memo/OLE included (page-02b-columns): the target's descriptor is edited, +// every row re-laid, and a column becoming or ceasing to be a long value gets or loses its §3.3.2 maps as ADD and +// DROP COLUMN give and take them. ACE and LibRed run the same ALTER on copies of one file and must leave the same +// file: the same catalog and rows, and the same bytes on every page but MSysObjects' own (its DateUpdate is the +// clock) — and, when a Memo is re-declared as a Memo, the dead space below the live records on the table's +// usage-map page, where ACE leaves stale copies of the records it retired. +[Collection(AceCollection.Name)] +public class ColumnRetypeParityAccessTests +{ + private static readonly string[] Values = ["short", new string('a', 31), new string('b', 32), new string('c', 33), new string('d', 200)]; + + [Theory] + [InlineData("T", "ALTER TABLE T ALTER COLUMN B MEMO")] // Text -> Memo: short values inline, long on a page + [InlineData("M", "ALTER TABLE M ALTER COLUMN M TEXT(255)")] // Memo -> Text: the maps retired, the page released + [InlineData("M", "ALTER TABLE M ALTER COLUMN M MEMO")] // Memo re-declared: an id burned, maps replaced + [InlineData("T", "ALTER TABLE T ALTER COLUMN X DOUBLE")] // a fixed retype over a NULL: the dead bit and slot + public void A_retype_leaves_the_file_ACE_leaves(string table, string alter) + { + string basePath = TemporaryDatabase.CopyPath(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "retype-base-"); + string acePath = TemporaryDatabase.CopyPath(basePath, "retype-ace-"); + string libredPath = TemporaryDatabase.CopyPath(basePath, "retype-libred-"); + try + { + using (OleDbConnection c = AceTestDatabase.Open(acePath)) + { + Exec(c, "CREATE TABLE T (ID LONG PRIMARY KEY, B TEXT(255), X LONG)"); + Exec(c, "CREATE TABLE M (ID LONG PRIMARY KEY, M MEMO, X LONG)"); + for (int i = 0; i < Values.Length; i++) + foreach (string t in (string[])["T", "M"]) + Exec(c, $"INSERT INTO {t} VALUES ({i + 1}, '{Values[i]}', {i + 1})"); + Exec(c, "INSERT INTO T VALUES (9, NULL, 9)"); + Exec(c, "INSERT INTO M VALUES (9, NULL, 9)"); + Exec(c, "INSERT INTO T VALUES (10, 'z', NULL)"); + } + OleDbConnection.ReleaseObjectPool(); + File.Copy(acePath, libredPath, overwrite: true); + + using (OleDbConnection c = AceTestDatabase.Open(acePath)) Exec(c, alter); + OleDbConnection.ReleaseObjectPool(); + using (var db = JetDatabase.Open(libredPath, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery(alter); + + Assert.Equal(Describe(acePath, table), Describe(libredPath, table)); + + byte[] ace = File.ReadAllBytes(acePath), libred = File.ReadAllBytes(libredPath); + Assert.Equal(ace.Length, libred.Length); + (int catalogPage, int usageMapPage, int lowestRecord) = Landmarks(acePath, table); + for (int page = 1; page < ace.Length / 4096; page++) + { + var differ = Enumerable.Range(0, 4096).Where(i => ace[page * 4096 + i] != libred[page * 4096 + i]).ToList(); + if (differ.Count == 0 || BinaryPrimitives.ReadInt32LittleEndian(ace.AsSpan(page * 4096 + 4)) == catalogPage) continue; + Assert.True(page == usageMapPage && differ.All(i => i < lowestRecord), + $"page {page} differs at 0x{differ[0]:X3} ({differ.Count} bytes)"); + } + } + finally + { + TemporaryDatabase.Delete(basePath); + TemporaryDatabase.Delete(acePath); + TemporaryDatabase.Delete(libredPath); + } + } + + // The table's columns (descriptors whole), long-value maps, and each row's raw record and value. + private static string Describe(string path, string tableName) + { + var lines = new List(); + using var db = JetDatabase.Open(path, readOnly: true); + TableDef t = db.Catalog.FindTable(tableName)!; + var tdef = db.ReadTableDefinition(t.DefinitionPage); + lines.AddRange(t.Columns.Select(c => $"{c.Name} {c.Type} id {c.ColumnId} {Convert.ToHexString(c.RawDescriptor ?? [])}")); + lines.AddRange(tdef.LongValueOwnedMaps.Select(m => $"maps {m.Key}: {m.Value} {tdef.LongValueFreeMaps[m.Key]}")); + var table = db.OpenTable(tableName); + var reader = new RowInserter(table.Channel, t); + foreach ((RowId id, object?[] values) in table.Rows().WithIds()) + lines.Add($"{id.Page}:{id.Row} {Convert.ToHexString(reader.ReadRow(id))} {string.Join("|", values.Select(v => v?.ToString() ?? "null"))}"); + return string.Join("\n", lines); + } + + // MSysObjects' definition page (its data pages carry it as owner), the table's usage-map page, and the offset of + // the lowest live record on that page. + private static (int CatalogPage, int UsageMapPage, int LowestRecord) Landmarks(string path, string tableName) + { + using var db = JetDatabase.Open(path, readOnly: true); + TableDef t = db.Catalog.FindTable(tableName)!; + byte[] file = File.ReadAllBytes(path); + int holder = BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(t.DefinitionPage * 4096 + db.Format.TdefOwnedPagesOffset)) >> 8; + int rows = BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(holder * 4096 + db.Format.DataRowCountOffset)); + int lowest = Enumerable.Range(0, rows) + .Select(i => BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(holder * 4096 + db.Format.DataRowDirectoryOffset + i * 2))) + .Where(s => (s & 0x8000) == 0).Min(s => s & 0x1FFF); + return (db.Catalog.FindTable("MSysObjects")!.DefinitionPage, holder, lowest); + } + + private static void Exec(OleDbConnection connection, string sql) + { + using var command = connection.CreateCommand(); + command.CommandText = sql; + command.ExecuteNonQuery(); + } +} diff --git a/test/LibRed.Engine.Tests/AlterColumnTests.cs b/test/LibRed.Engine.Tests/AlterColumnTests.cs index 84498acb..f77498f4 100644 --- a/test/LibRed.Engine.Tests/AlterColumnTests.cs +++ b/test/LibRed.Engine.Tests/AlterColumnTests.cs @@ -35,6 +35,30 @@ public void Widen_a_text_column_keeps_existing_data() e.ExecuteNonQuery("INSERT INTO T (K, V) VALUES (2, 'a much longer value than twenty chars')"); } + // A Memo retype edits the definition in place, as ACE's does — it once rebuilt the table and recreated the primary + // key first. A key added after another index keeps its place, and so does every other index, and the + // relationship a child table holds to it. + [Fact] + public void A_memo_retype_keeps_the_indexes_in_their_order() + { + var (e, db) = Fresh(); + e.ExecuteNonQuery("CREATE TABLE P ( ID LONG NOT NULL, A LONG, B TEXT(20) )"); + e.ExecuteNonQuery("CREATE INDEX IX_A ON P (A)"); + e.ExecuteNonQuery("ALTER TABLE P ADD CONSTRAINT PK_P PRIMARY KEY (ID)"); + e.ExecuteNonQuery("CREATE INDEX IX_B ON P (B)"); + e.ExecuteNonQuery("CREATE TABLE C ( CID LONG, PID LONG, CONSTRAINT FK_C_P FOREIGN KEY (PID) REFERENCES P (ID) )"); + e.ExecuteNonQuery("INSERT INTO P (ID, A, B) VALUES (1, 10, 'x')"); + + e.ExecuteNonQuery("ALTER TABLE P ALTER COLUMN B MEMO"); + + var p = db.Catalog.FindTable("P")!; + Assert.Equal(["IX_A", "PK_P", "IX_B"], p.Indexes.Select(i => i.Name)); + Assert.True(p.Indexes[1].IsPrimaryKey); + Assert.Equal(["A", "ID", "B"], p.RealIndexes.Select(i => i.Columns.Single().Column.Name)); + Assert.Contains(db.Catalog.Relationships, r => r.Name == "FK_C_P" && r.ReferencedTable == "P"); + Assert.Equal("x", e.ExecuteQuery("SELECT B FROM P WHERE ID = 1").Rows.Single()[0]); + } + [Fact] public void Narrow_a_text_column_is_a_metadata_change() { From 6bee7f83ba1eb1b930ea971c5f3bbd5bdf3d7771 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 21:44:56 +0800 Subject: [PATCH 64/88] Remove what only the ALTER COLUMN table rebuild used With every ALTER now an edit in place, nothing builds a column from an existing descriptor any more. The raw-descriptor passthrough on ColumnSpec, BuildColumnDescriptor's start-from-the-original path, the two flag masks it needed, and TdefBuilder.Build's complex-counter parameter had the rebuild as their only caller, and go. The comments and the spec sentence that described unmodelled descriptor bytes surviving "through a rebuild" now say why they survive: the descriptor is never re-emitted. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/ColumnDef.cs | 9 +-- src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs | 75 ++++++------------- .../LibRed.Core/Formats/JetFormatBase.cs | 7 -- src/LibRed/LibRed.Core/JetDatabase.cs | 6 +- .../LibRed.Core/Pages/TableDefinitionPage.cs | 7 +- .../LibRed.Core/Storage/TableCreator.cs | 30 ++++---- src/LibRed/docs/format/page-02b-columns.md | 8 +- .../AceAlterColumnTests.cs | 2 +- .../ColumnDescriptorFlagTests.cs | 31 ++------ .../ComplexAutoNumberRoundTripTests.cs | 14 ++-- 10 files changed, 60 insertions(+), 129 deletions(-) diff --git a/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs b/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs index fd8f7618..5b85a024 100644 --- a/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs +++ b/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs @@ -117,11 +117,10 @@ public sealed class ColumnDef /// LibRed encodes index keys for 417 collation configurations, not only General legacy. public Collation Collation { get; init; } = Collation.GeneralLegacy; - /// The column's original on-disk descriptor bytes (the 25-byte Jet4 record), captured verbatim on - /// read. Every documented field is now modelled explicitly, so this is carried only to re-emit the - /// genuinely reserved/unknown bytes on a rewrite — the reserved words at 0x03 and 0x11, - /// and the undocumented bits of the two flag bytes (0x0F/0x10) — instead of stamping zero over - /// them (the faithful round-trip rule). Null for a freshly-built (never-read) column. + /// The column's on-disk descriptor bytes (the 25-byte Jet4 record), captured verbatim on read — + /// the reserved words at 0x03 and 0x11 and the undocumented flag bits included, which no + /// modelled property carries. Nothing writes from it: every DDL edits the descriptor in place, so those + /// bytes are never re-emitted. Null for a freshly-built (never-read) column. public byte[]? RawDescriptor { get; init; } /// Catalog flag bits (byte 0x0F) to force-set on a created column — the system-catalog column marker diff --git a/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs b/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs index e7a06782..3adab88a 100644 --- a/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs +++ b/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs @@ -19,11 +19,9 @@ public sealed record ColumnSpec( // Default 1/1 (a plain COUNTER). Stored in the TDEF header: last-value 0x14 = Seed-Increment, 0x18 = Increment. int Seed = 1, int Increment = 1, - // Faithful-rebuild passthrough (ALTER COLUMN): an explicit column id (else the column's position is used) - // and the column's original 25-byte descriptor, re-emitted verbatim except for the fields LibRed manages - // so a rebuild preserves unmodeled bytes. Both null for an ordinary CREATE/ADD column. See ColumnDef.RawDescriptor. + // An explicit column id, else the column's position is used. Access's own catalog tables declare their + // columns in an order that is not their id order (DatabaseCreator); null for an ordinary CREATE/ADD column. int? ColumnId = null, - byte[]? RawDescriptor = null, // The catalog flag bits (0x0F) Access sets on the columns of its own catalog — MSysObjects, MSysACEs, // MSysQueries, MSysRelationships and MSysComplexColumns: 0x10 on every one, and 0x20 as well on the two that // hold a security identifier (MSysObjects.Owner, MSysACEs.SID). Every other column leaves them clear, the @@ -150,7 +148,6 @@ public sealed record Result(byte[] Page, IReadOnlyList Columns); /// The table's indexes, or null for none. /// The memo/OLE columns' usage-map pointers. /// Explicit logical-index blocks, or null to derive them from the indexes. - /// The complex-column counter (TDEF 0x1C). public static Result Build( JetFormatBase format, TableType tableType, @@ -158,8 +155,7 @@ public static Result Build( Collation collation, IReadOnlyList? indexes = null, IReadOnlyList? longValueColumns = null, - IReadOnlyList? logicalIndexes = null, - int complexAutoNumber = 0) + IReadOnlyList? logicalIndexes = null) { indexes ??= []; longValueColumns ??= []; @@ -191,9 +187,9 @@ public static Result Build( // CREATE silently took the first and ignored the rest. // // Complex columns are exempt, and are not a second claimant: they carry the same 0x04 flag but are - // allocated from 0x1C, not from this pair. Counting them would refuse to rebuild any table that has an - // ordinary counter beside a complex column — complex1.accdb's Table1 has one of each kind — and could - // hand `counter` a complex spec whose seed/increment describe nothing. + // allocated from 0x1C, not from this pair. Counting them would refuse a table that has an ordinary + // counter beside a complex column — complex1.accdb's Table1 has one of each kind — and could hand + // `counter` a complex spec whose seed/increment describe nothing. var counters = specs.Where(s => s.IsAutoNumber && s.Type != JetDataType.Complex).ToList(); if (counters.Count > 1) throw new NotSupportedException( @@ -201,9 +197,8 @@ public static Result Build( ColumnSpec? counter = counters.FirstOrDefault(); if (counter is { Increment: 0 }) throw ZeroIncrement(counter.Name); BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefAutoNumberIncrementOffset, 4), counter?.Increment ?? 1); - // Complex-type AutoNumber high-water (0x1C) — 0 for a table with no complex column, carried through on - // a rebuild for faithful round-trip. - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefComplexAutoNumberOffset, 4), complexAutoNumber); + // Complex-type AutoNumber high-water (0x1C): 0 for a new table, which has no complex column. + BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefComplexAutoNumberOffset, 4), 0); BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefNextPageOffset, 4), 0); BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefRowCountOffset, 4), 0); if (counter is not null) @@ -211,7 +206,7 @@ public static Result Build( page.AsSpan(format.TdefLastAutoNumberOffset, 4), counter.Seed - counter.Increment); page[format.TdefTableTypeOffset] = (byte)tableType; // The 0x29 high-water is the next column id to hand out = max existing id + 1. For contiguous ids this - // equals the column count; when a rebuild carries a burned id (ALTER COLUMN) it exceeds the count. + // equals the column count. int maxColumnId = columns.Select(c => c.ColumnId).DefaultIfEmpty(-1).Max(); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefMaxColumnsOffset, 2), (ushort)(maxColumnId + 1)); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefVariableColumnsOffset, 2), @@ -292,9 +287,6 @@ private static void ValidateColumnSpecs(JetFormatBase format, IReadOnlyList= MaxColumnsPerTable) throw new NotSupportedException( @@ -511,9 +503,8 @@ private static void WriteUsageMapPointer(byte[] page, int offset, int row, int m private static List ResolveColumns(JetFormatBase format, IReadOnlyList specs, Collation collation) { - // A column's id is its declaration position unless the spec pins one explicitly (ALTER COLUMN burns a - // fresh id at the same position, so ids can be non-contiguous — the row codec reads var-index from the - // descriptor, not from id arithmetic). Variable columns are addressed in ascending column-id order. + // A column's id is its declaration position unless the spec pins one explicitly (the catalog tables + // DatabaseCreator writes). Variable columns are addressed in ascending column-id order. int EffectiveId(int i) => specs[i].ColumnId ?? i; var variableRank = new Dictionary(); @@ -547,12 +538,6 @@ int VarTableIndex(int i) => // Booleans live in the null bitmap and occupy no fixed-data bytes, so they don't // advance the fixed offset (matching how the row codec skips them). bool occupiesFixedData = s.IsFixedLength && s.Type != JetDataType.Boolean; - // The documented flag bits LibRed doesn't drive from the spec (updatable, GUID-autonumber, hyperlink, - // compressed-Unicode, calculated) are carried through a rebuild by reading them off the original - // descriptor. A fresh column (no raw) is updatable with the rest clear — the CREATE default. - bool hasRaw = s.RawDescriptor is { } r && r.Length == format.ColumnDescriptorSize; - byte rawFlags = hasRaw ? s.RawDescriptor![format.ColumnFlagsOffset] : JetFormatBase.ColumnFlagUpdatable; - byte rawExt = hasRaw ? s.RawDescriptor![format.ColumnExtendedFlagsOffset] : (byte)0; columns.Add(new ColumnDef { Name = s.Name, @@ -565,13 +550,8 @@ int VarTableIndex(int i) => VariableTableIndex = VarTableIndex(i), IsFixedLength = s.IsFixedLength, IsAutoNumber = s.IsAutoNumber, - IsUpdatable = (rawFlags & JetFormatBase.ColumnFlagUpdatable) != 0, - IsGuidAutoNumber = (rawFlags & JetFormatBase.ColumnFlagGuidAutoNumber) != 0, - IsHyperlink = (rawFlags & JetFormatBase.ColumnFlagHyperlink) != 0, - SupportsCompressedUnicode = s.SupportsCompressedUnicode - || (rawExt & JetFormatBase.ColumnExtFlagCompressedUnicode) != 0, - IsCalculated = s.CalculatedExpression is not null - || (rawExt & JetFormatBase.ColumnExtFlagCalculated) != 0, + SupportsCompressedUnicode = s.SupportsCompressedUnicode, + IsCalculated = s.CalculatedExpression is not null, CalculatedExpression = s.CalculatedExpression, CalculatedResultType = s.CalculatedResultType, SystemFlags = s.SystemFlags, @@ -582,7 +562,6 @@ int VarTableIndex(int i) => // Numeric columns carry no collation (their 0x0B/0x0C bytes are precision/scale); every // other column inherits the database's collating order. Collation = s.Type == JetDataType.FixedPoint ? Collation.GeneralLegacy : collation, - RawDescriptor = s.RawDescriptor, }); } return columns; @@ -605,14 +584,7 @@ internal static NotSupportedException ZeroIncrement(string column) => /// ALTER TABLE ADD COLUMN. public static byte[] BuildColumnDescriptor(ColumnDef c, JetFormatBase format) { - // Faithful round-trip: when we have the column's original bytes (a rebuild of a read column that we're - // NOT retyping), start from them and overwrite every field LibRed models, so the only bytes that survive - // untouched are the genuinely reserved/unknown ones — the reserved words 0x03/0x11 and the undocumented - // bits of the two flag bytes. A fresh column (RawDescriptor null — CREATE, ADD COLUMN, or the - // deliberately-retyped ALTER target) builds from zero. - byte[] d = c.RawDescriptor is { } raw && raw.Length == format.ColumnDescriptorSize - ? (byte[])raw.Clone() - : new byte[format.ColumnDescriptorSize]; + byte[] d = new byte[format.ColumnDescriptorSize]; d[format.ColumnTypeOffset] = (byte)c.Type; BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(ColumnRecordMarkerOffset, 2), (ushort)JetFormatBase.TdefRecordMarker); BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnNumberOffset, 2), (ushort)c.ColumnId); @@ -620,31 +592,29 @@ public static byte[] BuildColumnDescriptor(ColumnDef c, JetFormatBase format) // model and DAO-executed SQL — and every user table in every fixture carries it, while only the // engine's own bootstrap tables (MSysObjects and friends) leave it zero, which is what a system // column keeps here. An earlier comment claimed real files store zero; that had been read off the - // system tables alone. On a rebuild the original value survives untouched, because it stops - // tracking 0x05 once an ALTER COLUMN type change burns a new id there (§3.8). - if (c.RawDescriptor is null or { Length: 0 }) - BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnSecondaryNumberOffset, 2), - (ushort)(c.IsEngineColumn ? 0 : c.ColumnId)); + // system tables alone. + BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnSecondaryNumberOffset, 2), + (ushort)(c.IsEngineColumn ? 0 : c.ColumnId)); // Offset 7 = variable-table index (count of variable columns with a smaller id), stored on fixed columns // too. Prefer the precomputed value; fall back to the legacy rule (0 for fixed) when unset (ADD COLUMN). BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnVariableIndexOffset, 2), (ushort)(c.VariableTableIndex >= 0 ? c.VariableTableIndex : (c.IsFixedLength ? 0 : c.VariableIndex))); WriteLocaleUnion(d, c.Type, c.Precision, c.Scale, c.Collation, format); // Compose the flag byte (0x0F) from every bit a user column models, plus the catalog bits a created system - // column asks for; the rest — an existing column's catalog bits — survive from the original. Likewise the - // extended-flag byte (0x10), whose unmodelled bits the complex columns' flat tables set. + // column asks for; likewise the extended-flag byte (0x10), plus the unmodelled bits an attachment's + // value columns ask for. byte flags = (byte)( (c.IsUpdatable ? JetFormatBase.ColumnFlagUpdatable : 0) | (c.IsFixedLength ? JetFormatBase.ColumnFlagFixedLength : 0) | (c.IsAutoNumber ? JetFormatBase.ColumnFlagAutoNumber : 0) | (c.IsGuidAutoNumber ? JetFormatBase.ColumnFlagGuidAutoNumber : 0) | (c.IsHyperlink ? JetFormatBase.ColumnFlagHyperlink : 0)); - d[format.ColumnFlagsOffset] = (byte)((d[format.ColumnFlagsOffset] & ~JetFormatBase.ColumnFlagsDocumented) | flags | c.SystemFlags); + d[format.ColumnFlagsOffset] = (byte)(flags | c.SystemFlags); byte extFlags = (byte)( (c.SupportsCompressedUnicode ? JetFormatBase.ColumnExtFlagCompressedUnicode : 0) | (c.IsCalculated ? JetFormatBase.ColumnExtFlagCalculated : 0)); - d[format.ColumnExtendedFlagsOffset] = (byte)((d[format.ColumnExtendedFlagsOffset] & ~JetFormatBase.ColumnExtFlagsDocumented) | extFlags | c.ExtendedFlags); + d[format.ColumnExtendedFlagsOffset] = (byte)(extFlags | c.ExtendedFlags); BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnFixedOffsetOffset, 2), (ushort)c.FixedOffset); BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnLengthOffset, 2), (ushort)c.Length); @@ -670,8 +640,7 @@ internal static void WriteLocaleUnion( { // A complex (multi-value / attachment) column stores its MSysComplexColumns key here (page-02b // §3.4) — there is nothing to collate, since the values are rows of the flat table and carry their - // own collations. Writing a LANGID over it severs the column from its values, which is what a - // rebuild of such a table used to do. The bytes come from the original descriptor and stay. + // own collations. Writing a LANGID over it severs the column from its values, so the bytes stand. } else if (type == JetDataType.DateTimeExtended) { diff --git a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs index 5420f3f6..3249ebf3 100644 --- a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs +++ b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs @@ -243,19 +243,12 @@ public abstract class JetFormatBase public const byte ColumnFlagGuidAutoNumber = 0x40; /// Column flag: a hyperlink (a Memo column presented as a hyperlink). public const byte ColumnFlagHyperlink = 0x80; - /// Mask of the flag bits a user column's models. The complement — the - /// catalog bits 0x10/0x20 of a system column, and 0x08, never seen set — is preserved from - /// raw. - public const byte ColumnFlagsDocumented = - ColumnFlagFixedLength | ColumnFlagUpdatable | ColumnFlagAutoNumber | ColumnFlagGuidAutoNumber | ColumnFlagHyperlink; // Extended flag byte (0x10). /// Extended flag: the column can store compressed Unicode text (§7). public const byte ColumnExtFlagCompressedUnicode = 0x01; /// Extended flag: a calculated (computed) column (ACE 14+); the 0xC0 pair. public const byte ColumnExtFlagCalculated = 0xC0; - /// Mask of the documented extended-flag bits — the complement is preserved from raw. - public const byte ColumnExtFlagsDocumented = ColumnExtFlagCompressedUnicode | ColumnExtFlagCalculated; // Note: nullability is NOT in the column flag byte (bit 0x02 is set on every column). A NOT NULL column // is marked by a boolean `Required` property in the LvProp blob instead — see PropertyBlob / §11. diff --git a/src/LibRed/LibRed.Core/JetDatabase.cs b/src/LibRed/LibRed.Core/JetDatabase.cs index 3e72a2b5..c9ecbd06 100644 --- a/src/LibRed/LibRed.Core/JetDatabase.cs +++ b/src/LibRed/LibRed.Core/JetDatabase.cs @@ -426,7 +426,7 @@ public bool DropCheckConstraint(string table, string name) } /// Changes a column's declared type — ALTER TABLE … ALTER COLUMN. A variable text/binary length - /// change is an in-place descriptor edit; a storage-type change rebuilds the table (converting values). + /// change is an in-place descriptor edit; a storage-type change is . public void AlterColumn(string table, string column, ColumnSpec newSpec) { new Storage.TableCreator(_channel, Catalog, Collation).AlterColumn(table, column, newSpec); @@ -434,8 +434,8 @@ public void AlterColumn(string table, string column, ColumnSpec newSpec) } /// Full in-place column type change, byte-for-byte like ACE for every shape (fixed/variable columns - /// and targets, fixed↔variable, and indexed targets): TDEF edit + row re-lay preserving the dead old slot + - /// index rebuild. Falls back to the logical rebuild only for a Memo/OLE (long-value) source or target. + /// and targets, fixed↔variable, indexed targets, and a Memo/OLE source or target): TDEF edit + row re-lay + /// preserving the dead old slot + a rebuild of the indexes over the column. public void AlterColumnTypeInPlace(string table, string column, ColumnSpec newSpec) { new Storage.TableCreator(_channel, Catalog, Collation).AlterColumnTypeInPlace(table, column, newSpec); diff --git a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs index 9fae3af1..25fbca79 100644 --- a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs +++ b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs @@ -375,13 +375,12 @@ private int ReadColumns(PageBuffer buffer, JetFormatBase format, int columnBlock Length = d.Length, FixedOffset = d.FixedOffset, VariableIndex = isFixed ? -1 : d.VariableIndex, - // Byte 7 is stored on fixed columns too (the running count of preceding variable columns); keep - // it so a faithful rebuild re-emits the exact value instead of clobbering fixed columns to 0. + // Byte 7 is stored on fixed columns too (the running count of preceding variable columns). VariableTableIndex = d.VariableIndex, IsFixedLength = isFixed, IsAutoNumber = (d.Flags & JetFormatBase.ColumnFlagAutoNumber) != 0, // Every documented flag bit is modelled (0x0F: updatable/GUID-autonumber/hyperlink; 0x10: - // compressed-Unicode / calculated) so it round-trips explicitly, not via RawDescriptor. + // compressed-Unicode / calculated). IsUpdatable = (d.Flags & JetFormatBase.ColumnFlagUpdatable) != 0, IsGuidAutoNumber = (d.Flags & JetFormatBase.ColumnFlagGuidAutoNumber) != 0, IsHyperlink = (d.Flags & JetFormatBase.ColumnFlagHyperlink) != 0, @@ -390,7 +389,7 @@ private int ReadColumns(PageBuffer buffer, JetFormatBase format, int columnBlock Precision = d.Precision, Scale = d.Scale, Collation = d.Collation, - // Keep the original 25 bytes so a rewrite preserves fields we don't model (faithful round-trip). + // The descriptor's bytes as read, fields LibRed does not model included. RawDescriptor = buffer.Slice(columnBlock + i * format.ColumnDescriptorSize, format.ColumnDescriptorSize).ToArray(), }); } diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 3b2b721e..b2090468 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -14,14 +14,13 @@ namespace LibRed.Storage; /// Creates and alters tables in an existing database. allocates and writes the TDEF /// page, its indexes' B-tree roots and an owned-pages usage map, then records the table in MSysObjects so /// the catalog finds it. The rest of the class is the incremental DDL EF issues one statement at a time — -/// ADD/DROP/ALTER/RENAME for columns, indexes, relationships and CHECK constraints — each a surgical edit -/// of the existing TDEF rather than a rebuild, so unmodelled descriptor bytes survive. +/// ADD/DROP/ALTER/RENAME for columns, indexes, relationships and CHECK constraints — each an edit of the +/// existing TDEF in place, as ACE makes it, so unmodelled descriptor bytes survive. /// /// The database's collating order, from page 0. Every non-numeric column this /// class writes inherits it, which is what decides how that column's index keys are encoded, so it is -/// required rather than defaulted. It used to fall back to General-Legacy for "callers that don't create -/// columns" — but ALTER COLUMN creates them, by rebuilding the table, and passed nothing: on a General (v1) -/// database that silently produced v0 columns whose keys the rest of the file does not sort by. +/// required rather than defaulted: a General-Legacy fallback silently writes v0 columns into a General (v1) +/// database, whose keys the rest of the file does not sort by. /// The database file. /// The catalog to read and keep current. public sealed class TableCreator(PageChannel channel, JetCatalog catalog, Collation collation) @@ -138,9 +137,8 @@ public void Create( int usageMapPage = _allocator.Allocate(); // 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. + // CREATE TABLE, but a spec can carry an explicit id, 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. // A calculated column with a Memo RESULT needs the maps too, and its declared type does not say so: // ACE declares such a column Text with length 0 and reaches the value through a long-value // descriptor, so keying off Type alone leaves it without maps and its result nowhere to go (§3.4a). @@ -718,8 +716,7 @@ private int InsertIndex(TableDef table, string indexName, List<(int Id, bool Asc // block in name order. ACE's order ignores case (verified: a3 goes before IX2, and an FK named fk before // IX2); how it orders punctuation and accented letters is not measured. parts.Stats.Add(new byte[format.RealIndexEntrySize]); - // An index over a complex column carries the complex-column flag — every one Access writes does, which a - // rebuild restoring the index from its IndexDef would otherwise drop. + // An index over a complex column carries the complex-column flag — every one Access writes does. bool complexColumn = slots.Any(s => table.Columns.Any(c => c.ColumnId == s.Id && c.Type == JetDataType.Complex)); parts.DataBlocks.Add(BuildIndexDataBlock( slots, rootPage, newIndexUsageRow, usageMapPage, unique, required, ignoreNulls, complexColumn)); @@ -786,7 +783,7 @@ private void EnsureExistingRowsFitIndex( /// descent is gone. The comparison is still on the encoded key, which /// is Access's uniqueness domain (see ). /// A built index's statistics are set as ACE sets them (verified, for CREATE INDEX, a foreign key's - /// backing index and an ALTER COLUMN's rebuild): the total entry count to the entries it now holds and the + /// backing index and an index an ALTER COLUMN rebuilds): the total entry count to the entries it now holds and the /// unique entry count to its distinct keys — both from the rows present, not from any earlier history. /// private void BackfillIndex(string tableName, string indexName, bool ignoreNulls, bool validateUnique) @@ -2274,8 +2271,8 @@ or JetDataType.FixedPoint !col.IsFixedLength && !newSpec.IsFixedLength && col.Type == newSpec.Type && newSpec.Type is JetDataType.Text or JetDataType.Binary or JetDataType.BigBinary; // A variable text/binary length change is a cheap in-place descriptor edit (below). A storage-type change - // (numeric type, fixed size, fixed↔variable) is a full column rewrite: the byte-faithful in-place edit - // where it applies (all-fixed non-indexed target), else the logical rebuild (AlterColumnTypeInPlace picks). + // (numeric type, fixed size, fixed↔variable, to or from a long value) edits the descriptor in place and + // re-lays every row, rebuilding only the indexes over the column. if (!variableLengthChange) { AlterColumnTypeInPlace(tableName, columnName, newSpec); @@ -2371,8 +2368,8 @@ private bool ColumnIsInRelationship(TableDef table, ColumnDef column) } /// ACE rejects every type/length alteration of a relationship column, on either the - /// referencing or referenced side. Keep this check ahead of all specialized ALTER paths so an - /// in-place descriptor edit cannot bypass the same rule enforced by a logical table rebuild. + /// referencing or referenced side. Keep this check ahead of all specialized ALTER paths so none of + /// them can bypass it. private void EnsureColumnIsNotInRelationship(TableDef table, ColumnDef column) { if (ColumnIsInRelationship(table, column)) @@ -3577,8 +3574,7 @@ private void AddCatalogRow(string name, int tdefPage, props.Add(new PropertyBlob.Property("", PropertyBlob.CheckConstraintsProperty, PropertyBlob.WriteCheckList(checkConstraints))); - // A table owning a complex column is flagged so — the rebuild recreates such tables, and Access marks - // every one it writes. + // A table owning a complex column is flagged so — Access marks every one it writes. new CatalogWriter(_channel, _catalog).AddObjectRow( name, tdefPage, CatalogFormat.ObjectTypeTable, CatalogFormat.ObjectContainerParentId, flags: ownsComplexColumns ? CatalogFormat.ObjectFlagOwnsComplexColumns : 0, props); diff --git a/src/LibRed/docs/format/page-02b-columns.md b/src/LibRed/docs/format/page-02b-columns.md index e6609f86..2109827c 100644 --- a/src/LibRed/docs/format/page-02b-columns.md +++ b/src/LibRed/docs/format/page-02b-columns.md @@ -135,10 +135,10 @@ > **Every flag a user column carries is modelled; the rest ride through raw.** LibRed reads the user-column > bits of `0x0F` and `0x10` into `ColumnDef` (`IsUpdatable`/`IsGuidAutoNumber`/`IsHyperlink`, > `SupportsCompressedUnicode`/`IsCalculated`) and composes them back on write, so they round-trip explicitly. -> It writes the catalog bits `0x10`/`0x20` on the system tables it creates. Everything else is preserved -> verbatim through `ColumnDef.RawDescriptor` on a rewrite: the reserved words at `0x03` and `0x11`, the catalog -> bits of an existing column, and the `0x10` bits it does not model, which the complex columns' flat tables -> set. +> It writes the catalog bits `0x10`/`0x20` on the system tables it creates. Everything else — the reserved +> words at `0x03` and `0x11`, the catalog bits of an existing column, and the `0x10` bits it does not model, +> which the complex columns' flat tables set — survives because every ALTER edits the existing descriptor in +> place, as ACE does, rather than re-emitting it. > **Nullability, defaults and checks are *not* in the descriptor.** The column's *Required* (NOT NULL) > property is **not** encoded anywhere in the 25-byte descriptor — verified: a nullable column and a diff --git a/test/LibRed.Core.AccessTests/AceAlterColumnTests.cs b/test/LibRed.Core.AccessTests/AceAlterColumnTests.cs index ac40ed8b..f6cd5c2f 100644 --- a/test/LibRed.Core.AccessTests/AceAlterColumnTests.cs +++ b/test/LibRed.Core.AccessTests/AceAlterColumnTests.cs @@ -199,7 +199,7 @@ public void Access_enforces_the_relationship_after_a_libred_parent_side_rewrite( // Faithful round-trip: changing ONE column's type must not disturb another column's on-disk descriptor. // ACE authors the table (so the untouched column's bytes are ACE's, not LibRed's defaults); LibRed retypes a // different, same-width column (no layout shift), then the untouched column's 25 descriptor bytes must be - // byte-identical — the RawDescriptor passthrough, not a rebuild-from-model that would stamp defaults. + // byte-identical — the ALTER edits the TDEF in place, never re-emitting a descriptor from the model. [Fact] public void Libred_type_change_preserves_an_untouched_columns_descriptor_bytes() { diff --git a/test/LibRed.Core.Tests/ColumnDescriptorFlagTests.cs b/test/LibRed.Core.Tests/ColumnDescriptorFlagTests.cs index 79bf5f3b..20e1be61 100644 --- a/test/LibRed.Core.Tests/ColumnDescriptorFlagTests.cs +++ b/test/LibRed.Core.Tests/ColumnDescriptorFlagTests.cs @@ -4,47 +4,26 @@ namespace LibRed.Core.Tests; -// Every DOCUMENTED column-descriptor field is modelled and round-trips explicitly (read into ColumnDef, -// written from it). Only the genuinely reserved/unknown bytes ride through RawDescriptor: the reserved words -// at 0x03/0x11 and the undocumented bits of the two flag bytes (0x0F/0x10). This pins that the documented -// flag bits — GUID-autonumber (0x40), hyperlink (0x80), compressed-Unicode (0x10/0x01) and calculated -// (0x10/0xC0) — survive a rebuild rather than being dropped when composed from the model. +// Every DOCUMENTED column-descriptor flag is modelled and composed from ColumnDef when a column is created. public class ColumnDescriptorFlagTests { [Fact] - public void BuildColumnDescriptor_round_trips_documented_flags_and_reserved_bytes() + public void BuildColumnDescriptor_composes_the_documented_flags_from_the_model() { JetFormatBase format = JetFormatBase.FromVersionByte(0x02); // ACE 12 - int size = format.ColumnDescriptorSize; - - // A crafted original: updatable + GUID-autonumber + hyperlink, plus an UNDOCUMENTED bit (0x08) in 0x0F; - // compressed-Unicode + calculated in 0x10; and non-zero RESERVED words at 0x03 and 0x11. - var raw = new byte[size]; - raw[format.ColumnFlagsOffset] = (byte)(ColumnFlags(updatable: true, guid: true, hyperlink: true) | 0x08); - raw[format.ColumnExtendedFlagsOffset] = - JetFormatBase.ColumnExtFlagCompressedUnicode | JetFormatBase.ColumnExtFlagCalculated; - raw[0x03] = 0xAB; raw[0x04] = 0xCD; // reserved word - raw[0x11] = 0xDE; raw[0x12] = 0xAD; raw[0x13] = 0xBE; raw[0x14] = 0xEF; // reserved word var col = new ColumnDef { Name = "H", Type = JetDataType.Memo, Index = 0, ColumnId = 3, Length = 4, IsUpdatable = true, IsGuidAutoNumber = true, IsHyperlink = true, SupportsCompressedUnicode = true, IsCalculated = true, - RawDescriptor = raw, }; byte[] d = TdefBuilder.BuildColumnDescriptor(col, format); - // Both flag bytes reproduce the original exactly — documented bits composed from the model, the - // undocumented 0x08 bit preserved from raw. - Assert.Equal(raw[format.ColumnFlagsOffset], d[format.ColumnFlagsOffset]); - Assert.Equal(raw[format.ColumnExtendedFlagsOffset], d[format.ColumnExtendedFlagsOffset]); - - // Reserved/unknown words survive verbatim. - Assert.Equal(raw[0x03], d[0x03]); Assert.Equal(raw[0x04], d[0x04]); - Assert.Equal(raw[0x11], d[0x11]); Assert.Equal(raw[0x12], d[0x12]); - Assert.Equal(raw[0x13], d[0x13]); Assert.Equal(raw[0x14], d[0x14]); + Assert.Equal(ColumnFlags(updatable: true, guid: true, hyperlink: true), d[format.ColumnFlagsOffset]); + Assert.Equal(JetFormatBase.ColumnExtFlagCompressedUnicode | JetFormatBase.ColumnExtFlagCalculated, + d[format.ColumnExtendedFlagsOffset]); } [Fact] diff --git a/test/LibRed.Core.Tests/ComplexAutoNumberRoundTripTests.cs b/test/LibRed.Core.Tests/ComplexAutoNumberRoundTripTests.cs index a214e955..babb9e44 100644 --- a/test/LibRed.Core.Tests/ComplexAutoNumberRoundTripTests.cs +++ b/test/LibRed.Core.Tests/ComplexAutoNumberRoundTripTests.cs @@ -8,23 +8,19 @@ namespace LibRed.Core.Tests; // The TDEF header's complex-type AutoNumber high-water (0x1C) is a documented, meaningful field (the next id -// for a complex multi-value/attachment column). LibRed now reads it into the model and writes it explicitly -// through TdefBuilder, rather than leaving it to the raw surgery path. It is 0 for every table LibRed creates -// (no complex columns), so this pins the read/write path with a non-zero value directly. +// for a complex multi-value/attachment column). LibRed reads it into the model, and TdefBuilder writes 0 there +// for every table it creates (no complex columns), so the read path is pinned with a non-zero value directly. public class ComplexAutoNumberRoundTripTests { [Fact] - public void Complex_autonumber_high_water_is_written_and_read_back() + public void Complex_autonumber_high_water_is_read_back() { JetFormatBase format = JetFormatBase.FromVersionByte(0x02); // ACE 12 var specs = new[] { new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true) }; - byte[] page = TdefBuilder.Build(format, TableType.User, specs, Collation.GeneralLegacy, complexAutoNumber: 42).Page; - - // Written explicitly at 0x1C… - Assert.Equal(42, BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(format.TdefComplexAutoNumberOffset, 4))); + byte[] page = TdefBuilder.Build(format, TableType.User, specs, Collation.GeneralLegacy).Page; + BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefComplexAutoNumberOffset, 4), 42); - // …and read back into the model. var tdef = new TableDefinitionPage(); tdef.Read(new PageBuffer(page, 0), format); Assert.Equal(42, tdef.ComplexAutoNumber); From ee109912585d5760691d3c443fbaa66ed93aa34c Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 22:27:56 +0800 Subject: [PATCH 65/88] Retype a text length, and several indexes over a column, as ACE does A TEXT(n) to TEXT(m) change edited the descriptor's length and nothing else. ACE has no such path: widening or narrowing, indexed or not, it burns a fresh column id, re-lays every row and rebuilds the indexes over the column, as for any other retype - which the spec already said. The length change now takes that retype, and a value too wide for the new declaration is refused by the re-lay's own width check, with ACE's message. With more than one index over the retyped column, LibRed rebuilt each in its own slot. ACE rebuilds them in logical-block (name) order, the primary key included, allocating their roots in that order, and hands them back the real-index slots they held between them in that order; the logical blocks over them hand their numbers round the same way, which is distinct from the data ordinal once two logical indexes share a real one. A column becoming a Memo gets its map records ahead of the rebuilds' on the usage-map page. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/JetDatabase.cs | 4 +- .../LibRed.Core/Storage/TableCreator.cs | 129 ++++++++---------- src/LibRed/docs/format/data-types.md | 6 +- src/LibRed/docs/format/page-02b-columns.md | 19 ++- .../ColumnRetypeRelayAccessTests.cs | 6 +- .../ColumnRetypeParityAccessTests.cs | 21 ++- 6 files changed, 99 insertions(+), 86 deletions(-) diff --git a/src/LibRed/LibRed.Core/JetDatabase.cs b/src/LibRed/LibRed.Core/JetDatabase.cs index c9ecbd06..7922012b 100644 --- a/src/LibRed/LibRed.Core/JetDatabase.cs +++ b/src/LibRed/LibRed.Core/JetDatabase.cs @@ -425,8 +425,8 @@ public bool DropCheckConstraint(string table, string name) return dropped; } - /// Changes a column's declared type — ALTER TABLE … ALTER COLUMN. A variable text/binary length - /// change is an in-place descriptor edit; a storage-type change is . + /// Changes a column's declared type — ALTER TABLE … ALTER COLUMN. Any type or length change, a text + /// column's length included, is . public void AlterColumn(string table, string column, ColumnSpec newSpec) { new Storage.TableCreator(_channel, Catalog, Collation).AlterColumn(table, column, newSpec); diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index b2090468..062a6c7d 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -5,7 +5,6 @@ using LibRed.Pages; using System.Buffers.Binary; using System.Globalization; -using System.Text; using MapRetirement = ((int Row, int Page) Map, System.Collections.Generic.IReadOnlyList<(int Row, int Page)> Clear, System.Collections.Generic.IReadOnlyList Pages); namespace LibRed.Storage; @@ -2189,12 +2188,11 @@ public bool DropCheckConstraint(string tableName, string checkName) throw new InvalidOperationException($"MSysObjects row for table '{tableName}' (page {tdefPage}) was not found."); } - /// Changes a column's declared type — ALTER TABLE … ALTER COLUMN. A **variable text/binary - /// column's max length** is a descriptor-length edit at ColumnLengthOffset: variable columns store - /// each row's actual length, so widening rewrites no rows, and narrowing only scans them to check they - /// still fit. Every other change — numeric type, a fixed column's size, fixed↔variable — is a full column - /// rewrite, handled in place by (byte-faithful with ACE), Memo/OLE - /// included. Nothing here throws NotSupported for a storage-type change. + /// Changes a column's declared type — ALTER TABLE … ALTER COLUMN. Apart from the counter edits and + /// a re-declaration that changes nothing, every type or length change — a text column's length included, as + /// ACE makes it — is a full column rewrite, handled in place by + /// (byte-faithful with ACE), Memo/OLE included. Nothing here throws NotSupported for a storage-type + /// change. public void AlterColumn(string tableName, string columnName, ColumnSpec newSpec) { TableDef table = _catalog.FindTable(tableName) @@ -2217,8 +2215,8 @@ public void AlterColumn(string tableName, string columnName, ColumnSpec newSpec) - (col.IsFixedLength && col.Type != JetDataType.Boolean ? col.Length : 0) + (newSpec.IsFixedLength && newSpec.Type != JetDataType.Boolean ? newSpec.Length : 0), table.VariableColumnCount + (col.IsFixedLength && !newSpec.IsFixedLength ? 1 : 0), - // A type change burns a fresh column id, so the bitmap can widen by one. - HighWater(table) + (col.Type == newSpec.Type ? 0 : 1), + // A type or length change burns a fresh column id, so the bitmap can widen by one. + HighWater(table) + (col.Type == newSpec.Type && col.Length == newSpec.Length ? 0 : 1), _channel.Format); // A pure reseed of an existing counter — ALTER COLUMN c COUNTER(seed, increment) where c is already an @@ -2267,40 +2265,10 @@ or JetDataType.FixedPoint && (col.Type != JetDataType.FixedPoint || (col.Precision == newSpec.Precision && col.Scale == newSpec.Scale))) return; - bool variableLengthChange = - !col.IsFixedLength && !newSpec.IsFixedLength && col.Type == newSpec.Type && - newSpec.Type is JetDataType.Text or JetDataType.Binary or JetDataType.BigBinary; - // A variable text/binary length change is a cheap in-place descriptor edit (below). A storage-type change - // (numeric type, fixed size, fixed↔variable, to or from a long value) edits the descriptor in place and - // re-lays every row, rebuilding only the indexes over the column. - if (!variableLengthChange) - { - AlterColumnTypeInPlace(tableName, columnName, newSpec); - return; - } - - // Widening needs no row work — a variable column stores each row's actual length. NARROWING does: the - // invariant "no stored value exceeds its column's declared width" is enforced on every insert and - // update, so the statement that changes the declaration has to hold it too. Without this the ALTER - // succeeds and leaves behind exactly the rows Access will not read back that the insert-time check - // exists to prevent. Scanned before the TDEF is touched, so a refusal changes nothing on disk. - if (newSpec.Length < col.Length) - EnsureExistingValuesFit(table, col, newSpec.Length); - - JetFormatBase format = _channel.Format; - TdefParts parts = ParseTdef(table.DefinitionPage); - byte[] cols = parts.Columns; - int descSize = format.ColumnDescriptorSize; - for (int i = 0; i < table.Columns.Count; i++) - { - int entry = i * descSize; - int colId = BinaryPrimitives.ReadUInt16LittleEndian(cols.AsSpan(entry + format.ColumnNumberOffset, 2)); - if (colId != col.ColumnId) continue; - BinaryPrimitives.WriteUInt16LittleEndian(cols.AsSpan(entry + format.ColumnLengthOffset, 2), (ushort)newSpec.Length); - WriteTdef(table.DefinitionPage, parts); - return; - } - throw new InvalidOperationException($"Descriptor for column '{columnName}' (id {col.ColumnId}) was not found."); + // Every other change — numeric type, a fixed size, fixed↔variable, to or from a long value, and a text or + // binary length, widening as much as narrowing — edits the descriptor in place under a fresh id and + // re-lays every row, rebuilding only the indexes over the column. ACE has no cheap length-only path. + AlterColumnTypeInPlace(tableName, columnName, newSpec); } /// Sets or removes one property in the blob's table-owned (empty-owner) block, leaving every @@ -2316,28 +2284,6 @@ private static byte[] ReplaceTableProperty(byte[] blob, string name, string? val return PropertyBlob.Write(props, blob); } - /// Refuses a narrowing ALTER when a stored value would no longer fit, reporting the same way the - /// insert-time width check does. Text declares characters and stores UTF-16, hence the halving. - private void EnsureExistingValuesFit(TableDef table, ColumnDef column, int newLength) - { - bool text = column.Type == JetDataType.Text; - var rows = new Table(_channel, table); - foreach (object?[] values in rows.Rows(rows.DecodeOnly([column.Index]))) - { - int stored = values[column.Index] switch - { - string s => Encoding.Unicode.GetByteCount(s), - byte[] b => b.Length, - _ => 0, - }; - if (stored <= newLength) continue; - throw new InvalidOperationException( - $"The field '{column.Name}' cannot be narrowed to {(text ? newLength / 2 : newLength)} " - + $"{(text ? "characters" : "bytes")}: the table holds a value of " - + $"{(text ? stored / 2 : stored)}."); - } - } - /// The table's fixed-data region: the high-water of where its live columns END, not the sum of /// their lengths. DROP COLUMN is a metadata-only edit that leaves every survivor's fixed offset exactly /// where it was, so a dropped column's bytes stay in the region as dead space — a sum under-counts by @@ -2530,7 +2476,7 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp // Indexes that include the target column must be rebuilt (their keys change type) — captured now. var affectedIndexes = oldDef.Indexes .Where(i => i.Columns.Any(col => col.Column.Index == oldTarget.Index)) - .Select(i => i.Name).ToList(); + .GroupBy(i => i.RealIndexOrdinal).ToDictionary(g => g.Key, g => g.First()); int oldTargetId = oldTarget.ColumnId; // 1. Materialize (id + raw bytes + values) before touching disk; conversion throws here on bad data. @@ -2577,16 +2523,25 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp TdefParts parts = ParseTdef(oldDef.DefinitionPage); int newTargetId = EditTargetDescriptor(parts, oldTarget, newSpec, oldFixedLen, _collation, format); + // A column becoming a long value gets its entry, placed as ADD COLUMN places it, and its two map records + // ahead of the index rebuilds' records on the usage-map page (verified vs ACE). + if (toLongValue) WriteLongValueMaps(PlaceLongValueMaps(parts, newTargetId)); + + // ACE rebuilds the indexes over the target in logical-block (name) order, allocating their roots in that + // order, and hands them back the real-index slots they held in that order too. + List rebuildOrder = parts.Logical + .Select(b => BinaryPrimitives.ReadInt32LittleEndian(b.Info.AsSpan(IndexBlockFormat.InfoDataNumberOffset, 4))) + .Distinct().Where(affectedIndexes.ContainsKey).ToList(); var pending = new List<(string Name, int OldRoot, int NewRoot, bool IgnoreNulls)>(); - foreach (string ixName in affectedIndexes) + foreach (int ordinal in rebuildOrder) { - IndexDef index = oldDef.Indexes.First(i => string.Equals(i.Name, ixName, StringComparison.OrdinalIgnoreCase)); + IndexDef index = affectedIndexes[ordinal]; int newRoot = PrepareIndexRebuild(parts, oldDef, index, oldTargetId, newTargetId); - pending.Add((ixName, index.RootPage, newRoot, index.IgnoreNulls)); + pending.Add((index.Name, index.RootPage, newRoot, index.IgnoreNulls)); } + ReassignRebuiltIndexSlots(parts, rebuildOrder); - // The long-value side: the old column's maps, read before the TDEF loses them, as DROP COLUMN reads them; - // the new column's entry, placed as ADD COLUMN places it. + // The old column's long-value maps, read before the TDEF loses them, as DROP COLUMN reads them. UsageMap? oldMaps = null; var released = new HashSet(); var retire = new List(); @@ -2598,12 +2553,10 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp QueueLongValueMaps(oldDefinition, oldTarget, oldMaps, released, retire); RemoveLongValueMapEntry(parts, oldTargetId); } - LongValueMapPlacement? newMaps = toLongValue ? PlaceLongValueMaps(parts, newTargetId) : null; WriteTdef(oldDef.DefinitionPage, parts); _catalog.Invalidate(); - if (newMaps is { } placed) WriteLongValueMaps(placed); if (oldMaps is not null) { RetireMapRecords(retire, oldMaps, released); @@ -2715,6 +2668,36 @@ private static List ExtractVarChunks(byte[] row, bool hasVar) return chunks; } + /// Hands the rebuilt indexes back the real-index slots they held, in the order they were rebuilt: the + /// lowest slot to the first rebuilt, and so on, their stats and data blocks moving together, and every logical + /// block's data ordinal (+0x08) following its index. The logical blocks over them hand their numbers + /// (+0x04) round the same way: pooled, sorted, and given back in logical-block order. An index the ALTER + /// does not touch keeps its slot and its number, so with IX_Z, IX_E, IX_A in slots 1–3 and + /// E unaffected, a retype of the column under IX_Z and IX_A leaves IX_A, + /// IX_E, IX_Z (all verified vs ACE, including logical indexes sharing one real index — the primary + /// key moves like any other index). Only plain indexes and the primary key get here: a relationship's column + /// cannot be retyped. + private static void ReassignRebuiltIndexSlots(TdefParts parts, List rebuildOrder) + { + List slots = [.. rebuildOrder.Order()]; + byte[][] stats = [.. rebuildOrder.Select(o => parts.Stats[o])]; + byte[][] blocks = [.. rebuildOrder.Select(o => parts.DataBlocks[o])]; + for (int k = 0; k < slots.Count; k++) + { + parts.Stats[slots[k]] = stats[k]; + parts.DataBlocks[slots[k]] = blocks[k]; + } + List moved = [.. parts.Logical.Select(b => b.Info).Where(info => + rebuildOrder.Contains(BinaryPrimitives.ReadInt32LittleEndian(info.AsSpan(IndexBlockFormat.InfoDataNumberOffset, 4))))]; + List numbers = [.. moved.Select(info => BinaryPrimitives.ReadInt32LittleEndian(info.AsSpan(IndexBlockFormat.InfoNumberOffset, 4))).Order()]; + for (int i = 0; i < moved.Count; i++) + { + Span ordinal = moved[i].AsSpan(IndexBlockFormat.InfoDataNumberOffset, 4); + BinaryPrimitives.WriteInt32LittleEndian(ordinal, slots[rebuildOrder.IndexOf(BinaryPrimitives.ReadInt32LittleEndian(ordinal))]); + BinaryPrimitives.WriteInt32LittleEndian(moved[i].AsSpan(IndexBlockFormat.InfoNumberOffset, 4), numbers[i]); + } + } + /// Prepares one index rebuild over a just-modified column, matching ACE's reconstruction: allocate a /// fresh empty root leaf (appended — the old root is left orphaned) and extend/recycle the owned usage map to /// track it, then re-point the index-data block within to the new root with the diff --git a/src/LibRed/docs/format/data-types.md b/src/LibRed/docs/format/data-types.md index 1faab1f0..29ebe934 100644 --- a/src/LibRed/docs/format/data-types.md +++ b/src/LibRed/docs/format/data-types.md @@ -99,7 +99,7 @@ to add."* The permitted magnitude follows the standard rule: `p` total digits wi most `p − s` before it, and `DECIMAL(18,4)` accepts up to `99999999999999.9999` and refuses `10^14`. The enforcement covers **every path that can put a value in the column** — `INSERT`, `UPDATE`, -`INSERT … SELECT`, and an `ALTER COLUMN` that *narrows* the declaration over rows already stored. That last one +`INSERT … SELECT`, and an `ALTER COLUMN` that *narrows* or retypes the declaration over rows already stored. That last one is what makes it an invariant over the whole column rather than a filter on one statement: ACE will not shrink a declaration to something its existing data would violate. @@ -283,7 +283,9 @@ Points verified against ACE that aren't obvious from that page: over-long case instead of refusing it. The width check therefore belongs on the encode path for fixed columns (`JetTypeCodec.EnsureFitsFixedWidth`, before padding) and on the shared row-assembly path for variable ones (`RowEncoder.AssembleRow`, so the ALTER re-lay passes it too). - **Narrowing an existing column is checked against its rows.** `ALTER TABLE … ALTER COLUMN c TEXT(5)` on a - column holding wider values is refused rather than leaving rows that violate the declaration. + column holding wider values is refused rather than leaving rows that violate the declaration — and so is a + retype whose converted values are too wide: a Memo or a `LONG` to `TEXT(1)`, a `LONG` to `BINARY(1)`. ACE + refuses every one with *"The field is too small to accept the amount of data you attempted to add."* - `DECIMAL(p,s)` / `NUMERIC(p,s)` use precision `1..28` and scale `0..p`, and these are **ACE's own bounds, refused at DDL with two distinct messages**: *"Invalid precision for decimal data type."* for `(0)`, `(0,0)` and `(29)`, *"Invalid scale for decimal data type."* for `(5,7)`. `(1,0)` and `(28,28)` are both accepted, so diff --git a/src/LibRed/docs/format/page-02b-columns.md b/src/LibRed/docs/format/page-02b-columns.md index 2109827c..89a48379 100644 --- a/src/LibRed/docs/format/page-02b-columns.md +++ b/src/LibRed/docs/format/page-02b-columns.md @@ -397,8 +397,9 @@ null bitmap is keyed by **id** and not by position (§5). > anyway, since `Required` is applied separately and the in-place edit keeps the column's own. > > Because the edit is in place, everything the ALTER does not touch stays where it was — every other column's -> descriptor and id, and every index, the primary key included, in its place among the logical and index-data -> blocks (verified). +> descriptor and id, and every index not over the column, the primary key included, in its place among the +> logical and index-data blocks (verified). The indexes over the column keep the slots they held between them, +> but not necessarily their own (below). **Target column descriptor (§3.4)** — the *only* descriptor that changes; all others stay byte-identical: @@ -431,7 +432,8 @@ had at those offsets (the start of its variable data, on a row with some), not z COLUMN (verified, both directions and a Memo re-declared as Memo). A column becoming Memo/OLE gets its §3.3.2 entry, and its owned and free map records appended to the table's usage-map page; each converted value is then stored as an insert stores it — inline (a memo compressed) up to 64 bytes, else on an LVAL page — and the row -carries its descriptor as the appended chunk. A column ceasing to be one has its entry removed and its map +carries its descriptor as the appended chunk. Those two map records go onto the usage-map page ahead of the +records an index rebuild appends there, when the column is indexed. A column ceasing to be one has its entry removed and its map records retired as a dropped column's are; its LVAL pages go back to the global free map with their bytes untouched, and each row keeps the old descriptor as its dead chunk. @@ -447,6 +449,17 @@ index (its keys change type). Verified: - **Recycle the owned-pages usage-map row** the way ACE does — the append/move/tombstone dance, and the stale bytes it deliberately leaves behind, are [page-05 §9](page-05-usage-maps.md). - **Back-fill** the new B-tree with new-type keys (one `AddEntry` per row). +- **Several indexes over the column are rebuilt in logical-block (name) order** — the primary key like any + other — each taking its fresh root in that order, and they are handed back the real-index slots they held + between them in that order: the lowest slot to the first rebuilt, and so on, stats and data block together, + every logical block's data ordinal (`+0x08`) following its index. The logical blocks over those indexes hand + their numbers (`+0x04`, a numbering of the logical blocks, distinct from the data ordinal once two share a real + index) round the same way: pooled, sorted, and given back in logical-block order. An index not over the column + keeps its slot and its number even when it sits between them. With `PK_I`, `IX_Z(B)`, `IX_E(E)`, `IX_A(B, ID)` + in slots 0–3, a retype of `B` leaves `PK_I`, `IX_A`, `IX_E`, `IX_Z`, and a retype of `ID` leaves `IX_A`, + `IX_Z`, `IX_E`, `PK_I`. With `IX_Y(B)` sharing `IX_Z`'s real index as well, numbered `IX_Z` 1, `IX_Y` 2, + `IX_E` 3, `IX_A` 4, the retype of `B` numbers them `IX_A` 1, `IX_Y` 2, `IX_E` 3, `IX_Z` 4. With one index over + the column nothing moves. The descriptor edit and the index-block re-point are applied to **one** parsed TDEF and written **once**. diff --git a/test/LibRed.Core.AccessTests/ColumnRetypeRelayAccessTests.cs b/test/LibRed.Core.AccessTests/ColumnRetypeRelayAccessTests.cs index be7f7858..c59a7f2a 100644 --- a/test/LibRed.Core.AccessTests/ColumnRetypeRelayAccessTests.cs +++ b/test/LibRed.Core.AccessTests/ColumnRetypeRelayAccessTests.cs @@ -133,8 +133,8 @@ public void A_retype_that_would_overflow_the_new_width_is_refused() finally { TemporaryDatabase.Delete(path); } } - // Narrowing a variable column was a bare descriptor edit that never looked at the rows, leaving values - // wider than the declaration — exactly what the insert-time check exists to prevent. + // Narrowing is a retype like any other, so the re-lay's width check refuses a value wider than the new + // declaration, with ACE's message — rather than leaving exactly what the insert-time check exists to prevent. [Fact] public void Narrowing_a_column_below_its_stored_values_is_refused() { @@ -149,7 +149,7 @@ public void Narrowing_a_column_below_its_stored_values_is_refused() var error = Assert.Throws(() => db.AlterColumn( "T", "V", new ColumnSpec("V", JetDataType.Text, 10, IsFixedLength: false))); output.WriteLine(error.Message); - Assert.Contains("cannot be narrowed", error.Message); + Assert.Contains("too small to accept", error.Message); } finally { TemporaryDatabase.Delete(path); } } diff --git a/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs b/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs index 244af9c7..158b8e02 100644 --- a/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs @@ -7,9 +7,10 @@ namespace LibRed.Engine.Tests; -// ALTER COLUMN retypes a column in place, Memo/OLE included (page-02b-columns): the target's descriptor is edited, -// every row re-laid, and a column becoming or ceasing to be a long value gets or loses its §3.3.2 maps as ADD and -// DROP COLUMN give and take them. ACE and LibRed run the same ALTER on copies of one file and must leave the same +// ALTER COLUMN retypes a column in place, Memo/OLE and a text length included (page-02b-columns): the target's +// descriptor is edited, every row re-laid, the indexes over it rebuilt in name order into the slots they held, and a +// column becoming or ceasing to be a long value gets or loses its §3.3.2 maps as ADD and DROP COLUMN give and take +// them. ACE and LibRed run the same ALTER on copies of one file and must leave the same // file: the same catalog and rows, and the same bytes on every page but MSysObjects' own (its DateUpdate is the // clock) — and, when a Memo is re-declared as a Memo, the dead space below the live records on the table's // usage-map page, where ACE leaves stale copies of the records it retired. @@ -23,6 +24,11 @@ public class ColumnRetypeParityAccessTests [InlineData("M", "ALTER TABLE M ALTER COLUMN M TEXT(255)")] // Memo -> Text: the maps retired, the page released [InlineData("M", "ALTER TABLE M ALTER COLUMN M MEMO")] // Memo re-declared: an id burned, maps replaced [InlineData("T", "ALTER TABLE T ALTER COLUMN X DOUBLE")] // a fixed retype over a NULL: the dead bit and slot + [InlineData("T", "ALTER TABLE T ALTER COLUMN B TEXT(200)")] // a text narrowing: an id burned, every row re-laid + [InlineData("I", "ALTER TABLE I ALTER COLUMN E TEXT(100)")] // a text widening, under one index + [InlineData("I", "ALTER TABLE I ALTER COLUMN B TEXT(20)")] // two indexes whose names run against their slots + [InlineData("I", "ALTER TABLE I ALTER COLUMN ID DOUBLE")] // the primary key and IX_A: IX_A takes slot 0 + [InlineData("I", "ALTER TABLE I ALTER COLUMN B MEMO")] // the two indexes again, to Memo public void A_retype_leaves_the_file_ACE_leaves(string table, string alter) { string basePath = TemporaryDatabase.CopyPath(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "retype-base-"); @@ -40,6 +46,15 @@ public void A_retype_leaves_the_file_ACE_leaves(string table, string alter) Exec(c, "INSERT INTO T VALUES (9, NULL, 9)"); Exec(c, "INSERT INTO M VALUES (9, NULL, 9)"); Exec(c, "INSERT INTO T VALUES (10, 'z', NULL)"); + // Indexes over B in slots 1 and 3, out of name order, with IX_E between them; IX_Y shares IX_Z's. + Exec(c, "CREATE TABLE I (ID LONG CONSTRAINT PK_I PRIMARY KEY, B TEXT(50), E TEXT(50))"); + Exec(c, "CREATE INDEX IX_Z ON I (B)"); + Exec(c, "CREATE INDEX IX_Y ON I (B)"); + Exec(c, "CREATE INDEX IX_E ON I (E)"); + Exec(c, "CREATE UNIQUE INDEX IX_A ON I (B, ID)"); + for (int i = 0; i < Values.Length; i++) + Exec(c, $"INSERT INTO I VALUES ({i + 1}, 'b{Values.Length - i}', 'e{i}')"); + Exec(c, "INSERT INTO I VALUES (9, NULL, NULL)"); } OleDbConnection.ReleaseObjectPool(); File.Copy(acePath, libredPath, overwrite: true); From 36e4572672f8c8c9e661509b4bdc83eac2f18619 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Mon, 28 Sep 2026 23:48:26 +0800 Subject: [PATCH 66/88] Stop dropping and recreating indexes around an ALTER COLUMN The migrations SQL generators wrapped a column's type change, narrowing or NOT NULL in DROP INDEX / CREATE INDEX for every index over it - SQL Server's workaround, since SQL Server refuses to alter an indexed column. Jet does not: its ALTER COLUMN rebuilds the indexes over the column itself, keeping each one's name, columns, order and flags, the primary key included, and a same-type NOT NULL leaves them untouched. A computed-column change needs no index handling either, since Jet cannot usefully index a calculated column. That leaves nothing for GetIndexesToRebuild, DropIndexes, CreateIndexes or the operation list they read, and all of it goes, from the shared generator and LibRed's copy alike. Co-Authored-By: Claude Opus 5.5 --- .../Migrations/JetMigrationsSqlGenerator.cs | 124 +----------------- .../LibRedMigrationsSqlGenerator.cs | 124 +----------------- .../Migrations/MigrationsJetTest.cs | 4 - .../Migrations/MigrationsLibRedTest.cs | 4 - .../Migrations/MigrationsLibRedTest.cs | 4 - 5 files changed, 8 insertions(+), 252 deletions(-) diff --git a/src/EFCore.Jet.Common/Migrations/JetMigrationsSqlGenerator.cs b/src/EFCore.Jet.Common/Migrations/JetMigrationsSqlGenerator.cs index c4347e9e..59c8107c 100644 --- a/src/EFCore.Jet.Common/Migrations/JetMigrationsSqlGenerator.cs +++ b/src/EFCore.Jet.Common/Migrations/JetMigrationsSqlGenerator.cs @@ -31,32 +31,8 @@ public class JetMigrationsSqlGenerator( 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 @@ -184,7 +160,6 @@ protected override void Generate( Dependencies.MigrationsLogger.ColumnOrderIgnoredWarning(operation); } - IEnumerable? indexesToRebuild = null; var column = model?.GetRelationalModel().FindTable(operation.Table, operation.Schema) ?.Columns.FirstOrDefault(c => c.Name == operation.Name); @@ -225,12 +200,11 @@ protected override void Generate( addColumnOperation.AddAnnotations(operation.GetAnnotations()); // TODO: Use a column rebuild instead - indexesToRebuild = GetIndexesToRebuild(column, operation).ToList(); - DropIndexes(indexesToRebuild, builder); + // No index to drop first and recreate after: Jet cannot index a calculated column in any usable + // way (ACE accepts CREATE INDEX on one, then refuses every INSERT into the table). Generate(dropColumnOperation, model, builder, terminate: false); builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); Generate(addColumnOperation, model, builder); - CreateIndexes(indexesToRebuild, builder); builder.EndCommand(); return; @@ -266,12 +240,8 @@ protected override void Generate( || operation is { IsNullable: false, OldColumn.IsNullable: true }; } - if (narrowed) - { - indexesToRebuild = GetIndexesToRebuild(column, operation).ToList(); - DropIndexes(indexesToRebuild, builder); - } - + // No DROP INDEX / CREATE INDEX around the ALTER: Jet's ALTER COLUMN rebuilds the indexes over the column + // itself, keeping each one's name, columns, order and flags, the primary key included. var newAnnotations = operation.GetAnnotations().Where(a => a.Name != JetAnnotationNames.Identity); var oldAnnotations = operation.OldColumn.GetAnnotations().Where(a => a.Name != JetAnnotationNames.Identity); @@ -393,11 +363,6 @@ protected override void Generate( builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); } - if (narrowed) - { - CreateIndexes(indexesToRebuild!, builder); - } - builder.EndCommand(); } @@ -970,87 +935,6 @@ protected virtual void DropDefaultConstraint( .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? diff --git a/src/LibRed/LibRed.EFCore/Migrations/LibRedMigrationsSqlGenerator.cs b/src/LibRed/LibRed.EFCore/Migrations/LibRedMigrationsSqlGenerator.cs index 0d5ed70d..db435402 100644 --- a/src/LibRed/LibRed.EFCore/Migrations/LibRedMigrationsSqlGenerator.cs +++ b/src/LibRed/LibRed.EFCore/Migrations/LibRedMigrationsSqlGenerator.cs @@ -31,32 +31,8 @@ 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 @@ -184,7 +160,6 @@ protected override void Generate( Dependencies.MigrationsLogger.ColumnOrderIgnoredWarning(operation); } - IEnumerable? indexesToRebuild = null; var column = model?.GetRelationalModel().FindTable(operation.Table, operation.Schema) ?.Columns.FirstOrDefault(c => c.Name == operation.Name); @@ -225,12 +200,11 @@ protected override void Generate( addColumnOperation.AddAnnotations(operation.GetAnnotations()); // TODO: Use a column rebuild instead - indexesToRebuild = GetIndexesToRebuild(column, operation).ToList(); - DropIndexes(indexesToRebuild, builder); + // No index to drop first and recreate after: Jet cannot index a calculated column in any usable + // way (ACE accepts CREATE INDEX on one, then refuses every INSERT into the table). Generate(dropColumnOperation, model, builder, terminate: false); builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); Generate(addColumnOperation, model, builder); - CreateIndexes(indexesToRebuild, builder); builder.EndCommand(); return; @@ -266,12 +240,8 @@ protected override void Generate( || operation is { IsNullable: false, OldColumn.IsNullable: true }; } - if (narrowed) - { - indexesToRebuild = GetIndexesToRebuild(column, operation).ToList(); - DropIndexes(indexesToRebuild, builder); - } - + // No DROP INDEX / CREATE INDEX around the ALTER: Jet's ALTER COLUMN rebuilds the indexes over the column + // itself, keeping each one's name, columns, order and flags, the primary key included. var newAnnotations = operation.GetAnnotations().Where(a => a.Name != JetAnnotationNames.Identity); var oldAnnotations = operation.OldColumn.GetAnnotations().Where(a => a.Name != JetAnnotationNames.Identity); @@ -393,11 +363,6 @@ protected override void Generate( builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); } - if (narrowed) - { - CreateIndexes(indexesToRebuild!, builder); - } - builder.EndCommand(); } @@ -999,87 +964,6 @@ protected virtual void DropDefaultConstraint( .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? diff --git a/test/EFCore.Jet.FunctionalTests/Migrations/MigrationsJetTest.cs b/test/EFCore.Jet.FunctionalTests/Migrations/MigrationsJetTest.cs index 0368448e..21abd50a 100644 --- a/test/EFCore.Jet.FunctionalTests/Migrations/MigrationsJetTest.cs +++ b/test/EFCore.Jet.FunctionalTests/Migrations/MigrationsJetTest.cs @@ -824,11 +824,9 @@ public override async Task Alter_column_make_required_with_index() AssertSql( """ -DROP INDEX `IX_People_SomeColumn` ON `People`; ALTER TABLE `People` ALTER COLUMN `SomeColumn` DROP DEFAULT; UPDATE `People` SET `SomeColumn` = '' WHERE `SomeColumn` IS NULL; ALTER TABLE `People` ALTER COLUMN `SomeColumn` varchar(255) NOT NULL DEFAULT ''; -CREATE INDEX `IX_People_SomeColumn` ON `People` (`SomeColumn`); """); } @@ -839,11 +837,9 @@ public override async Task Alter_column_make_required_with_composite_index() AssertSql( """ -DROP INDEX `IX_People_FirstName_LastName` ON `People`; ALTER TABLE `People` ALTER COLUMN `FirstName` DROP DEFAULT; UPDATE `People` SET `FirstName` = '' WHERE `FirstName` IS NULL; ALTER TABLE `People` ALTER COLUMN `FirstName` varchar(255) NOT NULL DEFAULT ''; -CREATE INDEX `IX_People_FirstName_LastName` ON `People` (`FirstName`, `LastName`); """); } diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Migrations/MigrationsLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Migrations/MigrationsLibRedTest.cs index 50eee754..4bc084cd 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Migrations/MigrationsLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Migrations/MigrationsLibRedTest.cs @@ -830,11 +830,9 @@ public override async Task Alter_column_make_required_with_index() AssertSql( """ -DROP INDEX `IX_People_SomeColumn` ON `People`; ALTER TABLE `People` ALTER COLUMN `SomeColumn` DROP DEFAULT; UPDATE `People` SET `SomeColumn` = '' WHERE `SomeColumn` IS NULL; ALTER TABLE `People` ALTER COLUMN `SomeColumn` varchar(255) NOT NULL DEFAULT ''; -CREATE INDEX `IX_People_SomeColumn` ON `People` (`SomeColumn`); """); } @@ -845,11 +843,9 @@ public override async Task Alter_column_make_required_with_composite_index() AssertSql( """ -DROP INDEX `IX_People_FirstName_LastName` ON `People`; ALTER TABLE `People` ALTER COLUMN `FirstName` DROP DEFAULT; UPDATE `People` SET `FirstName` = '' WHERE `FirstName` IS NULL; ALTER TABLE `People` ALTER COLUMN `FirstName` varchar(255) NOT NULL DEFAULT ''; -CREATE INDEX `IX_People_FirstName_LastName` ON `People` (`FirstName`, `LastName`); """); } diff --git a/test/EFCore.LibRed.FunctionalTests/Migrations/MigrationsLibRedTest.cs b/test/EFCore.LibRed.FunctionalTests/Migrations/MigrationsLibRedTest.cs index 4f0f519b..d0a0ff4d 100644 --- a/test/EFCore.LibRed.FunctionalTests/Migrations/MigrationsLibRedTest.cs +++ b/test/EFCore.LibRed.FunctionalTests/Migrations/MigrationsLibRedTest.cs @@ -831,11 +831,9 @@ public override async Task Alter_column_make_required_with_index() AssertSql( """ -DROP INDEX `IX_People_SomeColumn` ON `People`; ALTER TABLE `People` ALTER COLUMN `SomeColumn` DROP DEFAULT; UPDATE `People` SET `SomeColumn` = '' WHERE `SomeColumn` IS NULL; ALTER TABLE `People` ALTER COLUMN `SomeColumn` varchar(255) NOT NULL DEFAULT ''; -CREATE INDEX `IX_People_SomeColumn` ON `People` (`SomeColumn`); """); } @@ -846,11 +844,9 @@ public override async Task Alter_column_make_required_with_composite_index() AssertSql( """ -DROP INDEX `IX_People_FirstName_LastName` ON `People`; ALTER TABLE `People` ALTER COLUMN `FirstName` DROP DEFAULT; UPDATE `People` SET `FirstName` = '' WHERE `FirstName` IS NULL; ALTER TABLE `People` ALTER COLUMN `FirstName` varchar(255) NOT NULL DEFAULT ''; -CREATE INDEX `IX_People_FirstName_LastName` ON `People` (`FirstName`, `LastName`); """); } From ed54ac5fb7f06ed750138535137e4f3aa0fc1bac Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Tue, 29 Sep 2026 00:08:08 +0800 Subject: [PATCH 67/88] Skip the reserved-word names an ACE older than Access 2311 refuses OLE DB runs ACE in ANSI-92 mode, and before Access version 2311 (build 16.0.17029) ACE refused an ANSI-92 reserved word that Access's own SQL does not use as a name - even qualified, K.Except or K!Except - with reserved error -1001, which has no message. 2311 fixed it, so the current engine takes Full, Then, Cross, End, Fetch, Next, Rows, Only, Intersect, Except, Restrict, Temporary and Language like any other name, and LibRed follows it. The ACE 2016 redistributable CI installs and ACE 2010 both predate the fix, so there those cases now skip with that reason; any other refusal still fails. Co-Authored-By: Claude Opus 5.5 --- .../QueryNamesAccessTests.cs | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs b/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs index 06df664a..fec7d2d1 100644 --- a/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs @@ -24,9 +24,19 @@ public class QueryNamesAccessTests(ITestOutputHelper output) : TempDatabaseTest "Drop", "Column", "Insert", "Into", "Values", "Primary", "Key", "Constraint", "Foreign", "References", "Delete", "Update", "Cascade", "Restrict", "Action", "Set", "Default", "No", "Unique", "Clustered", "Identity", "Nonclustered", "Index", "Temporary", "With", "Compression", "Comp", "Disallow", "Ignore", "Check", "View", - "Procedure", "Parameters", "Execute", "Exec", "Asc", "Desc", "True", "False", "Null", "Yes", "Off", + "Procedure", "Parameters", "Execute", "Exec", "Asc", "Desc", "True", "False", "Null", "Yes", "Off", "Language", ]; + // ANSI-92 reserved words Access's own SQL does not use. OLE DB runs ACE in ANSI-92 mode, and ACE before Access + // version 2311 (build 16.0.17029) refuses one as a name even qualified, with reserved error -1001, which has no + // message; 2311 fixed that, so the current engine takes each as any other name. The ACE 2016 redistributable and + // ACE 2010 are both older, and LibRed follows the current engine. + private static readonly HashSet RefusedBefore2311 = new(StringComparer.OrdinalIgnoreCase) + { + "Full", "Then", "Cross", "End", "Fetch", "Next", "Rows", "Only", "Intersect", "Except", "Restrict", "Temporary", + "Language", + }; + // The operator words, which ACE refuses after a bang (as are Select and All). private static readonly HashSet RefusedAfterBang = new(StringComparer.OrdinalIgnoreCase) { @@ -95,7 +105,8 @@ public static TheoryData Queries [Theory] [MemberData(nameof(Queries))] public void A_query_reads_and_names_as_ace_does(string query) => - Matches(query, command => { }, null); + Matches(query, command => { }, null, + refusedBefore2311: RefusedBefore2311.Any(k => query == $"SELECT K.{k} FROM K" || query == $"SELECT K!{k} FROM K")); [Theory] [MemberData(nameof(Declared))] @@ -147,8 +158,10 @@ public void A_stored_query_declaring_a_form_control_round_trips_with_ace() } /// Runs the query through ACE and through LibRed, over a copy of Northwind with tables K and Árú made - /// by ACE, and compares each column's name and type and every value. - private void Matches(string query, Action bind, IReadOnlyDictionary? parameters) + /// by ACE, and compares each column's name and type and every value. marks a + /// query an ACE older than version 2311 refuses (), which is skipped there. + private void Matches(string query, Action bind, IReadOnlyDictionary? parameters, + bool refusedBefore2311 = false) { string path = Copy(); try @@ -165,7 +178,15 @@ private void Matches(string query, Action bind, IReadOnlyDictionar using OleDbCommand command = connection.CreateCommand(); command.CommandText = query; bind(command); - using OleDbDataReader reader = command.ExecuteReader(); + OleDbDataReader executed; + try { executed = command.ExecuteReader(); } + catch (OleDbException) when (refusedBefore2311) + { + Assert.Skip("This ACE predates Access version 2311 and refuses an unused ANSI-92 reserved word as a name " + + "(reserved error -1001)."); + throw; + } + using OleDbDataReader reader = executed; var columns = Enumerable.Range(0, reader.FieldCount).Select(i => $"{reader.GetName(i)}:{reader.GetFieldType(i).Name}").ToList(); var rows = new List(); while (reader.Read()) From 429c075ddc5ce94c0937e3f2f6ed0aea60a4af85 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Tue, 29 Sep 2026 18:29:06 +0800 Subject: [PATCH 68/88] Add Access's Nz, with the results Access gives Nz is the Access application's, not the expression service's - ACE's OLE DB provider answers "Undefined function" - yet queries written in Access use it everywhere, and whoever wrote them expects what Access returns. Measured against Access itself over the same rows: the result is always a Variant, so Nz(K, 0) is written out as the text "0", ORDER BY Nz(K, 0) sorts 10 before 2, and Nz(K, 0) + 1 still adds. LibRed already models that Variant for CVar, and Nz takes the same path. With one argument a Null gives VBA's Empty, written out as "" but read as 0 by arithmetic, so Nz(Null) + 2 is 2. Co-Authored-By: Claude Opus 5.5 --- .../Execution/ExpressionEvaluator.cs | 29 +++++++- .../LibRed.Engine/Execution/QueryExecutor.cs | 7 +- .../LibRed.Engine/Execution/VbaEmpty.cs | 15 ++++ src/LibRed/README.md | 2 + src/LibRed/docs/functions.md | 16 ++++- test/LibRed.Engine.Tests/NzTests.cs | 69 +++++++++++++++++++ 6 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 src/LibRed/LibRed.Engine/Execution/VbaEmpty.cs create mode 100644 test/LibRed.Engine.Tests/NzTests.cs diff --git a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs index 111505dc..501c4667 100644 --- a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs +++ b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs @@ -225,6 +225,7 @@ private static bool TryNiladicFunction(ColumnReference c, out object? value) "SWITCH" => Switch(f), "NULLIF" => NullIf(f), "COALESCE" => Coalesce(f), + "NZ" => Nz(f), "GREATEST" => Extreme(f, greatest: true), "LEAST" => Extreme(f, greatest: false), "DATEPART" => DatePart(f), @@ -447,6 +448,7 @@ internal static void ValidateArity(string name, int count) // COALESCE(expression [, ...n]). SQL Server insists on two, but one is harmless and the standard's // own grammar allows it, so only an empty list is rejected. "COALESCE" => (1, int.MaxValue), + "NZ" => (1, 2), // GREATEST/LEAST(expression [, ...n]), as SQL Server and PostgreSQL take them: one argument or more. "GREATEST" or "LEAST" => (1, int.MaxValue), @@ -557,6 +559,20 @@ _ when RunningAggregate.Supports(name) => RunningAggregate.IsPair(name) ? (2, 2) return null; } + /// + /// Access's Nz(value [, valueIfNull]): value, or when it is Null valueIfNull, or VBA's + /// when there is none. ACE's expression service has no Nz — it is the Access application's, + /// so it runs in queries opened in Access but not over OLE DB — and it is here for the queries written in Access. + /// + /// + /// In Access the result is a Variant, and LibRed makes it one (QueryExecutor.VarianceOf), with what that brings + /// (verified vs Access): it is written out as text — Nz(K, 0) is "0" and Nz(Null, 5) is + /// "5" — and sorts and groups as its text, so ORDER BY Nz(K, 0) puts 10 before 2; as an operand it + /// keeps its own value, so Nz(K, 0) + 1 adds and Nz(K, 0) > 2 compares as a number. + /// + private object? Nz(FunctionCall f) => + Evaluate(f.Arguments[0]) ?? (f.Arguments.Count == 2 ? Evaluate(f.Arguments[1]) : VbaEmpty.Value); + /// /// GREATEST(a, b, …) and LEAST(a, b, …) — the largest or smallest of the arguments, compared /// as the < and > operators compare. Access/ACE has neither, so like COALESCE they are @@ -2699,6 +2715,7 @@ private static object Add(object left, object right) => /// is one character of text. private static object? NumericOperand(object? v) => v switch { + VbaEmpty => (short)0, string s => TextAsNumber(s), char c => TextAsNumber(c.ToString()), Guid or byte[] => throw new InvalidCastException("Type mismatch: a GUID or binary value is not a number."), @@ -2714,6 +2731,7 @@ private static object Add(object left, object right) => internal static string ConcatText(object v) => v switch { string s => s, + VbaEmpty => "", bool b => b ? "-1" : "0", double d => FloatingText(d, 15), float f => FloatingText(f, 7), @@ -2930,7 +2948,12 @@ v is bool b ? b : Dbl(ConversionNumber(v)) != 0; // Jet's boolean convention (true = -1, false = 0) so a bool matches the numeric column it is stored in. - private static object Numeric(object v) => v is bool b ? (b ? -1 : 0) : v; + private static object Numeric(object v) => v switch + { + bool b => b ? -1 : 0, + VbaEmpty => (short)0, + _ => v, + }; private static decimal Dec(object v) => JetDecimalConverter.ToDecimal(Numeric(v), CultureInfo.InvariantCulture); private static double Dbl(object v) => Convert.ToDouble(Numeric(v), CultureInfo.InvariantCulture); // Narrow to single precision (the cast yields ±Infinity for an out-of-range double rather than throwing). @@ -2943,6 +2966,10 @@ v is bool b ? b private static int Compare(object left, object right, JetTextComparer text) { + // Empty is "" beside text and 0 beside anything else, as VBA compares it. + if (left is VbaEmpty) left = right is string ? "" : (short)0; + if (right is VbaEmpty) right = left is string ? "" : (short)0; + if (IsNumeric(left) && IsNumeric(right)) { // A single-precision operand (a Single column value, a CSNG result, a SUM of singles) compares in diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs index fb881e7a..76d176df 100644 --- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs @@ -1114,8 +1114,8 @@ or BinaryOperator.Imp or BinaryOperator.Like or BinaryOperator.In /// /// What ACE's expression service makes of a value beyond its type (verified vs ACE, over OLE DB, for IIF, SWITCH /// and CHOOSE; CASE, COALESCE, GREATEST and LEAST, which ACE does not have, take IIF's rule). - /// A Variant — CVar(x), a Variant plus a Variant, a Mixed value or text, a negated one, a - /// column or scalar subquery holding them, and a choice whose values all are — keeps its own type through an + /// A Variant — CVar(x), Access's Nz (verified vs Access), a Variant plus a Variant, a + /// Mixed value or text, a negated one, a column or scalar subquery holding them, and a choice whose values all are — keeps its own type through an /// expression and through a derived table, so X + X over CVar(B) AS X still adds. A result, a /// scalar subquery and a set operation write it out as text, as CStr writes it. /// A Mixed value is a choice whose values disagree in kind — text beside anything else, or a Variant @@ -1136,6 +1136,9 @@ private Variance VarianceOf(Expression expression, IReadOnlyList c case FunctionCall { Arguments: [var argument] } function when function.Name.TrimEnd('$').Equals("CVAR", StringComparison.OrdinalIgnoreCase): return argument is LiteralExpression { Value: null } ? Variance.None : Variance.Variant; + // Access's Nz always gives a Variant, whatever it is given (verified vs Access): Nz(K, 0) writes "0". + case FunctionCall function when function.Name.TrimEnd('$').Equals("NZ", StringComparison.OrdinalIgnoreCase): + return Variance.Variant; case ColumnReference reference: return OutputColumn.Find(columns, reference) is { Variant: true } ? Variance.Variant : Variance.None; case ScalarSubquery subquery: diff --git a/src/LibRed/LibRed.Engine/Execution/VbaEmpty.cs b/src/LibRed/LibRed.Engine/Execution/VbaEmpty.cs new file mode 100644 index 00000000..7f2ab15d --- /dev/null +++ b/src/LibRed/LibRed.Engine/Execution/VbaEmpty.cs @@ -0,0 +1,15 @@ +namespace LibRed.Engine.Execution; + +/// +/// VBA's Empty: what Access's one-argument Nz gives for a Null (verified vs Access). It is written out +/// as "", and read as 0 where a number is wanted and as "" where text is — so Nz(Null) + 2 is 2 +/// and Nz(Null) & 'x' is 'x'. Only Nz makes one. +/// +internal sealed class VbaEmpty +{ + public static readonly VbaEmpty Value = new(); + + private VbaEmpty() { } + + public override string ToString() => ""; +} diff --git a/src/LibRed/README.md b/src/LibRed/README.md index 03a8107a..72420f84 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -222,6 +222,8 @@ Treat the number as of its date — an EF Core version bump moves it. - **Standard scalar syntax** ACE lacks: `CASE`, `COALESCE`, `NULLIF`, `GREATEST`/`LEAST` (NULL arguments ignored, as SQL Server and PostgreSQL treat them — extended mode translates `Math.Max`/`Math.Min` to them), and the `VALUES` table value constructor standing in for a query. + - **Access's `Nz`**, which the Access application has and ACE's OLE DB provider does not, with Access's own + results — see [functions.md](docs/functions.md#nz). - **Arguments ACE's own functions do not take**: `LOG(x, base)`, and SQL Server 2022's `LTRIM(x, characters)` / `RTRIM(x, characters)`, where the second argument is a set of characters to strip rather than a substring. diff --git a/src/LibRed/docs/functions.md b/src/LibRed/docs/functions.md index f16c7c8f..1e09f614 100644 --- a/src/LibRed/docs/functions.md +++ b/src/LibRed/docs/functions.md @@ -45,9 +45,10 @@ reference throws "Column not found" — a default is row-blind by design). The e > Service (JES)**, the built-in set the ACE OLE DB provider carries **standalone**, and (2) the **Access > Application Expression Service**, the full VBA runtime available only inside `MSACCESS.EXE`. LibRed targets > the **JES (standalone)** surface — the correct reference for a standalone engine. Functions that live only -> in the application service (`Nz`, `Split`, `Environ`, `CurDir`, `CurrentUser`, the domain aggregates +> in the application service (`Split`, `Environ`, `CurDir`, `CurrentUser`, the domain aggregates > `DCount`/`DLookup`/…) are therefore **correctly absent**, matching the OLE DB provider ("Undefined -> function"), not a gap. +> function"), not a gap. The one exception is **`Nz`**, which queries written in Access use everywhere, so LibRed +> has it as Access has it (below). --- @@ -100,6 +101,15 @@ LibRed's `CASE`, `Coalesce`, `Greatest` and `Least`, which ACE does not have, fo `CVar(Null)` untyped, as a bare Null is, where ACE makes it a Variant and so a union with an arm of them a text column: EFCore.Jet writes `CVar(Null)` for every projected Null, and the union keeps its other arm's types. +#### Nz + +**`Nz(value [, valueIfNull])`** is `value`, or when it is Null `valueIfNull`. It is the Access application's, not the +JES's — the OLE DB provider answers "Undefined function" — so its reference is Access itself (verified vs Access, the +application, over the same rows). Its result is always a **Variant**, with all that brings: `Nz(K, 0)` is written +out as the text `'0'`, `ORDER BY Nz(K, 0)` puts 10 before 2, and `Nz(K, 0) + 1` and `Nz(K, 0) > 2` work as numbers. +With one argument a Null gives VBA's **Empty**, which is written out as `''` and read as 0 by arithmetic and as `''` +by `&`: `Nz(Null) + 2` is 2, `Nz(Null) & 'x'` is `'x'`, `Len(Nz(Null))` is 0 and `Nz(Null) = 0` is True. + ### Math `Abs` `Sgn` `Int` (floor, toward −∞) `Fix` (truncate, toward zero) `Round` (banker's) `Sqr` `Exp` `Log` @@ -322,7 +332,7 @@ behaviour over a grouped query are described in the [README](../README.md). ## Not supported (by design) -- **Access-application-only** (JES-undefined, so correctly absent): `Nz`, `Split`, `CurDir`, `CurrentUser`, +- **Access-application-only** (JES-undefined, so correctly absent): `Split`, `CurDir`, `CurrentUser`, `Environ`, `Randomize`, and the domain aggregates. `Split` also returns a Variant array (no scalar-SQL representation). - **No scalar-SQL form:** `IRR` / `NPV` (array argument); `Array` / `Join` / `CVErr` (VBA-only). diff --git a/test/LibRed.Engine.Tests/NzTests.cs b/test/LibRed.Engine.Tests/NzTests.cs new file mode 100644 index 00000000..b3e599a4 --- /dev/null +++ b/test/LibRed.Engine.Tests/NzTests.cs @@ -0,0 +1,69 @@ +using System.Globalization; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// Access's Nz(value [, valueIfNull]), which the Access application has and ACE's OLE DB provider does not. Every +/// expected value here is what Access itself returned for the same query over the same rows: a Variant, written out as +/// text, sorted and grouped as its text, and kept as its own value where it is an operand; and, with one argument, VBA's +/// Empty for a Null — written out as "", read as 0 by arithmetic. +/// +public class NzTests(NzTests.Database database) : TempDatabaseTest, IClassFixture +{ + private static readonly string[] Setup = + [ + "CREATE TABLE T (Id LONG, K LONG, K2 TEXT(10))", + "INSERT INTO T VALUES (1, 1, 'a')", + "INSERT INTO T VALUES (2, 2, 'b')", + "INSERT INTO T VALUES (7, 10, 'c')", + "INSERT INTO T VALUES (8, NULL, NULL)", + ]; + + public sealed class Database() : SharedDatabase("nz-", Setup); + + private string ById(string expression) + { + var (types, rows) = database.Query($"SELECT Id, {expression} AS r FROM T ORDER BY Id", CultureInfo.InvariantCulture); + return $"{types[1].Name} " + string.Join(" ", rows.Select(row => + $"{row[0]}:{(row[1] is null ? "Null" : Convert.ToString(row[1], CultureInfo.InvariantCulture))}")); + } + + [Theory] + [InlineData("Nz(K, 0)", "String 1:1 2:2 7:10 8:0")] + [InlineData("Nz(K2, '-')", "String 1:a 2:b 7:c 8:-")] + [InlineData("Nz(K, 'none')", "String 1:1 2:2 7:10 8:none")] + [InlineData("Nz(K, 0.5)", "String 1:1 2:2 7:10 8:0.5")] + [InlineData("Nz(Null, 5)", "String 1:5 2:5 7:5 8:5")] + [InlineData("Nz(K)", "String 1:1 2:2 7:10 8:")] + [InlineData("Nz(K2)", "String 1:a 2:b 7:c 8:")] + [InlineData("Nz(Null)", "String 1: 2: 7: 8:")] + [InlineData("Nz(Null, Null)", "String 1:Null 2:Null 7:Null 8:Null")] + public void Nz_is_written_out_as_text(string expression, string expected) => + Assert.Equal(expected, ById(expression)); + + [Theory] + [InlineData("Nz(K, 0) + 1", "Double 1:2 2:3 7:11 8:1")] + [InlineData("Nz(K) + 2", "Double 1:3 2:4 7:12 8:2")] + [InlineData("Nz(K) * 3", "Double 1:3 2:6 7:30 8:0")] + [InlineData("Nz(K2) & 'x'", "String 1:ax 2:bx 7:cx 8:x")] + [InlineData("Len(Nz(K))", "Int32 1:1 2:1 7:2 8:0")] + [InlineData("Nz(K) = 0", "Boolean 1:False 2:False 7:False 8:True")] + [InlineData("Nz(Nz(K))", "String 1:1 2:2 7:10 8:")] + public void As_an_operand_nz_is_its_value_and_empty_is_zero_or_empty_text(string expression, string expected) => + Assert.Equal(expected, ById(expression)); + + [Fact] + public void Nz_sorts_and_groups_as_its_text() => + Assert.Equal([8, 1, 7, 2], database.Query("SELECT Id FROM T ORDER BY Nz(K, 0)", CultureInfo.InvariantCulture) + .Rows.Select(row => (int)row[0]!)); + + [Fact] + public void Nz_compares_as_a_number_beside_one() => + Assert.Equal([7], database.Query("SELECT Id FROM T WHERE Nz(K, 0) > 2 ORDER BY Id", CultureInfo.InvariantCulture) + .Rows.Select(row => (int)row[0]!)); + + [Fact] + public void Nz_takes_one_or_two_arguments() => + Assert.Throws(() => database.Query("SELECT Nz(K, 0, 1) FROM T", CultureInfo.InvariantCulture)); +} From aa94b9151fc7c258ad82e7e59eb4ef8fe388100d Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Tue, 29 Sep 2026 18:31:06 +0800 Subject: [PATCH 69/88] =?UTF-8?q?Add=20TOP=20n=20WITH=20TIES=20and=20FETCH?= =?UTF-8?q?=20=E2=80=A6=20WITH=20TIES?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WITH TIES returns the n rows and every further row whose ORDER BY keys equal the last one's, PERCENT included. It is the rule ACE's own TOP always follows - a plain TOP 2 there returns every row tied at the boundary, which LibRed's exact-n TOP deliberately does not - so WITH TIES chooses the rows ACE chooses (checked against ACE's plain TOP as a set, since ACE orders tied rows unstably). The standard's FETCH … ROWS WITH TIES takes the same meaning. The node that orders the rows makes the cut, since only it holds their keys: the sort, which then stays above the joins, or the aggregate over a grouped query. It needs an ORDER BY, as in SQL Server, and is refused beside DISTINCT, whose rows collapse above the sort. A view refuses it: a stored TOP has no ties flag, and ACE reads one back with ties where LibRed reads it without. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Engine/Execution/QueryExecutor.cs | 31 +- .../Execution/StatementExecutor.cs | 2 +- src/LibRed/LibRed.Engine/Plan/PlanNodes.cs | 14 +- .../LibRed.Engine/Planning/QueryPlanner.cs | 30 +- src/LibRed/LibRed.Sql/Ast/Statements.cs | 11 +- src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 | 12 +- .../Grammar/Generated/AccessSqlParser.cs | 1231 +++++++++-------- src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs | 16 +- src/LibRed/README.md | 4 + .../TopWithTiesAccessTests.cs | 70 + test/LibRed.Engine.Tests/TopWithTiesTests.cs | 93 ++ 11 files changed, 909 insertions(+), 605 deletions(-) create mode 100644 test/LibRed.Engine.AccessTests/TopWithTiesAccessTests.cs create mode 100644 test/LibRed.Engine.Tests/TopWithTiesTests.cs diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs index 76d176df..0d9f5e18 100644 --- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs @@ -745,7 +745,7 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) int? bound = sort.Limit is { } lim && !_describing ? Convert.ToInt32(Eval([], [], outer).Evaluate(lim), System.Globalization.CultureInfo.InvariantCulture) : null; - return (columns, SortRows(sort.Keys, columns, outer, rows, bound)); + return (columns, SortRows(sort.Keys, columns, outer, rows, bound, _describing ? null : sort.Ties)); } case ProjectNode project: @@ -1939,7 +1939,8 @@ public int GetHashCode(object?[] a) IReadOnlyList columns, EvalScope? outer, IEnumerable rows, - int? bound = null) + int? bound = null, + TieCut? ties = null) { // Rows paired with their evaluated keys and their input position. The position makes the ordering TOTAL, // which is what lets the bounded path below be stable without relying on a stable algorithm. @@ -1975,6 +1976,8 @@ public int GetHashCode(object?[] a) { decorated.RemoveRange(take, decorated.Count - take); } + if (ties is not null) + decorated = CutWithTies(decorated, x => x.Keys, keys, ties, outer); return decorated.Select(x => x.Row).ToList(); @@ -1998,6 +2001,26 @@ static void Trim( } } + /// What a WITH TIES cut keeps of : the offset skipped, the count taken — as TOP n + /// PERCENT takes one, ceil(rows × n / 100), when it is a percentage — and then every further row whose ORDER BY + /// keys equal the last kept row's (ACE's own TOP keeps them all, verified). + private List CutWithTies( + List sorted, Func keysOf, IReadOnlyList keys, TieCut cut, EvalScope? outer) + { + // As in LimitNode: the counts are literal/parameter/arithmetic, so an empty row scope suffices. + ExpressionEvaluator counts = Eval([], [], outer); + int Count(Expression e) => Convert.ToInt32(counts.Evaluate(e), System.Globalization.CultureInfo.InvariantCulture); + int n = Count(cut.Count); + int take = cut.Percent ? (int)(((long)sorted.Count * n + 99) / 100) : n; + int skip = cut.Offset is { } offset ? Math.Max(0, Count(offset)) : 0; + if (take <= 0 || skip >= sorted.Count) + return []; + int end = (int)Math.Min(sorted.Count, (long)skip + take); + while (end < sorted.Count && CompareEvaluatedKeys(keys, keysOf(sorted[end - 1]), keysOf(sorted[end]), TextComparer) == 0) + end++; + return sorted.GetRange(skip, end - skip); + } + /// Compares two rows' already-evaluated ORDER BY key values, honouring each key's direction, with /// text in 's collation. private static int CompareEvaluatedKeys( @@ -2419,6 +2442,10 @@ GroupState NewGroupState(object?[]? firstRow) return 0; }); + // WITH TIES is cut here, where the groups' ORDER BY keys still are (it always has an ORDER BY). + if (node.Ties is { } ties) + outRows = CutWithTies(outRows, x => x.SortKeys, node.OrderBy, ties, outer); + return (outColumns, outRows.Select(x => x.Row)); } diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index a84c54b6..3ccfde26 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -1317,7 +1317,7 @@ void WalkGroup(JoinTable j, JoinTable group) { if (sq.Query is not SelectStatement { - From: NamedTable or JoinTable, GroupBy.Count: 0, Having: null, Distinct: false, Into: null, + From: NamedTable or JoinTable, GroupBy.Count: 0, Having: null, Distinct: false, Into: null, WithTies: false, } select || select.Projection.Any(item => item.Value is StarExpression or QualifiedStarExpression && item.Alias is not null)) return null; diff --git a/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs b/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs index 7dce84bc..fc8dd1ac 100644 --- a/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs +++ b/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs @@ -112,11 +112,20 @@ public sealed record AggregateNode( IReadOnlyList Projection, Expression? Having, IReadOnlyList OrderBy, - IReadOnlyList? Windows = null) : PlanNode + IReadOnlyList? Windows = null, + TieCut? Ties = null) : PlanNode { public override IReadOnlyList Children => [Input]; } +/// +/// TOP n [PERCENT] WITH TIES (or [OFFSET m ROWS] FETCH … WITH TIES), cut by the node that orders the +/// rows, since only it holds their ORDER BY keys: skip , take rows — +/// a percentage of them all, ceil, when — and then every further row whose keys equal the +/// last one's. It takes the place of a . +/// +public sealed record TieCut(Expression Count, bool Percent, Expression? Offset); + /// One window function and the name of the column publishes its value under. /// The planner mints the name and rewrites the call in the projection into a reference to it. public sealed record WindowOutput(string Name, WindowFunction Function); @@ -146,7 +155,8 @@ public sealed record WindowNode(PlanNode Input, IReadOnlyList Outp /// when nothing between the two changes the row count; the still applies the count itself, /// so this is purely a way to avoid ordering rows that cannot survive it. /// -public sealed record SortNode(PlanNode Input, IReadOnlyList Keys, Expression? Limit = null) : PlanNode +/// A WITH TIES cut this sort makes itself, in place of a . +public sealed record SortNode(PlanNode Input, IReadOnlyList Keys, Expression? Limit = null, TieCut? Ties = null) : PlanNode { public override IReadOnlyList Children => [Input]; } diff --git a/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs b/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs index 9b2204b7..b2879f27 100644 --- a/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs +++ b/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs @@ -25,7 +25,7 @@ public static PlanNode Plan(BoundStatement bound) // rows in the wrong order (measured against ACE). SetOperationStatement set => PageAndSort( new SetOperationNode(PlanStatement(set.Left), PlanStatement(set.Right), set.Operator), - OrderByPositions(set.OrderBy ?? [], _ => null), set.Top, set.Offset), + OrderByPositions(set.OrderBy ?? [], _ => null), set.Top, set.Offset, set.WithTies), ValuesStatement values => new ValuesNode(values.Rows), _ => throw new NotImplementedException( $"Planning for {statement.GetType().Name} is not yet implemented."), @@ -35,8 +35,11 @@ public static PlanNode Plan(BoundStatement bound) /// paging for the same reason does — only that many rows can survive it — except /// under OFFSET, where the skipped rows must be produced before they can be discarded. private static PlanNode PageAndSort( - PlanNode node, List orderBy, Expression? top, Expression? offset) + PlanNode node, List orderBy, Expression? top, Expression? offset, bool withTies) { + if (withTies) + return new SortNode(node, RequireTieOrder(orderBy), Ties: new TieCut(top!, Percent: false, offset)); + if (orderBy.Count > 0) { Expression? bound = top is not null && offset is null ? top : null; @@ -46,6 +49,11 @@ private static PlanNode PageAndSort( return top is null && offset is null ? node : new LimitNode(node, top, Offset: offset); } + /// A WITH TIES cut ties rows by their ORDER BY keys, so it needs some — SQL Server's rule too. + private static IReadOnlyList RequireTieOrder(IReadOnlyList orderBy) => orderBy.Count > 0 + ? orderBy + : throw new InvalidOperationException("TOP … WITH TIES needs an ORDER BY: the ties are rows with equal ORDER BY keys."); + /// Plans a SELECT statement directly (used for subqueries). public static PlanNode PlanSelect(SelectStatement select) { @@ -86,10 +94,24 @@ public static PlanNode PlanSelect(SelectStatement select) if (windows.Count > 0 && !aggregate) node = new WindowNode(node, windows); + // WITH TIES is cut by whatever orders the rows — the only node that holds their keys — so it replaces the + // LimitNode. DISTINCT collapses rows above the sort, where the cut would already have counted duplicates. + TieCut? ties = null; + if (select.WithTies) + { + if (select.Distinct || select.DistinctRow) + throw new NotSupportedException("TOP … WITH TIES cannot be combined with DISTINCT or DISTINCTROW."); + RequireTieOrder(select.OrderBy); + ties = new TieCut(select.Top!, select.TopPercent, select.Offset); + } + if (aggregate) // The aggregate node owns ORDER BY: its keys are evaluated in the group scope (so they can // reference grouping expressions / aggregates), not over the already-projected output. - node = new AggregateNode(node, select.GroupBy, select.Projection, select.Having, select.OrderBy, windows); + node = new AggregateNode(node, select.GroupBy, select.Projection, select.Having, select.OrderBy, windows, ties); + else if (ties is not null) + // Kept above the joins rather than sunk into one side (PushSort): the cut needs each row's keys. + node = new SortNode(node, select.OrderBy, Ties: ties); else if (select.OrderBy.Count > 0) node = PushSort(node, select.OrderBy); @@ -105,7 +127,7 @@ public static PlanNode PlanSelect(SelectStatement select) if (select.Distinct) node = new DistinctNode(node); - if (select.Top is { } top || select.Offset is not null) + if (ties is null && (select.Top is not null || select.Offset is not null)) { // `TOP n ... ORDER BY k` only needs the n smallest rows by k, so tell the sort the bound and let it // discard rows that can't survive rather than ordering everything. Only sound when nothing between the diff --git a/src/LibRed/LibRed.Sql/Ast/Statements.cs b/src/LibRed/LibRed.Sql/Ast/Statements.cs index b016e823..e9fcc01d 100644 --- a/src/LibRed/LibRed.Sql/Ast/Statements.cs +++ b/src/LibRed/LibRed.Sql/Ast/Statements.cs @@ -45,7 +45,12 @@ public sealed record SelectStatement( /// TOP m and so reuse the path that already exists for it. is never set /// from a FETCH, which has no PERCENT form. /// - Expression? Offset = null) : SqlStatement; + Expression? Offset = null, + /// + /// TOP n WITH TIES (or FETCH … WITH TIES): also returns every further row whose + /// ORDER BY keys equal the last one's. It needs an ORDER BY. + /// + bool WithTies = false) : 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). @@ -81,7 +86,9 @@ public sealed record SetOperationStatement( SqlStatement Right, IReadOnlyList? OrderBy = null, Expression? Top = null, - Expression? Offset = null) : SqlStatement; + Expression? Offset = null, + // FETCH … WITH TIES, as SelectStatement.WithTies. + bool WithTies = false) : SqlStatement; /// /// A table value constructor used as a query rather than as an INSERT's VALUES clause — diff --git a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 index 3f75b7d7..fdb1b028 100644 --- a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 +++ b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 @@ -320,8 +320,10 @@ havingClause : HAVING expression ; // Access allows only a literal after TOP, but LibRed also accepts a parameter (or a +/- expression of // literals/parameters) — EFCore.Jet normally inlines the value, and we can evaluate it directly instead. // Restricted to additive operands (no bare '*') so it can't swallow a following SELECT star ('TOP n *'). -// A trailing PERCENT returns that percentage of rows (ceil) instead of a fixed count. -topClause : TOP topOperand ((PLUS | MINUS) topOperand)* percent=PERCENT? ; +// A trailing PERCENT returns that percentage of rows (ceil) instead of a fixed count. WITH TIES (SQL Server's) also +// returns every further row whose ORDER BY keys equal the last one's — ACE's own TOP always does, where LibRed's plain +// TOP returns exactly n. +topClause : TOP topOperand ((PLUS | MINUS) topOperand)* percent=PERCENT? (WITH ties=TIES)? ; topOperand : INTEGER_LITERAL | PARAM | LPAREN expression RPAREN ; // ANSI SQL:2008 paging, which EF Core's base QuerySqlGenerator.GenerateLimitOffset emits whenever the @@ -345,9 +347,11 @@ topOperand : INTEGER_LITERAL | PARAM | LPAREN expression RPAREN ; // `Where_subquery_with_ElementAt_using_column_as_index` carries `OFFSET [s].[Id] ROWS`, and that baseline only // exists because the test passed against a real server. So this matches the engine's behaviour; it is the // documentation that is incomplete. +// +// The standard's WITH TIES may stand in for ONLY, with TOP … WITH TIES's meaning. offsetFetchClause - : OFFSET offset=expression rowKeyword (FETCH (NEXT | FIRST) limit=expression rowKeyword ONLY)? - | FETCH (FIRST | NEXT) limit=expression rowKeyword ONLY + : OFFSET offset=expression rowKeyword (FETCH (NEXT | FIRST) limit=expression rowKeyword (ONLY | WITH ties=TIES))? + | FETCH (FIRST | NEXT) limit=expression rowKeyword (ONLY | WITH ties=TIES) ; rowKeyword : ROW | ROWS ; diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs index 88e32170..2b950e23 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs @@ -5130,6 +5130,7 @@ public HavingClauseContext havingClause() { public partial class TopClauseContext : ParserRuleContext { public IToken percent; + public IToken ties; [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode TOP() { return GetToken(AccessSqlParser.TOP, 0); } [System.Diagnostics.DebuggerNonUserCode] public TopOperandContext[] topOperand() { return GetRuleContexts(); @@ -5137,6 +5138,7 @@ [System.Diagnostics.DebuggerNonUserCode] public TopOperandContext[] topOperand() [System.Diagnostics.DebuggerNonUserCode] public TopOperandContext topOperand(int i) { return GetRuleContext(i); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode WITH() { return GetToken(AccessSqlParser.WITH, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode[] PLUS() { return GetTokens(AccessSqlParser.PLUS); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode PLUS(int i) { return GetToken(AccessSqlParser.PLUS, i); @@ -5146,6 +5148,7 @@ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode MINUS(int i) { return GetToken(AccessSqlParser.MINUS, i); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode PERCENT() { return GetToken(AccessSqlParser.PERCENT, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode TIES() { return GetToken(AccessSqlParser.TIES, 0); } public TopClauseContext(ParserRuleContext parent, int invokingState) : base(parent, invokingState) { @@ -5207,6 +5210,18 @@ public TopClauseContext topClause() { } } + State = 850; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + if (_la==WITH) { + { + State = 848; + Match(WITH); + State = 849; + _localctx.ties = Match(TIES); + } + } + } } catch (RecognitionException re) { @@ -5246,31 +5261,31 @@ public TopOperandContext topOperand() { TopOperandContext _localctx = new TopOperandContext(Context, State); EnterRule(_localctx, 86, RULE_topOperand); try { - State = 854; + State = 858; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: EnterOuterAlt(_localctx, 1); { - State = 848; + State = 852; Match(INTEGER_LITERAL); } break; case PARAM: EnterOuterAlt(_localctx, 2); { - State = 849; + State = 853; Match(PARAM); } break; case LPAREN: EnterOuterAlt(_localctx, 3); { - State = 850; + State = 854; Match(LPAREN); - State = 851; + State = 855; expression(0); - State = 852; + State = 856; Match(RPAREN); } break; @@ -5292,6 +5307,7 @@ public TopOperandContext topOperand() { public partial class OffsetFetchClauseContext : ParserRuleContext { public ExpressionContext offset; public ExpressionContext limit; + public IToken ties; [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OFFSET() { return GetToken(AccessSqlParser.OFFSET, 0); } [System.Diagnostics.DebuggerNonUserCode] public RowKeywordContext[] rowKeyword() { return GetRuleContexts(); @@ -5306,9 +5322,11 @@ [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression(int return GetRuleContext(i); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FETCH() { return GetToken(AccessSqlParser.FETCH, 0); } - [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ONLY() { return GetToken(AccessSqlParser.ONLY, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode NEXT() { return GetToken(AccessSqlParser.NEXT, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FIRST() { return GetToken(AccessSqlParser.FIRST, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode ONLY() { return GetToken(AccessSqlParser.ONLY, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode WITH() { return GetToken(AccessSqlParser.WITH, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode TIES() { return GetToken(AccessSqlParser.TIES, 0); } public OffsetFetchClauseContext(ParserRuleContext parent, int invokingState) : base(parent, invokingState) { @@ -5328,26 +5346,26 @@ public OffsetFetchClauseContext offsetFetchClause() { EnterRule(_localctx, 88, RULE_offsetFetchClause); int _la; try { - State = 873; + State = 883; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case OFFSET: EnterOuterAlt(_localctx, 1); { - State = 856; + State = 860; Match(OFFSET); - State = 857; + State = 861; _localctx.offset = expression(0); - State = 858; + State = 862; rowKeyword(); - State = 865; + State = 872; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FETCH) { { - State = 859; + State = 863; Match(FETCH); - State = 860; + State = 864; _la = TokenStream.LA(1); if ( !(_la==NEXT || _la==FIRST) ) { ErrorHandler.RecoverInline(this); @@ -5356,12 +5374,30 @@ public OffsetFetchClauseContext offsetFetchClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 861; + State = 865; _localctx.limit = expression(0); - State = 862; + State = 866; rowKeyword(); - State = 863; - Match(ONLY); + State = 870; + ErrorHandler.Sync(this); + switch (TokenStream.LA(1)) { + case ONLY: + { + State = 867; + Match(ONLY); + } + break; + case WITH: + { + State = 868; + Match(WITH); + State = 869; + _localctx.ties = Match(TIES); + } + break; + default: + throw new NoViableAltException(this); + } } } @@ -5370,9 +5406,9 @@ public OffsetFetchClauseContext offsetFetchClause() { case FETCH: EnterOuterAlt(_localctx, 2); { - State = 867; + State = 874; Match(FETCH); - State = 868; + State = 875; _la = TokenStream.LA(1); if ( !(_la==NEXT || _la==FIRST) ) { ErrorHandler.RecoverInline(this); @@ -5381,12 +5417,30 @@ public OffsetFetchClauseContext offsetFetchClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 869; + State = 876; _localctx.limit = expression(0); - State = 870; + State = 877; rowKeyword(); - State = 871; - Match(ONLY); + State = 881; + ErrorHandler.Sync(this); + switch (TokenStream.LA(1)) { + case ONLY: + { + State = 878; + Match(ONLY); + } + break; + case WITH: + { + State = 879; + Match(WITH); + State = 880; + _localctx.ties = Match(TIES); + } + break; + default: + throw new NoViableAltException(this); + } } break; default: @@ -5428,7 +5482,7 @@ public RowKeywordContext rowKeyword() { try { EnterOuterAlt(_localctx, 1); { - State = 875; + State = 885; _la = TokenStream.LA(1); if ( !(_la==ROWS || _la==ROW) ) { ErrorHandler.RecoverInline(this); @@ -5481,13 +5535,13 @@ public SelectListContext selectList() { EnterRule(_localctx, 92, RULE_selectList); int _la; try { - State = 886; + State = 896; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: EnterOuterAlt(_localctx, 1); { - State = 877; + State = 887; Match(STAR); } break; @@ -5537,21 +5591,21 @@ public SelectListContext selectList() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 878; + State = 888; selectItem(); - State = 883; + State = 893; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 879; + State = 889; Match(COMMA); - State = 880; + State = 890; selectItem(); } } - State = 885; + State = 895; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5623,18 +5677,18 @@ public SelectItemContext selectItem() { EnterRule(_localctx, 94, RULE_selectItem); int _la; try { - State = 899; + State = 909; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,107,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,110,Context) ) { case 1: _localctx = new QualifiedStarSelectItemContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 888; + State = 898; ((QualifiedStarSelectItemContext)_localctx).qualifier = identifier(); - State = 889; + State = 899; Match(DOT); - State = 890; + State = 900; Match(STAR); } break; @@ -5642,24 +5696,24 @@ public SelectItemContext selectItem() { _localctx = new ExpressionSelectItemContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 892; + State = 902; expression(0); - State = 897; + State = 907; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 894; + State = 904; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 893; + State = 903; Match(AS); } } - State = 896; + State = 906; ((ExpressionSelectItemContext)_localctx).alias = identifier(); } } @@ -5712,23 +5766,23 @@ public FromClauseContext fromClause() { try { EnterOuterAlt(_localctx, 1); { - State = 901; + State = 911; Match(FROM); - State = 902; + State = 912; tableSource(); - State = 907; + State = 917; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 903; + State = 913; Match(COMMA); - State = 904; + State = 914; tableSource(); } } - State = 909; + State = 919; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5776,19 +5830,19 @@ public TableSourceContext tableSource() { try { EnterOuterAlt(_localctx, 1); { - State = 910; + State = 920; tablePrimary(); - State = 914; + State = 924; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 137455468544L) != 0)) { { { - State = 911; + State = 921; joinClause(); } } - State = 916; + State = 926; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5875,31 +5929,31 @@ public TablePrimaryContext tablePrimary() { EnterRule(_localctx, 100, RULE_tablePrimary); int _la; try { - State = 937; + State = 947; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,114,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,117,Context) ) { case 1: _localctx = new NamedTablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 917; + State = 927; ((NamedTablePrimaryContext)_localctx).table = identifier(); - State = 922; + State = 932; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 919; + State = 929; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 918; + State = 928; Match(AS); } } - State = 921; + State = 931; ((NamedTablePrimaryContext)_localctx).alias = identifier(); } } @@ -5910,28 +5964,28 @@ public TablePrimaryContext tablePrimary() { _localctx = new SubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 924; + State = 934; Match(LPAREN); - State = 925; + State = 935; queryExpression(); - State = 926; + State = 936; Match(RPAREN); - State = 931; + State = 941; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 928; + State = 938; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 927; + State = 937; Match(AS); } } - State = 930; + State = 940; ((SubqueryPrimaryContext)_localctx).alias = identifier(); } } @@ -5942,11 +5996,11 @@ public TablePrimaryContext tablePrimary() { _localctx = new ParenJoinPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 933; + State = 943; Match(LPAREN); - State = 934; + State = 944; tableSource(); - State = 935; + State = 945; Match(RPAREN); } break; @@ -6043,22 +6097,22 @@ public JoinClauseContext joinClause() { JoinClauseContext _localctx = new JoinClauseContext(Context, State); EnterRule(_localctx, 102, RULE_joinClause); try { - State = 954; + State = 964; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,115,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,118,Context) ) { case 1: _localctx = new ConditionalJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 939; + State = 949; joinType(); - State = 940; + State = 950; Match(JOIN); - State = 941; + State = 951; tablePrimary(); - State = 942; + State = 952; Match(ON); - State = 943; + State = 953; expression(0); } break; @@ -6066,11 +6120,11 @@ public JoinClauseContext joinClause() { _localctx = new CrossJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 945; + State = 955; Match(CROSS); - State = 946; + State = 956; Match(JOIN); - State = 947; + State = 957; tablePrimary(); } break; @@ -6078,11 +6132,11 @@ public JoinClauseContext joinClause() { _localctx = new CrossApplyContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 948; + State = 958; Match(CROSS); - State = 949; + State = 959; Match(APPLY); - State = 950; + State = 960; tablePrimary(); } break; @@ -6090,11 +6144,11 @@ public JoinClauseContext joinClause() { _localctx = new OuterApplyContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 951; + State = 961; Match(OUTER); - State = 952; + State = 962; Match(APPLY); - State = 953; + State = 963; tablePrimary(); } break; @@ -6173,7 +6227,7 @@ public JoinTypeContext joinType() { EnterRule(_localctx, 104, RULE_joinType); int _la; try { - State = 971; + State = 981; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INNER: @@ -6181,12 +6235,12 @@ public JoinTypeContext joinType() { _localctx = new InnerJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 957; + State = 967; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==INNER) { { - State = 956; + State = 966; Match(INNER); } } @@ -6197,14 +6251,14 @@ public JoinTypeContext joinType() { _localctx = new LeftJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 959; + State = 969; Match(LEFT); - State = 961; + State = 971; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 960; + State = 970; Match(OUTER); } } @@ -6215,14 +6269,14 @@ public JoinTypeContext joinType() { _localctx = new RightJoinContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 963; + State = 973; Match(RIGHT); - State = 965; + State = 975; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 964; + State = 974; Match(OUTER); } } @@ -6233,14 +6287,14 @@ public JoinTypeContext joinType() { _localctx = new FullJoinContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 967; + State = 977; Match(FULL); - State = 969; + State = 979; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 968; + State = 978; Match(OUTER); } } @@ -6287,9 +6341,9 @@ public WhereClauseContext whereClause() { try { EnterOuterAlt(_localctx, 1); { - State = 973; + State = 983; Match(WHERE); - State = 974; + State = 984; expression(0); } } @@ -6338,25 +6392,25 @@ public OrderByClauseContext orderByClause() { try { EnterOuterAlt(_localctx, 1); { - State = 976; + State = 986; Match(ORDER); - State = 977; + State = 987; Match(BY); - State = 978; + State = 988; orderByItem(); - State = 983; + State = 993; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 979; + State = 989; Match(COMMA); - State = 980; + State = 990; orderByItem(); } } - State = 985; + State = 995; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -6401,14 +6455,14 @@ public OrderByItemContext orderByItem() { try { EnterOuterAlt(_localctx, 1); { - State = 986; + State = 996; expression(0); - State = 988; + State = 998; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ASC || _la==DESC) { { - State = 987; + State = 997; _localctx.dir = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -6892,7 +6946,7 @@ private ExpressionContext expression(int _p) { int _alt; EnterOuterAlt(_localctx, 1); { - State = 996; + State = 1006; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case PLUS: @@ -6902,7 +6956,7 @@ private ExpressionContext expression(int _p) { Context = _localctx; _prevctx = _localctx; - State = 991; + State = 1001; ((NegateExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -6912,7 +6966,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 992; + State = 1002; expression(21); } break; @@ -6922,7 +6976,7 @@ private ExpressionContext expression(int _p) { _localctx = new NotExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 993; + State = 1003; ((NotExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==NOT || _la==BNOT) ) { @@ -6932,7 +6986,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 994; + State = 1004; expression(7); } break; @@ -6980,7 +7034,7 @@ private ExpressionContext expression(int _p) { _localctx = new PrimaryExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 995; + State = 1005; primary(); } break; @@ -6988,28 +7042,28 @@ private ExpressionContext expression(int _p) { throw new NoViableAltException(this); } Context.Stop = TokenStream.LT(-1); - State = 1096; + State = 1106; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,133,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,136,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { if ( ParseListeners!=null ) TriggerExitRuleEvent(); _prevctx = _localctx; { - State = 1094; + State = 1104; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,132,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,135,Context) ) { case 1: { _localctx = new PowExprContext(new ExpressionContext(_parentctx, _parentState)); ((PowExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 998; + State = 1008; if (!(Precpred(Context, 22))) throw new FailedPredicateException(this, "Precpred(Context, 22)"); - State = 999; + State = 1009; Match(CARET); - State = 1000; + State = 1010; ((PowExprContext)_localctx).right = expression(23); } break; @@ -7018,9 +7072,9 @@ private ExpressionContext expression(int _p) { _localctx = new MulDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((MulDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1001; + State = 1011; if (!(Precpred(Context, 20))) throw new FailedPredicateException(this, "Precpred(Context, 20)"); - State = 1002; + State = 1012; ((MulDivExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==STAR || _la==SLASH) ) { @@ -7030,7 +7084,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1003; + State = 1013; ((MulDivExprContext)_localctx).right = expression(21); } break; @@ -7039,11 +7093,11 @@ private ExpressionContext expression(int _p) { _localctx = new IntDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((IntDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1004; + State = 1014; if (!(Precpred(Context, 19))) throw new FailedPredicateException(this, "Precpred(Context, 19)"); - State = 1005; + State = 1015; ((IntDivExprContext)_localctx).op = Match(BACKSLASH); - State = 1006; + State = 1016; ((IntDivExprContext)_localctx).right = expression(20); } break; @@ -7052,11 +7106,11 @@ private ExpressionContext expression(int _p) { _localctx = new ModExprContext(new ExpressionContext(_parentctx, _parentState)); ((ModExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1007; + State = 1017; if (!(Precpred(Context, 18))) throw new FailedPredicateException(this, "Precpred(Context, 18)"); - State = 1008; + State = 1018; ((ModExprContext)_localctx).op = Match(MOD); - State = 1009; + State = 1019; ((ModExprContext)_localctx).right = expression(19); } break; @@ -7065,9 +7119,9 @@ private ExpressionContext expression(int _p) { _localctx = new AddSubExprContext(new ExpressionContext(_parentctx, _parentState)); ((AddSubExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1010; + State = 1020; if (!(Precpred(Context, 17))) throw new FailedPredicateException(this, "Precpred(Context, 17)"); - State = 1011; + State = 1021; ((AddSubExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -7077,7 +7131,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1012; + State = 1022; ((AddSubExprContext)_localctx).right = expression(18); } break; @@ -7086,11 +7140,11 @@ private ExpressionContext expression(int _p) { _localctx = new ConcatExprContext(new ExpressionContext(_parentctx, _parentState)); ((ConcatExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1013; + State = 1023; if (!(Precpred(Context, 16))) throw new FailedPredicateException(this, "Precpred(Context, 16)"); - State = 1014; + State = 1024; ((ConcatExprContext)_localctx).op = Match(AMP); - State = 1015; + State = 1025; ((ConcatExprContext)_localctx).right = expression(17); } break; @@ -7099,9 +7153,9 @@ private ExpressionContext expression(int _p) { _localctx = new ComparisonExprContext(new ExpressionContext(_parentctx, _parentState)); ((ComparisonExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1016; + State = 1026; if (!(Precpred(Context, 15))) throw new FailedPredicateException(this, "Precpred(Context, 15)"); - State = 1017; + State = 1027; ((ComparisonExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(((((_la - 131)) & ~0x3f) == 0 && ((1L << (_la - 131)) & 63L) != 0)) ) { @@ -7111,7 +7165,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1018; + State = 1028; ((ComparisonExprContext)_localctx).right = expression(16); } break; @@ -7120,25 +7174,25 @@ private ExpressionContext expression(int _p) { _localctx = new BetweenExprContext(new ExpressionContext(_parentctx, _parentState)); ((BetweenExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1019; + State = 1029; if (!(Precpred(Context, 14))) throw new FailedPredicateException(this, "Precpred(Context, 14)"); - State = 1021; + State = 1031; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1020; + State = 1030; ((BetweenExprContext)_localctx).not = Match(NOT); } } - State = 1023; + State = 1033; Match(BETWEEN); - State = 1024; + State = 1034; ((BetweenExprContext)_localctx).lo = expression(0); - State = 1025; + State = 1035; Match(AND); - State = 1026; + State = 1036; ((BetweenExprContext)_localctx).hi = expression(15); } break; @@ -7147,21 +7201,21 @@ private ExpressionContext expression(int _p) { _localctx = new LikeExprContext(new ExpressionContext(_parentctx, _parentState)); ((LikeExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1028; + State = 1038; if (!(Precpred(Context, 13))) throw new FailedPredicateException(this, "Precpred(Context, 13)"); - State = 1030; + State = 1040; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1029; + State = 1039; ((LikeExprContext)_localctx).not = Match(NOT); } } - State = 1032; + State = 1042; Match(LIKE); - State = 1033; + State = 1043; ((LikeExprContext)_localctx).right = expression(14); } break; @@ -7170,25 +7224,25 @@ private ExpressionContext expression(int _p) { _localctx = new IsDistinctFromExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsDistinctFromExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1034; + State = 1044; if (!(Precpred(Context, 8))) throw new FailedPredicateException(this, "Precpred(Context, 8)"); - State = 1035; + State = 1045; Match(IS); - State = 1037; + State = 1047; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1036; + State = 1046; ((IsDistinctFromExprContext)_localctx).not = Match(NOT); } } - State = 1039; + State = 1049; Match(DISTINCT); - State = 1040; + State = 1050; Match(FROM); - State = 1041; + State = 1051; ((IsDistinctFromExprContext)_localctx).right = expression(9); } break; @@ -7197,9 +7251,9 @@ private ExpressionContext expression(int _p) { _localctx = new AndExprContext(new ExpressionContext(_parentctx, _parentState)); ((AndExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1042; + State = 1052; if (!(Precpred(Context, 6))) throw new FailedPredicateException(this, "Precpred(Context, 6)"); - State = 1043; + State = 1053; ((AndExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==AND || _la==BAND) ) { @@ -7209,7 +7263,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1044; + State = 1054; ((AndExprContext)_localctx).right = expression(7); } break; @@ -7218,9 +7272,9 @@ private ExpressionContext expression(int _p) { _localctx = new OrExprContext(new ExpressionContext(_parentctx, _parentState)); ((OrExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1045; + State = 1055; if (!(Precpred(Context, 5))) throw new FailedPredicateException(this, "Precpred(Context, 5)"); - State = 1046; + State = 1056; ((OrExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==OR || _la==BOR) ) { @@ -7230,7 +7284,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1047; + State = 1057; ((OrExprContext)_localctx).right = expression(6); } break; @@ -7239,9 +7293,9 @@ private ExpressionContext expression(int _p) { _localctx = new XorExprContext(new ExpressionContext(_parentctx, _parentState)); ((XorExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1048; + State = 1058; if (!(Precpred(Context, 4))) throw new FailedPredicateException(this, "Precpred(Context, 4)"); - State = 1049; + State = 1059; ((XorExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==XOR || _la==BXOR) ) { @@ -7251,7 +7305,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1050; + State = 1060; ((XorExprContext)_localctx).right = expression(5); } break; @@ -7260,11 +7314,11 @@ private ExpressionContext expression(int _p) { _localctx = new EqvExprContext(new ExpressionContext(_parentctx, _parentState)); ((EqvExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1051; + State = 1061; if (!(Precpred(Context, 3))) throw new FailedPredicateException(this, "Precpred(Context, 3)"); - State = 1052; + State = 1062; ((EqvExprContext)_localctx).op = Match(EQV); - State = 1053; + State = 1063; ((EqvExprContext)_localctx).right = expression(4); } break; @@ -7273,11 +7327,11 @@ private ExpressionContext expression(int _p) { _localctx = new ImpExprContext(new ExpressionContext(_parentctx, _parentState)); ((ImpExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1054; + State = 1064; if (!(Precpred(Context, 2))) throw new FailedPredicateException(this, "Precpred(Context, 2)"); - State = 1055; + State = 1065; ((ImpExprContext)_localctx).op = Match(IMP); - State = 1056; + State = 1066; ((ImpExprContext)_localctx).right = expression(3); } break; @@ -7286,25 +7340,25 @@ private ExpressionContext expression(int _p) { _localctx = new InSubqueryExprContext(new ExpressionContext(_parentctx, _parentState)); ((InSubqueryExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1057; + State = 1067; if (!(Precpred(Context, 12))) throw new FailedPredicateException(this, "Precpred(Context, 12)"); - State = 1059; + State = 1069; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1058; + State = 1068; ((InSubqueryExprContext)_localctx).not = Match(NOT); } } - State = 1061; + State = 1071; Match(IN); - State = 1062; + State = 1072; Match(LPAREN); - State = 1063; + State = 1073; ((InSubqueryExprContext)_localctx).sub = queryExpression(); - State = 1064; + State = 1074; Match(RPAREN); } break; @@ -7313,43 +7367,43 @@ private ExpressionContext expression(int _p) { _localctx = new InExprContext(new ExpressionContext(_parentctx, _parentState)); ((InExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1066; + State = 1076; if (!(Precpred(Context, 11))) throw new FailedPredicateException(this, "Precpred(Context, 11)"); - State = 1068; + State = 1078; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1067; + State = 1077; ((InExprContext)_localctx).not = Match(NOT); } } - State = 1070; + State = 1080; Match(IN); - State = 1071; + State = 1081; Match(LPAREN); - State = 1072; + State = 1082; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); - State = 1077; + State = 1087; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1073; + State = 1083; Match(COMMA); - State = 1074; + State = 1084; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); } } - State = 1079; + State = 1089; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 1080; + State = 1090; Match(RPAREN); } break; @@ -7358,21 +7412,21 @@ private ExpressionContext expression(int _p) { _localctx = new IsNullExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsNullExprContext)_localctx).operand = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1082; + State = 1092; if (!(Precpred(Context, 10))) throw new FailedPredicateException(this, "Precpred(Context, 10)"); - State = 1083; + State = 1093; Match(IS); - State = 1085; + State = 1095; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1084; + State = 1094; ((IsNullExprContext)_localctx).not = Match(NOT); } } - State = 1087; + State = 1097; Match(NULL); } break; @@ -7381,21 +7435,21 @@ private ExpressionContext expression(int _p) { _localctx = new IsTruthExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsTruthExprContext)_localctx).operand = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1088; + State = 1098; if (!(Precpred(Context, 9))) throw new FailedPredicateException(this, "Precpred(Context, 9)"); - State = 1089; + State = 1099; Match(IS); - State = 1091; + State = 1101; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1090; + State = 1100; ((IsTruthExprContext)_localctx).not = Match(NOT); } } - State = 1093; + State = 1103; ((IsTruthExprContext)_localctx).truth = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==TRUE || _la==FALSE) ) { @@ -7410,9 +7464,9 @@ private ExpressionContext expression(int _p) { } } } - State = 1098; + State = 1108; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,133,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,136,Context); } } } @@ -7556,14 +7610,14 @@ public PrimaryContext primary() { PrimaryContext _localctx = new PrimaryContext(Context, State); EnterRule(_localctx, 114, RULE_primary); try { - State = 1118; + State = 1128; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,134,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,137,Context) ) { case 1: _localctx = new LiteralPrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1099; + State = 1109; literal(); } break; @@ -7571,7 +7625,7 @@ public PrimaryContext primary() { _localctx = new CasePrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1100; + State = 1110; caseExpression(); } break; @@ -7579,7 +7633,7 @@ public PrimaryContext primary() { _localctx = new FunctionCallPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1101; + State = 1111; functionCall(); } break; @@ -7587,7 +7641,7 @@ public PrimaryContext primary() { _localctx = new ColumnPrimaryContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 1102; + State = 1112; columnRef(); } break; @@ -7595,7 +7649,7 @@ public PrimaryContext primary() { _localctx = new ParamPrimaryContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 1103; + State = 1113; Match(PARAM); } break; @@ -7603,7 +7657,7 @@ public PrimaryContext primary() { _localctx = new SystemVariablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 1104; + State = 1114; Match(SYSVAR); } break; @@ -7611,13 +7665,13 @@ public PrimaryContext primary() { _localctx = new ExistsPrimaryContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 1105; + State = 1115; Match(EXISTS); - State = 1106; + State = 1116; Match(LPAREN); - State = 1107; + State = 1117; queryExpression(); - State = 1108; + State = 1118; Match(RPAREN); } break; @@ -7625,11 +7679,11 @@ public PrimaryContext primary() { _localctx = new ScalarSubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 1110; + State = 1120; Match(LPAREN); - State = 1111; + State = 1121; queryExpression(); - State = 1112; + State = 1122; Match(RPAREN); } break; @@ -7637,11 +7691,11 @@ public PrimaryContext primary() { _localctx = new ParenPrimaryContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 1114; + State = 1124; Match(LPAREN); - State = 1115; + State = 1125; expression(0); - State = 1116; + State = 1126; Match(RPAREN); } break; @@ -7697,45 +7751,45 @@ public CaseExpressionContext caseExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 1120; + State = 1130; Match(CASE); - State = 1122; + State = 1132; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775694237952L) != 0) || ((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & -283699838490247167L) != 0) || ((((_la - 149)) & ~0x3f) == 0 && ((1L << (_la - 149)) & 31L) != 0)) { { - State = 1121; + State = 1131; _localctx.operand = expression(0); } } - State = 1125; + State = 1135; ErrorHandler.Sync(this); _la = TokenStream.LA(1); do { { { - State = 1124; + State = 1134; caseWhen(); } } - State = 1127; + State = 1137; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } while ( _la==WHEN ); - State = 1131; + State = 1141; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ELSE) { { - State = 1129; + State = 1139; Match(ELSE); - State = 1130; + State = 1140; _localctx.elseResult = expression(0); } } - State = 1133; + State = 1143; Match(END); } } @@ -7781,13 +7835,13 @@ public CaseWhenContext caseWhen() { try { EnterOuterAlt(_localctx, 1); { - State = 1135; + State = 1145; Match(WHEN); - State = 1136; + State = 1146; _localctx.condition = expression(0); - State = 1137; + State = 1147; Match(THEN); - State = 1138; + State = 1148; _localctx.result = expression(0); } } @@ -7860,16 +7914,16 @@ public FunctionCallContext functionCall() { try { EnterOuterAlt(_localctx, 1); { - State = 1140; + State = 1150; _localctx.name = functionName(); - State = 1141; + State = 1151; Match(LPAREN); - State = 1154; + State = 1164; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: { - State = 1142; + State = 1152; _localctx.star = Match(STAR); } break; @@ -7920,31 +7974,31 @@ public FunctionCallContext functionCall() { case IDENTIFIER: { { - State = 1144; + State = 1154; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==DISTINCT) { { - State = 1143; + State = 1153; _localctx.distinct = Match(DISTINCT); } } - State = 1146; + State = 1156; expression(0); - State = 1151; + State = 1161; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1147; + State = 1157; Match(COMMA); - State = 1148; + State = 1158; expression(0); } } - State = 1153; + State = 1163; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -7956,56 +8010,56 @@ public FunctionCallContext functionCall() { default: break; } - State = 1156; + State = 1166; Match(RPAREN); - State = 1158; + State = 1168; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,141,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,144,Context) ) { case 1: { - State = 1157; + State = 1167; withinGroup(); } break; } - State = 1161; + State = 1171; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,142,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,145,Context) ) { case 1: { - State = 1160; + State = 1170; filterClause(); } break; } - State = 1171; + State = 1181; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,145,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,148,Context) ) { case 1: { - State = 1164; + State = 1174; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FROM) { { - State = 1163; + State = 1173; nthRowFrom(); } } - State = 1167; + State = 1177; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==IGNORE || _la==RESPECT) { { - State = 1166; + State = 1176; nullTreatment(); } } - State = 1169; + State = 1179; Match(OVER); - State = 1170; + State = 1180; windowSpecification(); } break; @@ -8050,7 +8104,7 @@ public FunctionNameContext functionName() { FunctionNameContext _localctx = new FunctionNameContext(Context, State); EnterRule(_localctx, 122, RULE_functionName); try { - State = 1179; + State = 1189; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -8074,42 +8128,42 @@ public FunctionNameContext functionName() { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1173; + State = 1183; identifier(); } break; case LEFT: EnterOuterAlt(_localctx, 2); { - State = 1174; + State = 1184; Match(LEFT); } break; case RIGHT: EnterOuterAlt(_localctx, 3); { - State = 1175; + State = 1185; Match(RIGHT); } break; case ASC: EnterOuterAlt(_localctx, 4); { - State = 1176; + State = 1186; Match(ASC); } break; case FIRST: EnterOuterAlt(_localctx, 5); { - State = 1177; + State = 1187; Match(FIRST); } break; case PARTITION: EnterOuterAlt(_localctx, 6); { - State = 1178; + State = 1188; Match(PARTITION); } break; @@ -8133,7 +8187,7 @@ public partial class ColumnRefContext : ParserRuleContext { public IToken _DOT; public IList _separators = new List(); public IToken _BANG; - public IToken _tset3252; + public IToken _tset3289; public MemberNameContext _memberName; public IList _rest = new List(); [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() { @@ -8175,35 +8229,35 @@ public ColumnRefContext columnRef() { int _alt; EnterOuterAlt(_localctx, 1); { - State = 1181; + State = 1191; _localctx.first = identifier(); - State = 1186; + State = 1196; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,147,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,150,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 1182; - _localctx._tset3252 = TokenStream.LT(1); + State = 1192; + _localctx._tset3289 = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==DOT || _la==BANG) ) { - _localctx._tset3252 = ErrorHandler.RecoverInline(this); + _localctx._tset3289 = ErrorHandler.RecoverInline(this); } else { ErrorHandler.ReportMatch(this); Consume(); } - _localctx._separators.Add(_localctx._tset3252); - State = 1183; + _localctx._separators.Add(_localctx._tset3289); + State = 1193; _localctx._memberName = memberName(); _localctx._rest.Add(_localctx._memberName); } } } - State = 1188; + State = 1198; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,147,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,150,Context); } } } @@ -8243,7 +8297,7 @@ public MemberNameContext memberName() { MemberNameContext _localctx = new MemberNameContext(Context, State); EnterRule(_localctx, 126, RULE_memberName); try { - State = 1191; + State = 1201; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -8267,7 +8321,7 @@ public MemberNameContext memberName() { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1189; + State = 1199; identifier(); } break; @@ -8380,7 +8434,7 @@ public MemberNameContext memberName() { case NULL: EnterOuterAlt(_localctx, 2); { - State = 1190; + State = 1200; reservedKeyword(); } break; @@ -8424,27 +8478,27 @@ public IdentifierContext identifier() { IdentifierContext _localctx = new IdentifierContext(Context, State); EnterRule(_localctx, 128, RULE_identifier); try { - State = 1197; + State = 1207; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1193; + State = 1203; Match(IDENTIFIER); } break; case BRACKET_ID: EnterOuterAlt(_localctx, 2); { - State = 1194; + State = 1204; Match(BRACKET_ID); } break; case BACKTICK_ID: EnterOuterAlt(_localctx, 3); { - State = 1195; + State = 1205; Match(BACKTICK_ID); } break; @@ -8466,7 +8520,7 @@ public IdentifierContext identifier() { case FILTER: EnterOuterAlt(_localctx, 4); { - State = 1196; + State = 1206; nonReservedKeyword(); } break; @@ -8598,14 +8652,14 @@ public LiteralContext literal() { EnterRule(_localctx, 130, RULE_literal); int _la; try { - State = 1208; + State = 1218; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: _localctx = new IntLiteralContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1199; + State = 1209; Match(INTEGER_LITERAL); } break; @@ -8613,7 +8667,7 @@ public LiteralContext literal() { _localctx = new NumberLiteralContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1200; + State = 1210; Match(NUMBER_LITERAL); } break; @@ -8621,7 +8675,7 @@ public LiteralContext literal() { _localctx = new HexLiteralContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1201; + State = 1211; Match(HEX_LITERAL); } break; @@ -8629,7 +8683,7 @@ public LiteralContext literal() { _localctx = new StringLiteralContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 1202; + State = 1212; Match(STRING_LITERAL); } break; @@ -8637,7 +8691,7 @@ public LiteralContext literal() { _localctx = new DateLiteralContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 1203; + State = 1213; Match(DATE_LITERAL); } break; @@ -8645,7 +8699,7 @@ public LiteralContext literal() { _localctx = new GuidLiteralContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 1204; + State = 1214; Match(GUID_LITERAL); } break; @@ -8655,7 +8709,7 @@ public LiteralContext literal() { _localctx = new TrueLiteralContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 1205; + State = 1215; _la = TokenStream.LA(1); if ( !(_la==ON || _la==TRUE || _la==YES) ) { ErrorHandler.RecoverInline(this); @@ -8672,7 +8726,7 @@ public LiteralContext literal() { _localctx = new FalseLiteralContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 1206; + State = 1216; _la = TokenStream.LA(1); if ( !(((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & 10485761L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -8687,7 +8741,7 @@ public LiteralContext literal() { _localctx = new NullLiteralContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 1207; + State = 1217; Match(NULL); } break; @@ -8761,21 +8815,21 @@ public TransactionStatementContext transactionStatement() { EnterRule(_localctx, 132, RULE_transactionStatement); int _la; try { - State = 1222; + State = 1232; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BEGIN: _localctx = new BeginTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1210; + State = 1220; Match(BEGIN); - State = 1212; + State = 1222; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1211; + State = 1221; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8793,14 +8847,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new CommitTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1214; + State = 1224; Match(COMMIT); - State = 1216; + State = 1226; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1215; + State = 1225; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8818,14 +8872,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new RollbackTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1218; + State = 1228; Match(ROLLBACK); - State = 1220; + State = 1230; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1219; + State = 1229; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8879,9 +8933,9 @@ public StandaloneExpressionContext standaloneExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 1224; + State = 1234; expression(0); - State = 1225; + State = 1235; Match(Eof); } } @@ -8940,61 +8994,61 @@ public WindowSpecificationContext windowSpecification() { try { EnterOuterAlt(_localctx, 1); { - State = 1227; + State = 1237; Match(LPAREN); - State = 1238; + State = 1248; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PARTITION) { { - State = 1228; + State = 1238; Match(PARTITION); - State = 1229; + State = 1239; Match(BY); - State = 1230; + State = 1240; _localctx._expression = expression(0); _localctx._partition.Add(_localctx._expression); - State = 1235; + State = 1245; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1231; + State = 1241; Match(COMMA); - State = 1232; + State = 1242; _localctx._expression = expression(0); _localctx._partition.Add(_localctx._expression); } } - State = 1237; + State = 1247; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } } } - State = 1241; + State = 1251; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ORDER) { { - State = 1240; + State = 1250; orderByClause(); } } - State = 1244; + State = 1254; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 6917529027641081857L) != 0)) { { - State = 1243; + State = 1253; windowFrame(); } } - State = 1246; + State = 1256; Match(RPAREN); } } @@ -9033,7 +9087,7 @@ public ClusteredOptionContext clusteredOption() { try { EnterOuterAlt(_localctx, 1); { - State = 1248; + State = 1258; _la = TokenStream.LA(1); if ( !(_la==CLUSTERED || _la==NONCLUSTERED) ) { ErrorHandler.RecoverInline(this); @@ -9096,7 +9150,7 @@ public WindowFrameContext windowFrame() { try { EnterOuterAlt(_localctx, 1); { - State = 1250; + State = 1260; _localctx.unit = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 6917529027641081857L) != 0)) ) { @@ -9106,18 +9160,18 @@ public WindowFrameContext windowFrame() { ErrorHandler.ReportMatch(this); Consume(); } - State = 1257; + State = 1267; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BETWEEN: { - State = 1251; + State = 1261; Match(BETWEEN); - State = 1252; + State = 1262; _localctx.start = frameBound(); - State = 1253; + State = 1263; Match(AND); - State = 1254; + State = 1264; _localctx.end = frameBound(); } break; @@ -9166,21 +9220,21 @@ public WindowFrameContext windowFrame() { case BACKTICK_ID: case IDENTIFIER: { - State = 1256; + State = 1266; _localctx.start = frameBound(); } break; default: throw new NoViableAltException(this); } - State = 1261; + State = 1271; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==EXCLUDE) { { - State = 1259; + State = 1269; Match(EXCLUDE); - State = 1260; + State = 1270; _localctx.exclusion = frameExclusion(); } } @@ -9228,15 +9282,15 @@ public FrameBoundContext frameBound() { EnterRule(_localctx, 142, RULE_frameBound); int _la; try { - State = 1270; + State = 1280; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,161,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,164,Context) ) { case 1: EnterOuterAlt(_localctx, 1); { - State = 1263; + State = 1273; Match(UNBOUNDED); - State = 1264; + State = 1274; _localctx.direction = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PRECEDING || _la==FOLLOWING) ) { @@ -9251,18 +9305,18 @@ public FrameBoundContext frameBound() { case 2: EnterOuterAlt(_localctx, 2); { - State = 1265; + State = 1275; Match(CURRENT); - State = 1266; + State = 1276; Match(ROW); } break; case 3: EnterOuterAlt(_localctx, 3); { - State = 1267; + State = 1277; _localctx.offset = expression(0); - State = 1268; + State = 1278; _localctx.direction = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PRECEDING || _la==FOLLOWING) ) { @@ -9312,38 +9366,38 @@ public FrameExclusionContext frameExclusion() { FrameExclusionContext _localctx = new FrameExclusionContext(Context, State); EnterRule(_localctx, 144, RULE_frameExclusion); try { - State = 1278; + State = 1288; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case CURRENT: EnterOuterAlt(_localctx, 1); { - State = 1272; + State = 1282; Match(CURRENT); - State = 1273; + State = 1283; Match(ROW); } break; case GROUP: EnterOuterAlt(_localctx, 2); { - State = 1274; + State = 1284; Match(GROUP); } break; case TIES: EnterOuterAlt(_localctx, 3); { - State = 1275; + State = 1285; Match(TIES); } break; case NO: EnterOuterAlt(_localctx, 4); { - State = 1276; + State = 1286; Match(NO); - State = 1277; + State = 1287; Match(OTHERS); } break; @@ -9400,7 +9454,7 @@ public NonReservedKeywordContext nonReservedKeyword() { try { EnterOuterAlt(_localctx, 1); { - State = 1280; + State = 1290; _la = TokenStream.LA(1); if ( !(((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 131067L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -9551,7 +9605,7 @@ public ReservedKeywordContext reservedKeyword() { try { EnterOuterAlt(_localctx, 1); { - State = 1282; + State = 1292; _la = TokenStream.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & 43980465111039L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -9602,15 +9656,15 @@ public FilterClauseContext filterClause() { try { EnterOuterAlt(_localctx, 1); { - State = 1284; + State = 1294; Match(FILTER); - State = 1285; + State = 1295; Match(LPAREN); - State = 1286; + State = 1296; Match(WHERE); - State = 1287; + State = 1297; _localctx.condition = expression(0); - State = 1288; + State = 1298; Match(RPAREN); } } @@ -9653,15 +9707,15 @@ public WithinGroupContext withinGroup() { try { EnterOuterAlt(_localctx, 1); { - State = 1290; + State = 1300; Match(WITHIN); - State = 1291; + State = 1301; Match(GROUP); - State = 1292; + State = 1302; Match(LPAREN); - State = 1293; + State = 1303; orderByClause(); - State = 1294; + State = 1304; Match(RPAREN); } } @@ -9702,9 +9756,9 @@ public NthRowFromContext nthRowFrom() { try { EnterOuterAlt(_localctx, 1); { - State = 1296; + State = 1306; Match(FROM); - State = 1297; + State = 1307; _localctx.edge = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==FIRST || _la==LAST) ) { @@ -9753,7 +9807,7 @@ public NullTreatmentContext nullTreatment() { try { EnterOuterAlt(_localctx, 1); { - State = 1299; + State = 1309; _localctx.treatment = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==IGNORE || _la==RESPECT) ) { @@ -9763,7 +9817,7 @@ public NullTreatmentContext nullTreatment() { ErrorHandler.ReportMatch(this); Consume(); } - State = 1300; + State = 1310; Match(NULLS); } } @@ -9810,7 +9864,7 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { } private static int[] _serializedATN = { - 4,1,156,1303,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,156,1313,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, @@ -9876,70 +9930,71 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { 8,38,1,38,3,38,814,8,38,1,38,3,38,817,8,38,1,38,3,38,820,8,38,1,39,1,39, 1,40,1,40,1,40,1,40,1,40,5,40,829,8,40,10,40,12,40,832,9,40,1,41,1,41, 1,41,1,42,1,42,1,42,1,42,5,42,841,8,42,10,42,12,42,844,9,42,1,42,3,42, - 847,8,42,1,43,1,43,1,43,1,43,1,43,1,43,3,43,855,8,43,1,44,1,44,1,44,1, - 44,1,44,1,44,1,44,1,44,1,44,3,44,866,8,44,1,44,1,44,1,44,1,44,1,44,1,44, - 3,44,874,8,44,1,45,1,45,1,46,1,46,1,46,1,46,5,46,882,8,46,10,46,12,46, - 885,9,46,3,46,887,8,46,1,47,1,47,1,47,1,47,1,47,1,47,3,47,895,8,47,1,47, - 3,47,898,8,47,3,47,900,8,47,1,48,1,48,1,48,1,48,5,48,906,8,48,10,48,12, - 48,909,9,48,1,49,1,49,5,49,913,8,49,10,49,12,49,916,9,49,1,50,1,50,3,50, - 920,8,50,1,50,3,50,923,8,50,1,50,1,50,1,50,1,50,3,50,929,8,50,1,50,3,50, - 932,8,50,1,50,1,50,1,50,1,50,3,50,938,8,50,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,1,51,3,51,955,8,51,1,52,3,52, - 958,8,52,1,52,1,52,3,52,962,8,52,1,52,1,52,3,52,966,8,52,1,52,1,52,3,52, - 970,8,52,3,52,972,8,52,1,53,1,53,1,53,1,54,1,54,1,54,1,54,1,54,5,54,982, - 8,54,10,54,12,54,985,9,54,1,55,1,55,3,55,989,8,55,1,56,1,56,1,56,1,56, - 1,56,1,56,3,56,997,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3, - 56,1022,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1031,8,56,1,56,1, - 56,1,56,1,56,1,56,3,56,1038,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1060, - 8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1069,8,56,1,56,1,56,1,56, - 1,56,1,56,5,56,1076,8,56,10,56,12,56,1079,9,56,1,56,1,56,1,56,1,56,1,56, - 3,56,1086,8,56,1,56,1,56,1,56,1,56,3,56,1092,8,56,1,56,5,56,1095,8,56, - 10,56,12,56,1098,9,56,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, - 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,3,57,1119,8,57,1,58,1,58, - 3,58,1123,8,58,1,58,4,58,1126,8,58,11,58,12,58,1127,1,58,1,58,3,58,1132, - 8,58,1,58,1,58,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60,1,60,3,60,1145, - 8,60,1,60,1,60,1,60,5,60,1150,8,60,10,60,12,60,1153,9,60,3,60,1155,8,60, - 1,60,1,60,3,60,1159,8,60,1,60,3,60,1162,8,60,1,60,3,60,1165,8,60,1,60, - 3,60,1168,8,60,1,60,1,60,3,60,1172,8,60,1,61,1,61,1,61,1,61,1,61,1,61, - 3,61,1180,8,61,1,62,1,62,1,62,5,62,1185,8,62,10,62,12,62,1188,9,62,1,63, - 1,63,3,63,1192,8,63,1,64,1,64,1,64,1,64,3,64,1198,8,64,1,65,1,65,1,65, - 1,65,1,65,1,65,1,65,1,65,1,65,3,65,1209,8,65,1,66,1,66,3,66,1213,8,66, - 1,66,1,66,3,66,1217,8,66,1,66,1,66,3,66,1221,8,66,3,66,1223,8,66,1,67, - 1,67,1,67,1,68,1,68,1,68,1,68,1,68,1,68,5,68,1234,8,68,10,68,12,68,1237, - 9,68,3,68,1239,8,68,1,68,3,68,1242,8,68,1,68,3,68,1245,8,68,1,68,1,68, - 1,69,1,69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,3,70,1258,8,70,1,70,1,70, - 3,70,1262,8,70,1,71,1,71,1,71,1,71,1,71,1,71,1,71,3,71,1271,8,71,1,72, - 1,72,1,72,1,72,1,72,1,72,3,72,1279,8,72,1,73,1,73,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,76,1,77,1,77,1,77,1,78, - 1,78,1,78,1,78,0,1,112,79,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,120, - 122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156, - 0,26,1,0,101,102,1,0,103,104,1,0,93,94,1,0,137,138,2,0,34,35,54,54,1,0, - 128,129,1,0,47,48,1,0,49,50,2,0,8,8,15,15,1,0,124,125,1,0,131,136,2,0, - 6,6,12,12,2,0,7,7,13,13,2,0,9,9,14,14,1,0,105,106,1,0,140,141,3,0,25,25, - 105,105,107,107,3,0,85,85,106,106,108,108,1,0,62,63,2,0,87,87,89,89,2, - 0,49,49,110,111,1,0,113,114,2,0,107,108,110,123,2,0,1,106,109,109,2,0, - 48,48,120,120,2,0,96,96,121,121,1485,0,159,1,0,0,0,2,182,1,0,0,0,4,204, - 1,0,0,0,6,206,1,0,0,0,8,218,1,0,0,0,10,239,1,0,0,0,12,243,1,0,0,0,14,263, - 1,0,0,0,16,272,1,0,0,0,18,279,1,0,0,0,20,290,1,0,0,0,22,314,1,0,0,0,24, - 333,1,0,0,0,26,361,1,0,0,0,28,363,1,0,0,0,30,368,1,0,0,0,32,370,1,0,0, - 0,34,432,1,0,0,0,36,439,1,0,0,0,38,441,1,0,0,0,40,478,1,0,0,0,42,480,1, - 0,0,0,44,489,1,0,0,0,46,491,1,0,0,0,48,502,1,0,0,0,50,509,1,0,0,0,52,528, - 1,0,0,0,54,601,1,0,0,0,56,692,1,0,0,0,58,701,1,0,0,0,60,710,1,0,0,0,62, - 720,1,0,0,0,64,722,1,0,0,0,66,743,1,0,0,0,68,756,1,0,0,0,70,758,1,0,0, - 0,72,787,1,0,0,0,74,795,1,0,0,0,76,797,1,0,0,0,78,821,1,0,0,0,80,823,1, - 0,0,0,82,833,1,0,0,0,84,836,1,0,0,0,86,854,1,0,0,0,88,873,1,0,0,0,90,875, - 1,0,0,0,92,886,1,0,0,0,94,899,1,0,0,0,96,901,1,0,0,0,98,910,1,0,0,0,100, - 937,1,0,0,0,102,954,1,0,0,0,104,971,1,0,0,0,106,973,1,0,0,0,108,976,1, - 0,0,0,110,986,1,0,0,0,112,996,1,0,0,0,114,1118,1,0,0,0,116,1120,1,0,0, - 0,118,1135,1,0,0,0,120,1140,1,0,0,0,122,1179,1,0,0,0,124,1181,1,0,0,0, - 126,1191,1,0,0,0,128,1197,1,0,0,0,130,1208,1,0,0,0,132,1222,1,0,0,0,134, - 1224,1,0,0,0,136,1227,1,0,0,0,138,1248,1,0,0,0,140,1250,1,0,0,0,142,1270, - 1,0,0,0,144,1278,1,0,0,0,146,1280,1,0,0,0,148,1282,1,0,0,0,150,1284,1, - 0,0,0,152,1290,1,0,0,0,154,1296,1,0,0,0,156,1299,1,0,0,0,158,160,3,18, + 847,8,42,1,42,1,42,3,42,851,8,42,1,43,1,43,1,43,1,43,1,43,1,43,3,43,859, + 8,43,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,871,8,44,3, + 44,873,8,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,882,8,44,3,44,884, + 8,44,1,45,1,45,1,46,1,46,1,46,1,46,5,46,892,8,46,10,46,12,46,895,9,46, + 3,46,897,8,46,1,47,1,47,1,47,1,47,1,47,1,47,3,47,905,8,47,1,47,3,47,908, + 8,47,3,47,910,8,47,1,48,1,48,1,48,1,48,5,48,916,8,48,10,48,12,48,919,9, + 48,1,49,1,49,5,49,923,8,49,10,49,12,49,926,9,49,1,50,1,50,3,50,930,8,50, + 1,50,3,50,933,8,50,1,50,1,50,1,50,1,50,3,50,939,8,50,1,50,3,50,942,8,50, + 1,50,1,50,1,50,1,50,3,50,948,8,50,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,1,51,3,51,965,8,51,1,52,3,52,968,8,52, + 1,52,1,52,3,52,972,8,52,1,52,1,52,3,52,976,8,52,1,52,1,52,3,52,980,8,52, + 3,52,982,8,52,1,53,1,53,1,53,1,54,1,54,1,54,1,54,1,54,5,54,992,8,54,10, + 54,12,54,995,9,54,1,55,1,55,3,55,999,8,55,1,56,1,56,1,56,1,56,1,56,1,56, + 3,56,1007,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56, + 1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1032, + 8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1041,8,56,1,56,1,56,1,56, + 1,56,1,56,3,56,1048,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56, + 1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1070,8,56, + 1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1079,8,56,1,56,1,56,1,56,1,56, + 1,56,5,56,1086,8,56,10,56,12,56,1089,9,56,1,56,1,56,1,56,1,56,1,56,3,56, + 1096,8,56,1,56,1,56,1,56,1,56,3,56,1102,8,56,1,56,5,56,1105,8,56,10,56, + 12,56,1108,9,56,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, + 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,3,57,1129,8,57,1,58,1,58,3,58, + 1133,8,58,1,58,4,58,1136,8,58,11,58,12,58,1137,1,58,1,58,3,58,1142,8,58, + 1,58,1,58,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60,1,60,3,60,1155,8,60, + 1,60,1,60,1,60,5,60,1160,8,60,10,60,12,60,1163,9,60,3,60,1165,8,60,1,60, + 1,60,3,60,1169,8,60,1,60,3,60,1172,8,60,1,60,3,60,1175,8,60,1,60,3,60, + 1178,8,60,1,60,1,60,3,60,1182,8,60,1,61,1,61,1,61,1,61,1,61,1,61,3,61, + 1190,8,61,1,62,1,62,1,62,5,62,1195,8,62,10,62,12,62,1198,9,62,1,63,1,63, + 3,63,1202,8,63,1,64,1,64,1,64,1,64,3,64,1208,8,64,1,65,1,65,1,65,1,65, + 1,65,1,65,1,65,1,65,1,65,3,65,1219,8,65,1,66,1,66,3,66,1223,8,66,1,66, + 1,66,3,66,1227,8,66,1,66,1,66,3,66,1231,8,66,3,66,1233,8,66,1,67,1,67, + 1,67,1,68,1,68,1,68,1,68,1,68,1,68,5,68,1244,8,68,10,68,12,68,1247,9,68, + 3,68,1249,8,68,1,68,3,68,1252,8,68,1,68,3,68,1255,8,68,1,68,1,68,1,69, + 1,69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,3,70,1268,8,70,1,70,1,70,3,70, + 1272,8,70,1,71,1,71,1,71,1,71,1,71,1,71,1,71,3,71,1281,8,71,1,72,1,72, + 1,72,1,72,1,72,1,72,3,72,1289,8,72,1,73,1,73,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,76,1,77,1,77,1,77,1,78,1,78, + 1,78,1,78,0,1,112,79,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,120,122, + 124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,0, + 26,1,0,101,102,1,0,103,104,1,0,93,94,1,0,137,138,2,0,34,35,54,54,1,0,128, + 129,1,0,47,48,1,0,49,50,2,0,8,8,15,15,1,0,124,125,1,0,131,136,2,0,6,6, + 12,12,2,0,7,7,13,13,2,0,9,9,14,14,1,0,105,106,1,0,140,141,3,0,25,25,105, + 105,107,107,3,0,85,85,106,106,108,108,1,0,62,63,2,0,87,87,89,89,2,0,49, + 49,110,111,1,0,113,114,2,0,107,108,110,123,2,0,1,106,109,109,2,0,48,48, + 120,120,2,0,96,96,121,121,1498,0,159,1,0,0,0,2,182,1,0,0,0,4,204,1,0,0, + 0,6,206,1,0,0,0,8,218,1,0,0,0,10,239,1,0,0,0,12,243,1,0,0,0,14,263,1,0, + 0,0,16,272,1,0,0,0,18,279,1,0,0,0,20,290,1,0,0,0,22,314,1,0,0,0,24,333, + 1,0,0,0,26,361,1,0,0,0,28,363,1,0,0,0,30,368,1,0,0,0,32,370,1,0,0,0,34, + 432,1,0,0,0,36,439,1,0,0,0,38,441,1,0,0,0,40,478,1,0,0,0,42,480,1,0,0, + 0,44,489,1,0,0,0,46,491,1,0,0,0,48,502,1,0,0,0,50,509,1,0,0,0,52,528,1, + 0,0,0,54,601,1,0,0,0,56,692,1,0,0,0,58,701,1,0,0,0,60,710,1,0,0,0,62,720, + 1,0,0,0,64,722,1,0,0,0,66,743,1,0,0,0,68,756,1,0,0,0,70,758,1,0,0,0,72, + 787,1,0,0,0,74,795,1,0,0,0,76,797,1,0,0,0,78,821,1,0,0,0,80,823,1,0,0, + 0,82,833,1,0,0,0,84,836,1,0,0,0,86,858,1,0,0,0,88,883,1,0,0,0,90,885,1, + 0,0,0,92,896,1,0,0,0,94,909,1,0,0,0,96,911,1,0,0,0,98,920,1,0,0,0,100, + 947,1,0,0,0,102,964,1,0,0,0,104,981,1,0,0,0,106,983,1,0,0,0,108,986,1, + 0,0,0,110,996,1,0,0,0,112,1006,1,0,0,0,114,1128,1,0,0,0,116,1130,1,0,0, + 0,118,1145,1,0,0,0,120,1150,1,0,0,0,122,1189,1,0,0,0,124,1191,1,0,0,0, + 126,1201,1,0,0,0,128,1207,1,0,0,0,130,1218,1,0,0,0,132,1232,1,0,0,0,134, + 1234,1,0,0,0,136,1237,1,0,0,0,138,1258,1,0,0,0,140,1260,1,0,0,0,142,1280, + 1,0,0,0,144,1288,1,0,0,0,146,1290,1,0,0,0,148,1292,1,0,0,0,150,1294,1, + 0,0,0,152,1300,1,0,0,0,154,1306,1,0,0,0,156,1309,1,0,0,0,158,160,3,18, 9,0,159,158,1,0,0,0,159,160,1,0,0,0,160,175,1,0,0,0,161,176,3,2,1,0,162, 176,3,20,10,0,163,176,3,38,19,0,164,176,3,22,11,0,165,176,3,24,12,0,166, 176,3,32,16,0,167,176,3,40,20,0,168,176,3,64,32,0,169,176,3,8,4,0,170, @@ -10137,161 +10192,165 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { 1,0,0,0,833,834,5,30,0,0,834,835,3,112,56,0,835,83,1,0,0,0,836,837,5,4, 0,0,837,842,3,86,43,0,838,839,7,5,0,0,839,841,3,86,43,0,840,838,1,0,0, 0,841,844,1,0,0,0,842,840,1,0,0,0,842,843,1,0,0,0,843,846,1,0,0,0,844, - 842,1,0,0,0,845,847,5,36,0,0,846,845,1,0,0,0,846,847,1,0,0,0,847,85,1, - 0,0,0,848,855,5,146,0,0,849,855,5,144,0,0,850,851,5,137,0,0,851,852,3, - 112,56,0,852,853,5,138,0,0,853,855,1,0,0,0,854,848,1,0,0,0,854,849,1,0, - 0,0,854,850,1,0,0,0,855,87,1,0,0,0,856,857,5,45,0,0,857,858,3,112,56,0, - 858,865,3,90,45,0,859,860,5,46,0,0,860,861,7,6,0,0,861,862,3,112,56,0, - 862,863,3,90,45,0,863,864,5,51,0,0,864,866,1,0,0,0,865,859,1,0,0,0,865, - 866,1,0,0,0,866,874,1,0,0,0,867,868,5,46,0,0,868,869,7,6,0,0,869,870,3, - 112,56,0,870,871,3,90,45,0,871,872,5,51,0,0,872,874,1,0,0,0,873,856,1, - 0,0,0,873,867,1,0,0,0,874,89,1,0,0,0,875,876,7,7,0,0,876,91,1,0,0,0,877, - 887,5,124,0,0,878,883,3,94,47,0,879,880,5,139,0,0,880,882,3,94,47,0,881, - 879,1,0,0,0,882,885,1,0,0,0,883,881,1,0,0,0,883,884,1,0,0,0,884,887,1, - 0,0,0,885,883,1,0,0,0,886,877,1,0,0,0,886,878,1,0,0,0,887,93,1,0,0,0,888, - 889,3,128,64,0,889,890,5,140,0,0,890,891,5,124,0,0,891,900,1,0,0,0,892, - 897,3,112,56,0,893,895,5,5,0,0,894,893,1,0,0,0,894,895,1,0,0,0,895,896, - 1,0,0,0,896,898,3,128,64,0,897,894,1,0,0,0,897,898,1,0,0,0,898,900,1,0, - 0,0,899,888,1,0,0,0,899,892,1,0,0,0,900,95,1,0,0,0,901,902,5,2,0,0,902, - 907,3,98,49,0,903,904,5,139,0,0,904,906,3,98,49,0,905,903,1,0,0,0,906, - 909,1,0,0,0,907,905,1,0,0,0,907,908,1,0,0,0,908,97,1,0,0,0,909,907,1,0, - 0,0,910,914,3,100,50,0,911,913,3,102,51,0,912,911,1,0,0,0,913,916,1,0, - 0,0,914,912,1,0,0,0,914,915,1,0,0,0,915,99,1,0,0,0,916,914,1,0,0,0,917, - 922,3,128,64,0,918,920,5,5,0,0,919,918,1,0,0,0,919,920,1,0,0,0,920,921, - 1,0,0,0,921,923,3,128,64,0,922,919,1,0,0,0,922,923,1,0,0,0,923,938,1,0, - 0,0,924,925,5,137,0,0,925,926,3,70,35,0,926,931,5,138,0,0,927,929,5,5, - 0,0,928,927,1,0,0,0,928,929,1,0,0,0,929,930,1,0,0,0,930,932,3,128,64,0, - 931,928,1,0,0,0,931,932,1,0,0,0,932,938,1,0,0,0,933,934,5,137,0,0,934, - 935,3,98,49,0,935,936,5,138,0,0,936,938,1,0,0,0,937,917,1,0,0,0,937,924, - 1,0,0,0,937,933,1,0,0,0,938,101,1,0,0,0,939,940,3,104,52,0,940,941,5,23, - 0,0,941,942,3,100,50,0,942,943,5,25,0,0,943,944,3,112,56,0,944,955,1,0, - 0,0,945,946,5,37,0,0,946,947,5,23,0,0,947,955,3,100,50,0,948,949,5,37, - 0,0,949,950,5,38,0,0,950,955,3,100,50,0,951,952,5,22,0,0,952,953,5,38, - 0,0,953,955,3,100,50,0,954,939,1,0,0,0,954,945,1,0,0,0,954,948,1,0,0,0, - 954,951,1,0,0,0,955,103,1,0,0,0,956,958,5,18,0,0,957,956,1,0,0,0,957,958, - 1,0,0,0,958,972,1,0,0,0,959,961,5,19,0,0,960,962,5,22,0,0,961,960,1,0, - 0,0,961,962,1,0,0,0,962,972,1,0,0,0,963,965,5,20,0,0,964,966,5,22,0,0, - 965,964,1,0,0,0,965,966,1,0,0,0,966,972,1,0,0,0,967,969,5,21,0,0,968,970, - 5,22,0,0,969,968,1,0,0,0,969,970,1,0,0,0,970,972,1,0,0,0,971,957,1,0,0, - 0,971,959,1,0,0,0,971,963,1,0,0,0,971,967,1,0,0,0,972,105,1,0,0,0,973, - 974,5,3,0,0,974,975,3,112,56,0,975,107,1,0,0,0,976,977,5,26,0,0,977,978, - 5,29,0,0,978,983,3,110,55,0,979,980,5,139,0,0,980,982,3,110,55,0,981,979, - 1,0,0,0,982,985,1,0,0,0,983,981,1,0,0,0,983,984,1,0,0,0,984,109,1,0,0, - 0,985,983,1,0,0,0,986,988,3,112,56,0,987,989,7,1,0,0,988,987,1,0,0,0,988, - 989,1,0,0,0,989,111,1,0,0,0,990,991,6,56,-1,0,991,992,7,5,0,0,992,997, - 3,112,56,21,993,994,7,8,0,0,994,997,3,112,56,7,995,997,3,114,57,0,996, - 990,1,0,0,0,996,993,1,0,0,0,996,995,1,0,0,0,997,1096,1,0,0,0,998,999,10, - 22,0,0,999,1000,5,127,0,0,1000,1095,3,112,56,23,1001,1002,10,20,0,0,1002, - 1003,7,9,0,0,1003,1095,3,112,56,21,1004,1005,10,19,0,0,1005,1006,5,126, - 0,0,1006,1095,3,112,56,20,1007,1008,10,18,0,0,1008,1009,5,17,0,0,1009, - 1095,3,112,56,19,1010,1011,10,17,0,0,1011,1012,7,5,0,0,1012,1095,3,112, - 56,18,1013,1014,10,16,0,0,1014,1015,5,130,0,0,1015,1095,3,112,56,17,1016, - 1017,10,15,0,0,1017,1018,7,10,0,0,1018,1095,3,112,56,16,1019,1021,10,14, - 0,0,1020,1022,5,8,0,0,1021,1020,1,0,0,0,1021,1022,1,0,0,0,1022,1023,1, - 0,0,0,1023,1024,5,52,0,0,1024,1025,3,112,56,0,1025,1026,5,6,0,0,1026,1027, - 3,112,56,15,1027,1095,1,0,0,0,1028,1030,10,13,0,0,1029,1031,5,8,0,0,1030, - 1029,1,0,0,0,1030,1031,1,0,0,0,1031,1032,1,0,0,0,1032,1033,5,16,0,0,1033, - 1095,3,112,56,14,1034,1035,10,8,0,0,1035,1037,5,28,0,0,1036,1038,5,8,0, - 0,1037,1036,1,0,0,0,1037,1038,1,0,0,0,1038,1039,1,0,0,0,1039,1040,5,35, - 0,0,1040,1041,5,2,0,0,1041,1095,3,112,56,9,1042,1043,10,6,0,0,1043,1044, - 7,11,0,0,1044,1095,3,112,56,7,1045,1046,10,5,0,0,1046,1047,7,12,0,0,1047, - 1095,3,112,56,6,1048,1049,10,4,0,0,1049,1050,7,13,0,0,1050,1095,3,112, - 56,5,1051,1052,10,3,0,0,1052,1053,5,10,0,0,1053,1095,3,112,56,4,1054,1055, - 10,2,0,0,1055,1056,5,11,0,0,1056,1095,3,112,56,3,1057,1059,10,12,0,0,1058, - 1060,5,8,0,0,1059,1058,1,0,0,0,1059,1060,1,0,0,0,1060,1061,1,0,0,0,1061, - 1062,5,24,0,0,1062,1063,5,137,0,0,1063,1064,3,70,35,0,1064,1065,5,138, - 0,0,1065,1095,1,0,0,0,1066,1068,10,11,0,0,1067,1069,5,8,0,0,1068,1067, - 1,0,0,0,1068,1069,1,0,0,0,1069,1070,1,0,0,0,1070,1071,5,24,0,0,1071,1072, - 5,137,0,0,1072,1077,3,112,56,0,1073,1074,5,139,0,0,1074,1076,3,112,56, - 0,1075,1073,1,0,0,0,1076,1079,1,0,0,0,1077,1075,1,0,0,0,1077,1078,1,0, - 0,0,1078,1080,1,0,0,0,1079,1077,1,0,0,0,1080,1081,5,138,0,0,1081,1095, - 1,0,0,0,1082,1083,10,10,0,0,1083,1085,5,28,0,0,1084,1086,5,8,0,0,1085, - 1084,1,0,0,0,1085,1086,1,0,0,0,1086,1087,1,0,0,0,1087,1095,5,109,0,0,1088, - 1089,10,9,0,0,1089,1091,5,28,0,0,1090,1092,5,8,0,0,1091,1090,1,0,0,0,1091, - 1092,1,0,0,0,1092,1093,1,0,0,0,1093,1095,7,14,0,0,1094,998,1,0,0,0,1094, - 1001,1,0,0,0,1094,1004,1,0,0,0,1094,1007,1,0,0,0,1094,1010,1,0,0,0,1094, - 1013,1,0,0,0,1094,1016,1,0,0,0,1094,1019,1,0,0,0,1094,1028,1,0,0,0,1094, - 1034,1,0,0,0,1094,1042,1,0,0,0,1094,1045,1,0,0,0,1094,1048,1,0,0,0,1094, - 1051,1,0,0,0,1094,1054,1,0,0,0,1094,1057,1,0,0,0,1094,1066,1,0,0,0,1094, - 1082,1,0,0,0,1094,1088,1,0,0,0,1095,1098,1,0,0,0,1096,1094,1,0,0,0,1096, - 1097,1,0,0,0,1097,113,1,0,0,0,1098,1096,1,0,0,0,1099,1119,3,130,65,0,1100, - 1119,3,116,58,0,1101,1119,3,120,60,0,1102,1119,3,124,62,0,1103,1119,5, - 144,0,0,1104,1119,5,143,0,0,1105,1106,5,31,0,0,1106,1107,5,137,0,0,1107, - 1108,3,70,35,0,1108,1109,5,138,0,0,1109,1119,1,0,0,0,1110,1111,5,137,0, - 0,1111,1112,3,70,35,0,1112,1113,5,138,0,0,1113,1119,1,0,0,0,1114,1115, - 5,137,0,0,1115,1116,3,112,56,0,1116,1117,5,138,0,0,1117,1119,1,0,0,0,1118, - 1099,1,0,0,0,1118,1100,1,0,0,0,1118,1101,1,0,0,0,1118,1102,1,0,0,0,1118, - 1103,1,0,0,0,1118,1104,1,0,0,0,1118,1105,1,0,0,0,1118,1110,1,0,0,0,1118, - 1114,1,0,0,0,1119,115,1,0,0,0,1120,1122,5,41,0,0,1121,1123,3,112,56,0, - 1122,1121,1,0,0,0,1122,1123,1,0,0,0,1123,1125,1,0,0,0,1124,1126,3,118, - 59,0,1125,1124,1,0,0,0,1126,1127,1,0,0,0,1127,1125,1,0,0,0,1127,1128,1, - 0,0,0,1128,1131,1,0,0,0,1129,1130,5,43,0,0,1130,1132,3,112,56,0,1131,1129, - 1,0,0,0,1131,1132,1,0,0,0,1132,1133,1,0,0,0,1133,1134,5,44,0,0,1134,117, - 1,0,0,0,1135,1136,5,42,0,0,1136,1137,3,112,56,0,1137,1138,5,33,0,0,1138, - 1139,3,112,56,0,1139,119,1,0,0,0,1140,1141,3,122,61,0,1141,1154,5,137, - 0,0,1142,1155,5,124,0,0,1143,1145,5,35,0,0,1144,1143,1,0,0,0,1144,1145, - 1,0,0,0,1145,1146,1,0,0,0,1146,1151,3,112,56,0,1147,1148,5,139,0,0,1148, - 1150,3,112,56,0,1149,1147,1,0,0,0,1150,1153,1,0,0,0,1151,1149,1,0,0,0, - 1151,1152,1,0,0,0,1152,1155,1,0,0,0,1153,1151,1,0,0,0,1154,1142,1,0,0, - 0,1154,1144,1,0,0,0,1154,1155,1,0,0,0,1155,1156,1,0,0,0,1156,1158,5,138, - 0,0,1157,1159,3,152,76,0,1158,1157,1,0,0,0,1158,1159,1,0,0,0,1159,1161, - 1,0,0,0,1160,1162,3,150,75,0,1161,1160,1,0,0,0,1161,1162,1,0,0,0,1162, - 1171,1,0,0,0,1163,1165,3,154,77,0,1164,1163,1,0,0,0,1164,1165,1,0,0,0, - 1165,1167,1,0,0,0,1166,1168,3,156,78,0,1167,1166,1,0,0,0,1167,1168,1,0, - 0,0,1168,1169,1,0,0,0,1169,1170,5,39,0,0,1170,1172,3,136,68,0,1171,1164, - 1,0,0,0,1171,1172,1,0,0,0,1172,121,1,0,0,0,1173,1180,3,128,64,0,1174,1180, - 5,19,0,0,1175,1180,5,20,0,0,1176,1180,5,103,0,0,1177,1180,5,48,0,0,1178, - 1180,5,40,0,0,1179,1173,1,0,0,0,1179,1174,1,0,0,0,1179,1175,1,0,0,0,1179, - 1176,1,0,0,0,1179,1177,1,0,0,0,1179,1178,1,0,0,0,1180,123,1,0,0,0,1181, - 1186,3,128,64,0,1182,1183,7,15,0,0,1183,1185,3,126,63,0,1184,1182,1,0, - 0,0,1185,1188,1,0,0,0,1186,1184,1,0,0,0,1186,1187,1,0,0,0,1187,125,1,0, - 0,0,1188,1186,1,0,0,0,1189,1192,3,128,64,0,1190,1192,3,148,74,0,1191,1189, - 1,0,0,0,1191,1190,1,0,0,0,1192,127,1,0,0,0,1193,1198,5,153,0,0,1194,1198, - 5,151,0,0,1195,1198,5,152,0,0,1196,1198,3,146,73,0,1197,1193,1,0,0,0,1197, - 1194,1,0,0,0,1197,1195,1,0,0,0,1197,1196,1,0,0,0,1198,129,1,0,0,0,1199, - 1209,5,146,0,0,1200,1209,5,147,0,0,1201,1209,5,145,0,0,1202,1209,5,148, - 0,0,1203,1209,5,149,0,0,1204,1209,5,150,0,0,1205,1209,7,16,0,0,1206,1209, - 7,17,0,0,1207,1209,5,109,0,0,1208,1199,1,0,0,0,1208,1200,1,0,0,0,1208, - 1201,1,0,0,0,1208,1202,1,0,0,0,1208,1203,1,0,0,0,1208,1204,1,0,0,0,1208, - 1205,1,0,0,0,1208,1206,1,0,0,0,1208,1207,1,0,0,0,1209,131,1,0,0,0,1210, - 1212,5,59,0,0,1211,1213,7,18,0,0,1212,1211,1,0,0,0,1212,1213,1,0,0,0,1213, - 1223,1,0,0,0,1214,1216,5,60,0,0,1215,1217,7,18,0,0,1216,1215,1,0,0,0,1216, - 1217,1,0,0,0,1217,1223,1,0,0,0,1218,1220,5,61,0,0,1219,1221,7,18,0,0,1220, - 1219,1,0,0,0,1220,1221,1,0,0,0,1221,1223,1,0,0,0,1222,1210,1,0,0,0,1222, - 1214,1,0,0,0,1222,1218,1,0,0,0,1223,133,1,0,0,0,1224,1225,3,112,56,0,1225, - 1226,5,0,0,1,1226,135,1,0,0,0,1227,1238,5,137,0,0,1228,1229,5,40,0,0,1229, - 1230,5,29,0,0,1230,1235,3,112,56,0,1231,1232,5,139,0,0,1232,1234,3,112, - 56,0,1233,1231,1,0,0,0,1234,1237,1,0,0,0,1235,1233,1,0,0,0,1235,1236,1, - 0,0,0,1236,1239,1,0,0,0,1237,1235,1,0,0,0,1238,1228,1,0,0,0,1238,1239, - 1,0,0,0,1239,1241,1,0,0,0,1240,1242,3,108,54,0,1241,1240,1,0,0,0,1241, - 1242,1,0,0,0,1242,1244,1,0,0,0,1243,1245,3,140,70,0,1244,1243,1,0,0,0, - 1244,1245,1,0,0,0,1245,1246,1,0,0,0,1246,1247,5,138,0,0,1247,137,1,0,0, - 0,1248,1249,7,19,0,0,1249,139,1,0,0,0,1250,1257,7,20,0,0,1251,1252,5,52, - 0,0,1252,1253,3,142,71,0,1253,1254,5,6,0,0,1254,1255,3,142,71,0,1255,1258, - 1,0,0,0,1256,1258,3,142,71,0,1257,1251,1,0,0,0,1257,1256,1,0,0,0,1258, - 1261,1,0,0,0,1259,1260,5,116,0,0,1260,1262,3,144,72,0,1261,1259,1,0,0, - 0,1261,1262,1,0,0,0,1262,141,1,0,0,0,1263,1264,5,112,0,0,1264,1271,7,21, - 0,0,1265,1266,5,115,0,0,1266,1271,5,50,0,0,1267,1268,3,112,56,0,1268,1269, - 7,21,0,0,1269,1271,1,0,0,0,1270,1263,1,0,0,0,1270,1265,1,0,0,0,1270,1267, - 1,0,0,0,1271,143,1,0,0,0,1272,1273,5,115,0,0,1273,1279,5,50,0,0,1274,1279, - 5,27,0,0,1275,1279,5,117,0,0,1276,1277,5,85,0,0,1277,1279,5,118,0,0,1278, - 1272,1,0,0,0,1278,1274,1,0,0,0,1278,1275,1,0,0,0,1278,1276,1,0,0,0,1279, - 145,1,0,0,0,1280,1281,7,22,0,0,1281,147,1,0,0,0,1282,1283,7,23,0,0,1283, - 149,1,0,0,0,1284,1285,5,123,0,0,1285,1286,5,137,0,0,1286,1287,5,3,0,0, - 1287,1288,3,112,56,0,1288,1289,5,138,0,0,1289,151,1,0,0,0,1290,1291,5, - 119,0,0,1291,1292,5,27,0,0,1292,1293,5,137,0,0,1293,1294,3,108,54,0,1294, - 1295,5,138,0,0,1295,153,1,0,0,0,1296,1297,5,2,0,0,1297,1298,7,24,0,0,1298, - 155,1,0,0,0,1299,1300,7,25,0,0,1300,1301,5,122,0,0,1301,157,1,0,0,0,163, - 159,175,178,184,204,213,216,224,233,237,249,257,261,269,274,277,285,292, - 302,309,323,328,337,348,358,361,368,377,384,391,396,405,432,439,443,455, - 461,478,482,489,494,499,509,512,515,521,525,528,541,550,555,559,563,567, - 577,582,587,595,599,601,605,610,618,625,629,637,644,650,658,670,675,680, - 685,692,699,701,710,720,731,736,741,749,756,764,768,771,784,787,791,795, - 799,802,807,810,813,816,819,830,842,846,854,865,873,883,886,894,897,899, - 907,914,919,922,928,931,937,954,957,961,965,969,971,983,988,996,1021,1030, - 1037,1059,1068,1077,1085,1091,1094,1096,1118,1122,1127,1131,1144,1151, - 1154,1158,1161,1164,1167,1171,1179,1186,1191,1197,1208,1212,1216,1220, - 1222,1235,1238,1241,1244,1257,1261,1270,1278 + 842,1,0,0,0,845,847,5,36,0,0,846,845,1,0,0,0,846,847,1,0,0,0,847,850,1, + 0,0,0,848,849,5,92,0,0,849,851,5,117,0,0,850,848,1,0,0,0,850,851,1,0,0, + 0,851,85,1,0,0,0,852,859,5,146,0,0,853,859,5,144,0,0,854,855,5,137,0,0, + 855,856,3,112,56,0,856,857,5,138,0,0,857,859,1,0,0,0,858,852,1,0,0,0,858, + 853,1,0,0,0,858,854,1,0,0,0,859,87,1,0,0,0,860,861,5,45,0,0,861,862,3, + 112,56,0,862,872,3,90,45,0,863,864,5,46,0,0,864,865,7,6,0,0,865,866,3, + 112,56,0,866,870,3,90,45,0,867,871,5,51,0,0,868,869,5,92,0,0,869,871,5, + 117,0,0,870,867,1,0,0,0,870,868,1,0,0,0,871,873,1,0,0,0,872,863,1,0,0, + 0,872,873,1,0,0,0,873,884,1,0,0,0,874,875,5,46,0,0,875,876,7,6,0,0,876, + 877,3,112,56,0,877,881,3,90,45,0,878,882,5,51,0,0,879,880,5,92,0,0,880, + 882,5,117,0,0,881,878,1,0,0,0,881,879,1,0,0,0,882,884,1,0,0,0,883,860, + 1,0,0,0,883,874,1,0,0,0,884,89,1,0,0,0,885,886,7,7,0,0,886,91,1,0,0,0, + 887,897,5,124,0,0,888,893,3,94,47,0,889,890,5,139,0,0,890,892,3,94,47, + 0,891,889,1,0,0,0,892,895,1,0,0,0,893,891,1,0,0,0,893,894,1,0,0,0,894, + 897,1,0,0,0,895,893,1,0,0,0,896,887,1,0,0,0,896,888,1,0,0,0,897,93,1,0, + 0,0,898,899,3,128,64,0,899,900,5,140,0,0,900,901,5,124,0,0,901,910,1,0, + 0,0,902,907,3,112,56,0,903,905,5,5,0,0,904,903,1,0,0,0,904,905,1,0,0,0, + 905,906,1,0,0,0,906,908,3,128,64,0,907,904,1,0,0,0,907,908,1,0,0,0,908, + 910,1,0,0,0,909,898,1,0,0,0,909,902,1,0,0,0,910,95,1,0,0,0,911,912,5,2, + 0,0,912,917,3,98,49,0,913,914,5,139,0,0,914,916,3,98,49,0,915,913,1,0, + 0,0,916,919,1,0,0,0,917,915,1,0,0,0,917,918,1,0,0,0,918,97,1,0,0,0,919, + 917,1,0,0,0,920,924,3,100,50,0,921,923,3,102,51,0,922,921,1,0,0,0,923, + 926,1,0,0,0,924,922,1,0,0,0,924,925,1,0,0,0,925,99,1,0,0,0,926,924,1,0, + 0,0,927,932,3,128,64,0,928,930,5,5,0,0,929,928,1,0,0,0,929,930,1,0,0,0, + 930,931,1,0,0,0,931,933,3,128,64,0,932,929,1,0,0,0,932,933,1,0,0,0,933, + 948,1,0,0,0,934,935,5,137,0,0,935,936,3,70,35,0,936,941,5,138,0,0,937, + 939,5,5,0,0,938,937,1,0,0,0,938,939,1,0,0,0,939,940,1,0,0,0,940,942,3, + 128,64,0,941,938,1,0,0,0,941,942,1,0,0,0,942,948,1,0,0,0,943,944,5,137, + 0,0,944,945,3,98,49,0,945,946,5,138,0,0,946,948,1,0,0,0,947,927,1,0,0, + 0,947,934,1,0,0,0,947,943,1,0,0,0,948,101,1,0,0,0,949,950,3,104,52,0,950, + 951,5,23,0,0,951,952,3,100,50,0,952,953,5,25,0,0,953,954,3,112,56,0,954, + 965,1,0,0,0,955,956,5,37,0,0,956,957,5,23,0,0,957,965,3,100,50,0,958,959, + 5,37,0,0,959,960,5,38,0,0,960,965,3,100,50,0,961,962,5,22,0,0,962,963, + 5,38,0,0,963,965,3,100,50,0,964,949,1,0,0,0,964,955,1,0,0,0,964,958,1, + 0,0,0,964,961,1,0,0,0,965,103,1,0,0,0,966,968,5,18,0,0,967,966,1,0,0,0, + 967,968,1,0,0,0,968,982,1,0,0,0,969,971,5,19,0,0,970,972,5,22,0,0,971, + 970,1,0,0,0,971,972,1,0,0,0,972,982,1,0,0,0,973,975,5,20,0,0,974,976,5, + 22,0,0,975,974,1,0,0,0,975,976,1,0,0,0,976,982,1,0,0,0,977,979,5,21,0, + 0,978,980,5,22,0,0,979,978,1,0,0,0,979,980,1,0,0,0,980,982,1,0,0,0,981, + 967,1,0,0,0,981,969,1,0,0,0,981,973,1,0,0,0,981,977,1,0,0,0,982,105,1, + 0,0,0,983,984,5,3,0,0,984,985,3,112,56,0,985,107,1,0,0,0,986,987,5,26, + 0,0,987,988,5,29,0,0,988,993,3,110,55,0,989,990,5,139,0,0,990,992,3,110, + 55,0,991,989,1,0,0,0,992,995,1,0,0,0,993,991,1,0,0,0,993,994,1,0,0,0,994, + 109,1,0,0,0,995,993,1,0,0,0,996,998,3,112,56,0,997,999,7,1,0,0,998,997, + 1,0,0,0,998,999,1,0,0,0,999,111,1,0,0,0,1000,1001,6,56,-1,0,1001,1002, + 7,5,0,0,1002,1007,3,112,56,21,1003,1004,7,8,0,0,1004,1007,3,112,56,7,1005, + 1007,3,114,57,0,1006,1000,1,0,0,0,1006,1003,1,0,0,0,1006,1005,1,0,0,0, + 1007,1106,1,0,0,0,1008,1009,10,22,0,0,1009,1010,5,127,0,0,1010,1105,3, + 112,56,23,1011,1012,10,20,0,0,1012,1013,7,9,0,0,1013,1105,3,112,56,21, + 1014,1015,10,19,0,0,1015,1016,5,126,0,0,1016,1105,3,112,56,20,1017,1018, + 10,18,0,0,1018,1019,5,17,0,0,1019,1105,3,112,56,19,1020,1021,10,17,0,0, + 1021,1022,7,5,0,0,1022,1105,3,112,56,18,1023,1024,10,16,0,0,1024,1025, + 5,130,0,0,1025,1105,3,112,56,17,1026,1027,10,15,0,0,1027,1028,7,10,0,0, + 1028,1105,3,112,56,16,1029,1031,10,14,0,0,1030,1032,5,8,0,0,1031,1030, + 1,0,0,0,1031,1032,1,0,0,0,1032,1033,1,0,0,0,1033,1034,5,52,0,0,1034,1035, + 3,112,56,0,1035,1036,5,6,0,0,1036,1037,3,112,56,15,1037,1105,1,0,0,0,1038, + 1040,10,13,0,0,1039,1041,5,8,0,0,1040,1039,1,0,0,0,1040,1041,1,0,0,0,1041, + 1042,1,0,0,0,1042,1043,5,16,0,0,1043,1105,3,112,56,14,1044,1045,10,8,0, + 0,1045,1047,5,28,0,0,1046,1048,5,8,0,0,1047,1046,1,0,0,0,1047,1048,1,0, + 0,0,1048,1049,1,0,0,0,1049,1050,5,35,0,0,1050,1051,5,2,0,0,1051,1105,3, + 112,56,9,1052,1053,10,6,0,0,1053,1054,7,11,0,0,1054,1105,3,112,56,7,1055, + 1056,10,5,0,0,1056,1057,7,12,0,0,1057,1105,3,112,56,6,1058,1059,10,4,0, + 0,1059,1060,7,13,0,0,1060,1105,3,112,56,5,1061,1062,10,3,0,0,1062,1063, + 5,10,0,0,1063,1105,3,112,56,4,1064,1065,10,2,0,0,1065,1066,5,11,0,0,1066, + 1105,3,112,56,3,1067,1069,10,12,0,0,1068,1070,5,8,0,0,1069,1068,1,0,0, + 0,1069,1070,1,0,0,0,1070,1071,1,0,0,0,1071,1072,5,24,0,0,1072,1073,5,137, + 0,0,1073,1074,3,70,35,0,1074,1075,5,138,0,0,1075,1105,1,0,0,0,1076,1078, + 10,11,0,0,1077,1079,5,8,0,0,1078,1077,1,0,0,0,1078,1079,1,0,0,0,1079,1080, + 1,0,0,0,1080,1081,5,24,0,0,1081,1082,5,137,0,0,1082,1087,3,112,56,0,1083, + 1084,5,139,0,0,1084,1086,3,112,56,0,1085,1083,1,0,0,0,1086,1089,1,0,0, + 0,1087,1085,1,0,0,0,1087,1088,1,0,0,0,1088,1090,1,0,0,0,1089,1087,1,0, + 0,0,1090,1091,5,138,0,0,1091,1105,1,0,0,0,1092,1093,10,10,0,0,1093,1095, + 5,28,0,0,1094,1096,5,8,0,0,1095,1094,1,0,0,0,1095,1096,1,0,0,0,1096,1097, + 1,0,0,0,1097,1105,5,109,0,0,1098,1099,10,9,0,0,1099,1101,5,28,0,0,1100, + 1102,5,8,0,0,1101,1100,1,0,0,0,1101,1102,1,0,0,0,1102,1103,1,0,0,0,1103, + 1105,7,14,0,0,1104,1008,1,0,0,0,1104,1011,1,0,0,0,1104,1014,1,0,0,0,1104, + 1017,1,0,0,0,1104,1020,1,0,0,0,1104,1023,1,0,0,0,1104,1026,1,0,0,0,1104, + 1029,1,0,0,0,1104,1038,1,0,0,0,1104,1044,1,0,0,0,1104,1052,1,0,0,0,1104, + 1055,1,0,0,0,1104,1058,1,0,0,0,1104,1061,1,0,0,0,1104,1064,1,0,0,0,1104, + 1067,1,0,0,0,1104,1076,1,0,0,0,1104,1092,1,0,0,0,1104,1098,1,0,0,0,1105, + 1108,1,0,0,0,1106,1104,1,0,0,0,1106,1107,1,0,0,0,1107,113,1,0,0,0,1108, + 1106,1,0,0,0,1109,1129,3,130,65,0,1110,1129,3,116,58,0,1111,1129,3,120, + 60,0,1112,1129,3,124,62,0,1113,1129,5,144,0,0,1114,1129,5,143,0,0,1115, + 1116,5,31,0,0,1116,1117,5,137,0,0,1117,1118,3,70,35,0,1118,1119,5,138, + 0,0,1119,1129,1,0,0,0,1120,1121,5,137,0,0,1121,1122,3,70,35,0,1122,1123, + 5,138,0,0,1123,1129,1,0,0,0,1124,1125,5,137,0,0,1125,1126,3,112,56,0,1126, + 1127,5,138,0,0,1127,1129,1,0,0,0,1128,1109,1,0,0,0,1128,1110,1,0,0,0,1128, + 1111,1,0,0,0,1128,1112,1,0,0,0,1128,1113,1,0,0,0,1128,1114,1,0,0,0,1128, + 1115,1,0,0,0,1128,1120,1,0,0,0,1128,1124,1,0,0,0,1129,115,1,0,0,0,1130, + 1132,5,41,0,0,1131,1133,3,112,56,0,1132,1131,1,0,0,0,1132,1133,1,0,0,0, + 1133,1135,1,0,0,0,1134,1136,3,118,59,0,1135,1134,1,0,0,0,1136,1137,1,0, + 0,0,1137,1135,1,0,0,0,1137,1138,1,0,0,0,1138,1141,1,0,0,0,1139,1140,5, + 43,0,0,1140,1142,3,112,56,0,1141,1139,1,0,0,0,1141,1142,1,0,0,0,1142,1143, + 1,0,0,0,1143,1144,5,44,0,0,1144,117,1,0,0,0,1145,1146,5,42,0,0,1146,1147, + 3,112,56,0,1147,1148,5,33,0,0,1148,1149,3,112,56,0,1149,119,1,0,0,0,1150, + 1151,3,122,61,0,1151,1164,5,137,0,0,1152,1165,5,124,0,0,1153,1155,5,35, + 0,0,1154,1153,1,0,0,0,1154,1155,1,0,0,0,1155,1156,1,0,0,0,1156,1161,3, + 112,56,0,1157,1158,5,139,0,0,1158,1160,3,112,56,0,1159,1157,1,0,0,0,1160, + 1163,1,0,0,0,1161,1159,1,0,0,0,1161,1162,1,0,0,0,1162,1165,1,0,0,0,1163, + 1161,1,0,0,0,1164,1152,1,0,0,0,1164,1154,1,0,0,0,1164,1165,1,0,0,0,1165, + 1166,1,0,0,0,1166,1168,5,138,0,0,1167,1169,3,152,76,0,1168,1167,1,0,0, + 0,1168,1169,1,0,0,0,1169,1171,1,0,0,0,1170,1172,3,150,75,0,1171,1170,1, + 0,0,0,1171,1172,1,0,0,0,1172,1181,1,0,0,0,1173,1175,3,154,77,0,1174,1173, + 1,0,0,0,1174,1175,1,0,0,0,1175,1177,1,0,0,0,1176,1178,3,156,78,0,1177, + 1176,1,0,0,0,1177,1178,1,0,0,0,1178,1179,1,0,0,0,1179,1180,5,39,0,0,1180, + 1182,3,136,68,0,1181,1174,1,0,0,0,1181,1182,1,0,0,0,1182,121,1,0,0,0,1183, + 1190,3,128,64,0,1184,1190,5,19,0,0,1185,1190,5,20,0,0,1186,1190,5,103, + 0,0,1187,1190,5,48,0,0,1188,1190,5,40,0,0,1189,1183,1,0,0,0,1189,1184, + 1,0,0,0,1189,1185,1,0,0,0,1189,1186,1,0,0,0,1189,1187,1,0,0,0,1189,1188, + 1,0,0,0,1190,123,1,0,0,0,1191,1196,3,128,64,0,1192,1193,7,15,0,0,1193, + 1195,3,126,63,0,1194,1192,1,0,0,0,1195,1198,1,0,0,0,1196,1194,1,0,0,0, + 1196,1197,1,0,0,0,1197,125,1,0,0,0,1198,1196,1,0,0,0,1199,1202,3,128,64, + 0,1200,1202,3,148,74,0,1201,1199,1,0,0,0,1201,1200,1,0,0,0,1202,127,1, + 0,0,0,1203,1208,5,153,0,0,1204,1208,5,151,0,0,1205,1208,5,152,0,0,1206, + 1208,3,146,73,0,1207,1203,1,0,0,0,1207,1204,1,0,0,0,1207,1205,1,0,0,0, + 1207,1206,1,0,0,0,1208,129,1,0,0,0,1209,1219,5,146,0,0,1210,1219,5,147, + 0,0,1211,1219,5,145,0,0,1212,1219,5,148,0,0,1213,1219,5,149,0,0,1214,1219, + 5,150,0,0,1215,1219,7,16,0,0,1216,1219,7,17,0,0,1217,1219,5,109,0,0,1218, + 1209,1,0,0,0,1218,1210,1,0,0,0,1218,1211,1,0,0,0,1218,1212,1,0,0,0,1218, + 1213,1,0,0,0,1218,1214,1,0,0,0,1218,1215,1,0,0,0,1218,1216,1,0,0,0,1218, + 1217,1,0,0,0,1219,131,1,0,0,0,1220,1222,5,59,0,0,1221,1223,7,18,0,0,1222, + 1221,1,0,0,0,1222,1223,1,0,0,0,1223,1233,1,0,0,0,1224,1226,5,60,0,0,1225, + 1227,7,18,0,0,1226,1225,1,0,0,0,1226,1227,1,0,0,0,1227,1233,1,0,0,0,1228, + 1230,5,61,0,0,1229,1231,7,18,0,0,1230,1229,1,0,0,0,1230,1231,1,0,0,0,1231, + 1233,1,0,0,0,1232,1220,1,0,0,0,1232,1224,1,0,0,0,1232,1228,1,0,0,0,1233, + 133,1,0,0,0,1234,1235,3,112,56,0,1235,1236,5,0,0,1,1236,135,1,0,0,0,1237, + 1248,5,137,0,0,1238,1239,5,40,0,0,1239,1240,5,29,0,0,1240,1245,3,112,56, + 0,1241,1242,5,139,0,0,1242,1244,3,112,56,0,1243,1241,1,0,0,0,1244,1247, + 1,0,0,0,1245,1243,1,0,0,0,1245,1246,1,0,0,0,1246,1249,1,0,0,0,1247,1245, + 1,0,0,0,1248,1238,1,0,0,0,1248,1249,1,0,0,0,1249,1251,1,0,0,0,1250,1252, + 3,108,54,0,1251,1250,1,0,0,0,1251,1252,1,0,0,0,1252,1254,1,0,0,0,1253, + 1255,3,140,70,0,1254,1253,1,0,0,0,1254,1255,1,0,0,0,1255,1256,1,0,0,0, + 1256,1257,5,138,0,0,1257,137,1,0,0,0,1258,1259,7,19,0,0,1259,139,1,0,0, + 0,1260,1267,7,20,0,0,1261,1262,5,52,0,0,1262,1263,3,142,71,0,1263,1264, + 5,6,0,0,1264,1265,3,142,71,0,1265,1268,1,0,0,0,1266,1268,3,142,71,0,1267, + 1261,1,0,0,0,1267,1266,1,0,0,0,1268,1271,1,0,0,0,1269,1270,5,116,0,0,1270, + 1272,3,144,72,0,1271,1269,1,0,0,0,1271,1272,1,0,0,0,1272,141,1,0,0,0,1273, + 1274,5,112,0,0,1274,1281,7,21,0,0,1275,1276,5,115,0,0,1276,1281,5,50,0, + 0,1277,1278,3,112,56,0,1278,1279,7,21,0,0,1279,1281,1,0,0,0,1280,1273, + 1,0,0,0,1280,1275,1,0,0,0,1280,1277,1,0,0,0,1281,143,1,0,0,0,1282,1283, + 5,115,0,0,1283,1289,5,50,0,0,1284,1289,5,27,0,0,1285,1289,5,117,0,0,1286, + 1287,5,85,0,0,1287,1289,5,118,0,0,1288,1282,1,0,0,0,1288,1284,1,0,0,0, + 1288,1285,1,0,0,0,1288,1286,1,0,0,0,1289,145,1,0,0,0,1290,1291,7,22,0, + 0,1291,147,1,0,0,0,1292,1293,7,23,0,0,1293,149,1,0,0,0,1294,1295,5,123, + 0,0,1295,1296,5,137,0,0,1296,1297,5,3,0,0,1297,1298,3,112,56,0,1298,1299, + 5,138,0,0,1299,151,1,0,0,0,1300,1301,5,119,0,0,1301,1302,5,27,0,0,1302, + 1303,5,137,0,0,1303,1304,3,108,54,0,1304,1305,5,138,0,0,1305,153,1,0,0, + 0,1306,1307,5,2,0,0,1307,1308,7,24,0,0,1308,155,1,0,0,0,1309,1310,7,25, + 0,0,1310,1311,5,122,0,0,1311,157,1,0,0,0,166,159,175,178,184,204,213,216, + 224,233,237,249,257,261,269,274,277,285,292,302,309,323,328,337,348,358, + 361,368,377,384,391,396,405,432,439,443,455,461,478,482,489,494,499,509, + 512,515,521,525,528,541,550,555,559,563,567,577,582,587,595,599,601,605, + 610,618,625,629,637,644,650,658,670,675,680,685,692,699,701,710,720,731, + 736,741,749,756,764,768,771,784,787,791,795,799,802,807,810,813,816,819, + 830,842,846,850,858,870,872,881,883,893,896,904,907,909,917,924,929,932, + 938,941,947,964,967,971,975,979,981,993,998,1006,1031,1040,1047,1069,1078, + 1087,1095,1101,1104,1106,1128,1132,1137,1141,1154,1161,1164,1168,1171, + 1174,1177,1181,1189,1196,1201,1207,1218,1222,1226,1230,1232,1245,1248, + 1251,1254,1267,1271,1280,1288 }; public static readonly ATN _ATN = diff --git a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs index 5972c502..cd588670 100644 --- a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs +++ b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs @@ -689,6 +689,10 @@ private static ViewDefinition BuildViewDefinition(QueryExpressionContext ctx) var orderBy = ctx.orderByClause() is { } ob ? ob.orderByItem().Select(i => new ViewOrderBy(OriginalText(i.expression()), i.dir?.Type == DESC)).ToList() : (IReadOnlyList)[]; + // A stored TOP has no WITH TIES of its own: it is ACE's TOP, which always keeps the ties, while LibRed reads + // it back as a plain one. So neither reading can be stored faithfully. + if (select.topClause()?.ties is not null || ctx.offsetFetchClause()?.ties is not null) + throw new NotSupportedException("A view cannot store TOP … WITH TIES."); // A stored view can only carry a literal TOP (Access stores it as text); reject a parameterized one. int? top = select.topClause() is { } t ? BuildTop(t) is LiteralExpression { Value: int n } @@ -857,10 +861,12 @@ private static SqlStatement BuildQueryExpression(QueryExpressionContext ctx) ? ob.orderByItem().Select(BuildOrderByItem).ToList() : []; Expression? top = null, offset = null; + bool ties = false; if (ctx.offsetFetchClause() is { } paging) { offset = paging.offset is { } off ? BuildExpression(off) : null; top = paging.limit is { } lim ? BuildExpression(lim) : null; + ties = paging.ties is not null; } bool ordered = orderBy.Count > 0 || top is not null || offset is not null; @@ -868,9 +874,10 @@ private static SqlStatement BuildQueryExpression(QueryExpressionContext ctx) { SqlStatement single = BuildQueryTerm(terms[0]); // A single term folds the clauses back into it, so an ordinary `SELECT … ORDER BY x` builds exactly - // the AST it always did and nothing downstream sees this restructuring at all. + // the AST it always did and nothing downstream sees this restructuring at all. A FETCH takes the place + // of the SELECT's own TOP, WITH TIES and all. return ordered && single is SelectStatement s - ? s with { OrderBy = orderBy, Top = top ?? s.Top, Offset = offset } + ? s with { OrderBy = orderBy, Top = top ?? s.Top, Offset = offset, WithTies = top is null ? s.WithTies : ties } : single; } @@ -899,7 +906,7 @@ private static SqlStatement BuildQueryExpression(QueryExpressionContext ctx) } // Only the outermost node carries them: the ordering is the expression's, not that of any inner pair. - return ordered ? result with { OrderBy = orderBy, Top = top, Offset = offset } : result; + return ordered ? result with { OrderBy = orderBy, Top = top, Offset = offset, WithTies = ties } : result; } @@ -987,7 +994,8 @@ private static SelectStatement BuildQuerySpecification(QuerySpecificationContext Distinct: predicate?.DISTINCT() is not null, DistinctRow: predicate?.DISTINCTROW() is not null, TopPercent: topPercent, - Into: ctx.into is null ? null : Identifier(ctx.into)); + Into: ctx.into is null ? null : Identifier(ctx.into), + WithTies: ctx.topClause()?.ties is not null); } /// The TOP count expression: a single operand, or a left-associative +/- chain of them (each diff --git a/src/LibRed/README.md b/src/LibRed/README.md index 72420f84..6b758ab6 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -219,6 +219,10 @@ Treat the number as of its date — an EF Core version bump moves it. - `FULL [OUTER] JOIN` — ACE offers only inner/left/right, and its query designer cannot express a full one. - **`OFFSET … ROWS FETCH NEXT … ROWS ONLY`** paging, where the count may be any expression, not just a literal. Access has only `TOP n`, and only with a literal. + - **`TOP n [PERCENT] WITH TIES`** and **`FETCH … ROWS WITH TIES`** — the n rows and every further one whose + `ORDER BY` keys equal the last one's. ACE's own `TOP n` always keeps those ties (it chooses the same rows, + verified); LibRed's plain `TOP n` returns exactly n, so `WITH TIES` is how to ask for ACE's rows. It needs an + `ORDER BY` and is refused beside `DISTINCT`, and a view cannot store it. - **Standard scalar syntax** ACE lacks: `CASE`, `COALESCE`, `NULLIF`, `GREATEST`/`LEAST` (NULL arguments ignored, as SQL Server and PostgreSQL treat them — extended mode translates `Math.Max`/`Math.Min` to them), and the `VALUES` table value constructor standing in for a query. diff --git a/test/LibRed.Engine.AccessTests/TopWithTiesAccessTests.cs b/test/LibRed.Engine.AccessTests/TopWithTiesAccessTests.cs new file mode 100644 index 00000000..c296b078 --- /dev/null +++ b/test/LibRed.Engine.AccessTests/TopWithTiesAccessTests.cs @@ -0,0 +1,70 @@ +using System.Data.OleDb; +using System.Globalization; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// ACE's own TOP n keeps every row that ties the last on the ORDER BY keys, so LibRed's TOP n WITH TIES +/// must choose the same rows. ACE orders tied rows unstably, so the rows are compared as a set, not as a sequence. +/// +[Collection(AceCollection.Name)] +public class TopWithTiesAccessTests : TempDatabaseTest +{ + private static readonly string[] Setup = + [ + "CREATE TABLE T (Id LONG, K LONG, K2 TEXT(10))", + "INSERT INTO T VALUES (1, 1, 'a')", + "INSERT INTO T VALUES (2, 2, 'b')", + "INSERT INTO T VALUES (3, 2, 'a')", + "INSERT INTO T VALUES (4, 2, 'b')", + "INSERT INTO T VALUES (5, 3, 'a')", + "INSERT INTO T VALUES (6, 3, 'a')", + "INSERT INTO T VALUES (7, 10, 'c')", + "INSERT INTO T VALUES (8, NULL, NULL)", + "INSERT INTO T VALUES (9, NULL, 'z')", + ]; + + [Theory] + [InlineData("TOP 1", "ORDER BY K")] + [InlineData("TOP 3", "ORDER BY K")] + [InlineData("TOP 4", "ORDER BY K")] + [InlineData("TOP 6", "ORDER BY K")] + [InlineData("TOP 3", "ORDER BY K, K2")] + [InlineData("TOP 2", "ORDER BY K DESC")] + [InlineData("TOP 5", "ORDER BY K2")] + [InlineData("TOP 2", "ORDER BY K MOD 2")] + [InlineData("TOP 30 PERCENT", "ORDER BY K")] + [InlineData("TOP 50 PERCENT", "ORDER BY K")] + public void Libred_with_ties_takes_the_rows_ace_top_takes(string top, string orderBy) + { + string path = TemporaryDatabase.CopyPath(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "ties-"); + try + { + List ace; + using (OleDbConnection connection = AceTestDatabase.Open(path)) + { + foreach (string statement in Setup) + { + using OleDbCommand setup = connection.CreateCommand(); + setup.CommandText = statement; + setup.ExecuteNonQuery(); + } + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = $"SELECT {top} Id FROM T {orderBy}"; + using OleDbDataReader reader = command.ExecuteReader(); + ace = []; + while (reader.Read()) ace.Add(reader.GetInt32(0)); + } + OleDbConnection.ReleaseObjectPool(); + + List libred; + using (var database = JetDatabase.Open(path, readOnly: true)) + libred = [.. new QueryEngine(database).ExecuteQuery($"SELECT {top} WITH TIES Id FROM T {orderBy}") + .Rows.Select(row => Convert.ToInt32(row[0], CultureInfo.InvariantCulture))]; + + Assert.Equal(ace.Order(), libred.Order()); + } + finally { TemporaryDatabase.Delete(path); } + } +} diff --git a/test/LibRed.Engine.Tests/TopWithTiesTests.cs b/test/LibRed.Engine.Tests/TopWithTiesTests.cs new file mode 100644 index 00000000..e16d275b --- /dev/null +++ b/test/LibRed.Engine.Tests/TopWithTiesTests.cs @@ -0,0 +1,93 @@ +using System.Globalization; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// TOP n [PERCENT] WITH TIES and FETCH … WITH TIES: the n rows, and every further row whose ORDER BY keys +/// equal the last one's. ACE's own TOP n always keeps the ties, and the rows each case expects here are the ones +/// ACE's plain TOP returned over the same data (verified; TopWithTiesAccessTests checks it). LibRed's plain TOP +/// stays exactly n. +/// +public class TopWithTiesTests(TopWithTiesTests.Database database) : TempDatabaseTest, IClassFixture +{ + // K ties three ways at 2 and two ways at 3 and at Null (which sorts first); K2 breaks some of those ties. + private static readonly string[] Setup = + [ + "CREATE TABLE T (Id LONG, K LONG, K2 TEXT(10))", + "INSERT INTO T VALUES (1, 1, 'a')", + "INSERT INTO T VALUES (2, 2, 'b')", + "INSERT INTO T VALUES (3, 2, 'a')", + "INSERT INTO T VALUES (4, 2, 'b')", + "INSERT INTO T VALUES (5, 3, 'a')", + "INSERT INTO T VALUES (6, 3, 'a')", + "INSERT INTO T VALUES (7, 10, 'c')", + "INSERT INTO T VALUES (8, NULL, NULL)", + "INSERT INTO T VALUES (9, NULL, 'z')", + ]; + + public sealed class Database() : SharedDatabase("ties-", Setup); + + private string Ids(string sql) => + string.Join(",", database.Query(sql, CultureInfo.InvariantCulture).Rows.Select(row => Convert.ToString(row[0], CultureInfo.InvariantCulture))); + + [Theory] + [InlineData("SELECT TOP 1 WITH TIES Id FROM T ORDER BY K", "8,9")] + [InlineData("SELECT TOP 2 WITH TIES Id FROM T ORDER BY K", "8,9")] + [InlineData("SELECT TOP 3 WITH TIES Id FROM T ORDER BY K", "8,9,1")] + [InlineData("SELECT TOP 4 WITH TIES Id FROM T ORDER BY K", "8,9,1,2,3,4")] + [InlineData("SELECT TOP 6 WITH TIES Id FROM T ORDER BY K", "8,9,1,2,3,4")] + [InlineData("SELECT TOP 3 WITH TIES Id FROM T ORDER BY K, K2", "8,9,1")] + [InlineData("SELECT TOP 1 WITH TIES Id FROM T ORDER BY K DESC", "7")] + [InlineData("SELECT TOP 2 WITH TIES Id FROM T ORDER BY K DESC", "7,5,6")] + [InlineData("SELECT TOP 5 WITH TIES Id FROM T ORDER BY K2", "8,1,3,5,6")] + [InlineData("SELECT TOP 2 WITH TIES Id FROM T ORDER BY K MOD 2", "8,9")] + [InlineData("SELECT TOP 2 WITH TIES Id FROM T WHERE K IS NOT NULL ORDER BY K", "1,2,3,4")] + [InlineData("SELECT TOP 30 PERCENT WITH TIES Id FROM T ORDER BY K", "8,9,1")] + [InlineData("SELECT TOP 50 PERCENT WITH TIES Id FROM T ORDER BY K", "8,9,1,2,3,4")] + [InlineData("SELECT TOP 0 WITH TIES Id FROM T ORDER BY K", "")] + public void The_ties_of_the_last_row_are_kept(string sql, string expected) => + Assert.Equal(expected, Ids(sql)); + + [Fact] + public void Plain_top_stays_exactly_n() => + Assert.Equal("8,9,1,2", Ids("SELECT TOP 4 Id FROM T ORDER BY K")); + + [Fact] + public void Ties_are_cut_across_a_join() => + Assert.Equal("5,6", Ids("SELECT TOP 1 WITH TIES a.Id FROM T AS a INNER JOIN T AS b ON a.Id = b.Id WHERE a.K < 10 ORDER BY a.K DESC")); + + // Groups by K2: a has 4 rows, b 2, and c, z and Null 1 each — so the third place is a three-way tie. + [Fact] + public void A_grouped_query_ties_its_groups() + { + var rows = database.Query( + "SELECT TOP 3 WITH TIES K2, COUNT(*) AS N FROM T GROUP BY K2 ORDER BY COUNT(*) DESC", CultureInfo.InvariantCulture).Rows; + Assert.Equal(["a:4", "b:2"], rows.Take(2).Select(row => $"{row[0]}:{row[1]}")); + Assert.Equal(["(null):1", "c:1", "z:1"], rows.Skip(2).Select(row => $"{row[0] ?? "(null)"}:{row[1]}").Order()); + } + + [Theory] + [InlineData("SELECT Id FROM T ORDER BY K FETCH FIRST 3 ROWS WITH TIES", "8,9,1")] + [InlineData("SELECT Id FROM T ORDER BY K OFFSET 2 ROWS FETCH NEXT 2 ROWS WITH TIES", "1,2,3,4")] + [InlineData("SELECT Id FROM T ORDER BY K OFFSET 2 ROWS FETCH NEXT 1 ROW WITH TIES", "1")] + [InlineData("SELECT Id FROM T UNION ALL SELECT Id FROM T ORDER BY Id FETCH FIRST 1 ROWS WITH TIES", "1,1")] + public void Fetch_takes_with_ties_as_well(string sql, string expected) => + Assert.Equal(expected, Ids(sql)); + + [Fact] + public void With_ties_needs_an_order_by() => + Assert.Throws(() => Ids("SELECT TOP 2 WITH TIES Id FROM T")); + + [Fact] + public void With_ties_is_refused_beside_distinct() => + Assert.Throws(() => Ids("SELECT DISTINCT TOP 2 WITH TIES K FROM T ORDER BY K")); + + [Fact] + public void A_view_cannot_store_with_ties() + { + QueryEngine engine = SharedDatabase.Fresh("ties-view-", Setup); + Assert.Throws(() => + engine.ExecuteNonQuery("CREATE VIEW V AS SELECT TOP 2 WITH TIES Id FROM T ORDER BY K")); + } +} From c2a01813773ac453be856b950270cb7c47a4f9d7 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Tue, 29 Sep 2026 18:35:01 +0800 Subject: [PATCH 70/88] Name a derived table's columns with a column list, and emit VALUES that way A derived table now takes the standard's column list, (query) AS t(a, b), naming its columns in order - a VALUES table's included, whose columns have no names of their own. SQL Server and PostgreSQL take the same syntax; ACE has none, so a view refuses one (Access stores a derived source as its query text alone), and a derived table with one is only ever read, never written through. The list must name every column, each once. With that, extended mode writes an inline collection the way EF Core's SQL Server provider does, (VALUES (0, CLNG(1)), (1, 2)) AS v(_ord, Value), instead of EF's fallback for databases without a column list, which names the columns on a leading SELECT and puts the other rows after UNION ALL VALUES. Co-Authored-By: Claude Opus 5.5 --- .../Sql/Internal/LibRedQuerySqlGenerator.cs | 29 + .../LibRed.Engine/Execution/QueryExecutor.cs | 13 +- .../Execution/StatementExecutor.cs | 9 +- src/LibRed/LibRed.Engine/Plan/PlanNodes.cs | 9 +- .../LibRed.Engine/Planning/IndexSelection.cs | 17 +- .../LibRed.Engine/Planning/QueryPlanner.cs | 2 +- src/LibRed/LibRed.Sql/Ast/Clauses.cs | 6 +- src/LibRed/LibRed.Sql/Ast/Statements.cs | 5 +- src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 | 7 +- .../Grammar/Generated/AccessSqlBaseVisitor.cs | 10 + .../Grammar/Generated/AccessSqlParser.cs | 2797 +++++++++-------- .../Grammar/Generated/AccessSqlVisitor.cs | 6 + src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs | 18 +- src/LibRed/README.md | 2 + .../AdHocMiscellaneousQueryLibRedTest.cs | 4 +- .../Query/GearsOfWarQueryLibRedTest.cs | 2 +- .../TPCGearsOfWarQueryLibRedTest.cs | 2 +- .../TPTGearsOfWarQueryLibRedTest.cs | 2 +- .../Query/NorthwindCompiledQueryLibRedTest.cs | 8 +- .../Query/NorthwindJoinQueryLibRedTest.cs | 12 +- .../NorthwindMiscellaneousQueryLibRedTest.cs | 4 +- .../Query/NorthwindSelectQueryLibRedTest.cs | 2 +- .../Query/PrecompiledQueryLibRedTest.cs | 2 +- .../PrimitiveCollectionsQueryLibRedTest.cs | 46 +- .../DerivedColumnListTests.cs | 71 + 25 files changed, 1669 insertions(+), 1416 deletions(-) create mode 100644 test/LibRed.Engine.Tests/DerivedColumnListTests.cs diff --git a/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs b/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs index 8e942e20..55c06a1b 100644 --- a/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs +++ b/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs @@ -46,6 +46,35 @@ protected override bool TryGenerateWithoutWrappingSelect(SelectExpression select => selectExpression.Tables is not [ValuesExpression] && base.TryGenerateWithoutWrappingSelect(selectExpression); + /// + /// A VALUES table as the standard writes it, its columns named by a column list after the alias: + /// (VALUES (0, CLNG(1)), (1, 2)) AS `v`(`_ord`, `Value`). EF's base names them on a leading SELECT + /// instead, with the other rows after UNION ALL VALUES, for databases that have no column list; + /// LibRed's engine has one. + /// + protected override Expression VisitValues(ValuesExpression valuesExpression) + { + base.VisitValues(valuesExpression); + + Sql.Append("("); + GenerateList(valuesExpression.ColumnNames, name => Sql.Append(_sqlGenerationHelper.DelimitIdentifier(name))); + Sql.Append(")"); + + return valuesExpression; + } + + /// + protected override void GenerateValues(ValuesExpression valuesExpression) + { + if (valuesExpression.RowValues is not { Count: > 0 } rowValues) + { + throw new InvalidOperationException(RelationalStrings.EmptyCollectionNotSupportedAsInlineQueryRoot); + } + + Sql.Append("VALUES "); + GenerateList(rowValues, row => Visit(row)); + } + private void GenerateList( IReadOnlyList items, Action generationAction, diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs index 0d9f5e18..2e05f27e 100644 --- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs @@ -698,8 +698,7 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) case DerivedTableNode derived: { var (inner, rows) = Execute(derived.Input, outer); - var columns = inner.Select(c => c with { Qualifier = derived.Alias }).ToList(); - return (columns, rows); + return (DerivedColumns(inner, derived.Alias, derived.Columns), rows); } case FilterNode filter: @@ -2021,6 +2020,16 @@ private List CutWithTies( return sorted.GetRange(skip, end - skip); } + /// A derived table's columns: under its alias, and named by its column list in order where it has one + /// (AS t(a, b)), which must name every column. + internal static List DerivedColumns(IReadOnlyList inner, string? alias, IReadOnlyList? names) + { + if (names is not null && names.Count != inner.Count) + throw new InvalidOperationException( + $"The derived table '{alias}' has {inner.Count} column(s), but its column list names {names.Count}."); + return [.. inner.Select((c, i) => c with { Qualifier = alias, Name = names?[i] ?? c.Name })]; + } + /// Compares two rows' already-evaluated ORDER BY key values, honouring each key's direction, with /// text in 's collation. private static int CompareEvaluatedKeys( diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 3ccfde26..610c92eb 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -1162,7 +1162,7 @@ void EmitDerived(SubqueryTable sq, JoinKind kind, Expression? on) "Operation must use an updateable query: a derived table written to must select from one table, " + "with no grouping or DISTINCT."); var (columns, rows) = ExecuteDerivedSource(sq.Query, alias); - tables.Add(new SourceTable(alias, null, columns, rows)); + tables.Add(new SourceTable(alias, null, QueryExecutor.DerivedColumns(columns, alias, sq.Columns), rows)); kinds.Add(kind); ons.Add(on); groupBases.Add(null); @@ -1185,8 +1185,7 @@ void EmitLateral(SubqueryTable sq, JoinKind kind, Expression? on) var (columns, _) = _scalarRunner.ExecuteCorrelated( plan, new EvalScope(outerColumns, new object?[outerColumns.Count], null)); - tables.Add(new SourceTable( - alias, null, columns.Select(c => c with { Qualifier = alias }).ToList(), null, plan)); + tables.Add(new SourceTable(alias, null, QueryExecutor.DerivedColumns(columns, alias, sq.Columns), null, plan)); kinds.Add(kind); ons.Add(on); groupBases.Add(null); @@ -1315,7 +1314,9 @@ void WalkGroup(JoinTable j, JoinTable group) /// private List? WritableDerivedTable(SubqueryTable sq) { - if (sq.Query is not SelectStatement + // A column list (which ACE has no syntax for) and WITH TIES leave it a query that is only read. + if (sq.Columns is not null + || sq.Query is not SelectStatement { From: NamedTable or JoinTable, GroupBy.Count: 0, Having: null, Distinct: false, Into: null, WithTies: false, } select diff --git a/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs b/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs index fc8dd1ac..0809dd0c 100644 --- a/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs +++ b/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs @@ -46,8 +46,9 @@ public sealed record IndexRangeSeekNode(string Table, string? Alias, IndexDef In } /// A derived table: the output of re-exposed under an alias. The alias -/// is optional (Access permits an aliasless derived table); its columns are then unqualified. -public sealed record DerivedTableNode(PlanNode Input, string? Alias) : PlanNode +/// is optional (Access permits an aliasless derived table); its columns are then unqualified. +/// , the column list of AS t(a, b), renames them in order. +public sealed record DerivedTableNode(PlanNode Input, string? Alias, IReadOnlyList? Columns = null) : PlanNode { public override IReadOnlyList Children => [Input]; } @@ -169,8 +170,8 @@ public sealed record SortNode(PlanNode Input, IReadOnlyList Keys, E /// /// The expressions may reference outer columns (EF emits VALUES (`p`.`Int`) inside a correlated /// subquery), so they are evaluated against the outer scope each time the node runs, not once at planning. -/// The columns are unnamed: today this only ever appears as an operand of a set operation, whose names come -/// from the leading query. +/// The columns are unnamed: a set operation names them from its leading query, and a derived table's column list +/// (AS v(a, b)) names them. /// public sealed record ValuesNode(IReadOnlyList> Rows) : PlanNode { diff --git a/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs b/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs index 2e2700e0..a6e63cbe 100644 --- a/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs +++ b/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs @@ -386,7 +386,7 @@ JetDataType.Boolean or JetDataType.Byte or JetDataType.Int16 or JetDataType.Int3 { if (col.Table is { } t && !string.Equals(t, d.Alias, StringComparison.OrdinalIgnoreCase)) continue; - if (ProjectionExprFor(d.Input, col.Column) is ColumnReference inner) + if (ProjectionExprFor(d, col.Column) is ColumnReference inner) return ResolveKind(inner, d.Input, catalog); } return null; @@ -411,11 +411,20 @@ JetDataType.Boolean or JetDataType.Byte or JetDataType.Int16 or JetDataType.Int3 _ => node.Children.SelectMany(DerivedTables), }; - /// The expression a derived query projects under output name , or null. - private static Expression? ProjectionExprFor(PlanNode derivedBody, string column) + /// The expression a derived table projects under output name , or null. A column + /// list renames the columns by position, so there the name finds its position rather than a projected name. + private static Expression? ProjectionExprFor(DerivedTableNode derived, string column) { - if (FindProject(derivedBody) is not { } proj) + if (FindProject(derived.Input) is not { } proj) return null; + if (derived.Columns is { } names) + { + int position = names.ToList().FindIndex(n => string.Equals(n, column, StringComparison.OrdinalIgnoreCase)); + return position >= 0 && position < proj.Projection.Count + && !proj.Projection.Any(i => i.Value is StarExpression or QualifiedStarExpression) + ? proj.Projection[position].Value + : null; + } foreach (SelectItem item in proj.Projection) { string name = item.Alias ?? (item.Value as ColumnReference)?.Column ?? ""; diff --git a/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs b/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs index b2879f27..dc3210f9 100644 --- a/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs +++ b/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs @@ -307,7 +307,7 @@ private static Expression LiftWindows(Expression e, List windows) null => new SingleRowNode(), // FROM-less SELECT (e.g. `SELECT 2`) — one row, no columns NamedTable t => new ScanNode(t.Name, t.Alias), JoinTable j => new JoinNode(PlanFrom(j.Left), PlanFrom(j.Right), j.Kind, j.On), - SubqueryTable s => new DerivedTableNode(PlanStatement(s.Query), s.Alias), // alias optional (Access allows aliasless) + SubqueryTable s => new DerivedTableNode(PlanStatement(s.Query), s.Alias, s.Columns), // alias optional (Access allows aliasless) _ => throw new NotSupportedException($"Unsupported FROM source {from.GetType().Name}."), }; diff --git a/src/LibRed/LibRed.Sql/Ast/Clauses.cs b/src/LibRed/LibRed.Sql/Ast/Clauses.cs index 46e82093..be86e221 100644 --- a/src/LibRed/LibRed.Sql/Ast/Clauses.cs +++ b/src/LibRed/LibRed.Sql/Ast/Clauses.cs @@ -7,8 +7,10 @@ public abstract record TableReference : SqlNode; public sealed record NamedTable(string Name, string? Alias) : TableReference; /// A derived table (subquery) in the FROM clause. The query is any -/// query — a or a (e.g. a UNION). -public sealed record SubqueryTable(SqlStatement Query, string? Alias) : TableReference; +/// query — a or a (e.g. a UNION). +/// is its column list, AS t(a, b), naming its columns in order; null without +/// one. +public sealed record SubqueryTable(SqlStatement Query, string? Alias, IReadOnlyList? Columns = null) : TableReference; /// /// is a LibRed extension - ACE has no full outer join. So are and diff --git a/src/LibRed/LibRed.Sql/Ast/Statements.cs b/src/LibRed/LibRed.Sql/Ast/Statements.cs index e9fcc01d..f9f7e4ba 100644 --- a/src/LibRed/LibRed.Sql/Ast/Statements.cs +++ b/src/LibRed/LibRed.Sql/Ast/Statements.cs @@ -96,9 +96,8 @@ public sealed record SetOperationStatement( /// an inline collection. /// /// -/// The rows carry no column names of their own. In the set operation that always encloses one today, names -/// come from the leading query per SQL, so none are needed; naming them would require the column alias list -/// (AS t(a, b)) that derived tables do not yet support. Row values may reference outer columns, so the +/// The rows carry no column names of their own: in a set operation names come from the leading query per SQL, and +/// as a derived table the column list names them (AS t(a, b)). Row values may reference outer columns, so the /// expressions are evaluated per outer row rather than once. DEFAULT is rejected here — the standard /// permits it only inside an INSERT. /// diff --git a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 index fdb1b028..7408b705 100644 --- a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 +++ b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 @@ -372,7 +372,7 @@ tableSource : tablePrimary joinClause* ; tablePrimary : table=identifier (AS? alias=identifier)? # NamedTablePrimary - | LPAREN queryExpression RPAREN (AS? alias=identifier)? # SubqueryPrimary + | LPAREN queryExpression RPAREN (AS? alias=identifier derivedColumns?)? # SubqueryPrimary | LPAREN tableSource RPAREN # ParenJoinPrimary ; @@ -608,6 +608,11 @@ nthRowFrom : FROM edge=(FIRST | LAST) ; // Whether LAG, LEAD, FIRST_VALUE, LAST_VALUE and NTH_VALUE count the rows whose value is Null (the default) or skip them. nullTreatment : treatment=(RESPECT | IGNORE) NULLS ; +// A derived table's column list, the standard's : (VALUES (1, 'a')) AS v(n, s) names the +// columns n and s, as SQL Server and PostgreSQL take it and EF Core emits it for an inline collection. ACE has no +// such syntax. Kept after the existing parser rules so adding it does not renumber their ids. +derivedColumns : LPAREN names+=identifier (COMMA names+=identifier)* RPAREN ; + // ---- Lexer ---- SELECT : [Ss][Ee][Ll][Ee][Cc][Tt] ; diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs index ed1fb44a..c2e6fb71 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs @@ -1721,5 +1721,15 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe parse tree. /// The visitor result. public virtual Result VisitNullTreatment([NotNull] AccessSqlParser.NullTreatmentContext context) { return VisitChildren(context); } + /// + /// Visit a parse tree produced by . + /// + /// The default implementation returns the result of calling + /// on . + /// + /// + /// The parse tree. + /// The visitor result. + public virtual Result VisitDerivedColumns([NotNull] AccessSqlParser.DerivedColumnsContext context) { return VisitChildren(context); } } } // namespace LibRed.Sql.Grammar diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs index 2b950e23..f9af4432 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs @@ -84,7 +84,8 @@ public const int RULE_standaloneExpression = 67, RULE_windowSpecification = 68, RULE_clusteredOption = 69, RULE_windowFrame = 70, RULE_frameBound = 71, RULE_frameExclusion = 72, RULE_nonReservedKeyword = 73, RULE_reservedKeyword = 74, RULE_filterClause = 75, - RULE_withinGroup = 76, RULE_nthRowFrom = 77, RULE_nullTreatment = 78; + RULE_withinGroup = 76, RULE_nthRowFrom = 77, RULE_nullTreatment = 78, + RULE_derivedColumns = 79; public static readonly string[] ruleNames = { "statement", "ifThenStatement", "thenBody", "executeStatement", "updateStatement", "assignment", "deleteStatement", "systemVariableSelect", "sysVarItem", @@ -102,7 +103,7 @@ public const int "functionName", "columnRef", "memberName", "identifier", "literal", "transactionStatement", "standaloneExpression", "windowSpecification", "clusteredOption", "windowFrame", "frameBound", "frameExclusion", "nonReservedKeyword", "reservedKeyword", - "filterClause", "withinGroup", "nthRowFrom", "nullTreatment" + "filterClause", "withinGroup", "nthRowFrom", "nullTreatment", "derivedColumns" }; private static readonly string[] _LiteralNames = { @@ -256,115 +257,115 @@ public StatementContext statement() { try { EnterOuterAlt(_localctx, 1); { - State = 159; + State = 161; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PARAMETERS) { { - State = 158; + State = 160; parametersClause(); } } - State = 175; + State = 177; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,1,Context) ) { case 1: { - State = 161; + State = 163; ifThenStatement(); } break; case 2: { - State = 162; + State = 164; createTableStatement(); } break; case 3: { - State = 163; + State = 165; createIndexStatement(); } break; case 4: { - State = 164; + State = 166; createViewStatement(); } break; case 5: { - State = 165; + State = 167; createProcedureStatement(); } break; case 6: { - State = 166; + State = 168; alterTableStatement(); } break; case 7: { - State = 167; + State = 169; dropStatement(); } break; case 8: { - State = 168; + State = 170; insertStatement(); } break; case 9: { - State = 169; + State = 171; updateStatement(); } break; case 10: { - State = 170; + State = 172; deleteStatement(); } break; case 11: { - State = 171; + State = 173; transactionStatement(); } break; case 12: { - State = 172; + State = 174; executeStatement(); } break; case 13: { - State = 173; + State = 175; systemVariableSelect(); } break; case 14: { - State = 174; + State = 176; queryExpression(); } break; } - State = 178; + State = 180; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==SEMI) { { - State = 177; + State = 179; Match(SEMI); } } - State = 180; + State = 182; Match(Eof); } } @@ -414,29 +415,29 @@ public IfThenStatementContext ifThenStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 182; - Match(IF); State = 184; + Match(IF); + State = 186; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 183; + State = 185; _localctx.not = Match(NOT); } } - State = 186; + State = 188; Match(EXISTS); - State = 187; + State = 189; Match(LPAREN); - State = 188; + State = 190; queryExpression(); - State = 189; + State = 191; Match(RPAREN); - State = 190; + State = 192; Match(THEN); - State = 191; + State = 193; thenBody(); } } @@ -503,83 +504,83 @@ public ThenBodyContext thenBody() { ThenBodyContext _localctx = new ThenBodyContext(Context, State); EnterRule(_localctx, 4, RULE_thenBody); try { - State = 204; + State = 206; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,4,Context) ) { case 1: EnterOuterAlt(_localctx, 1); { - State = 193; + State = 195; createTableStatement(); } break; case 2: EnterOuterAlt(_localctx, 2); { - State = 194; + State = 196; createIndexStatement(); } break; case 3: EnterOuterAlt(_localctx, 3); { - State = 195; + State = 197; createViewStatement(); } break; case 4: EnterOuterAlt(_localctx, 4); { - State = 196; + State = 198; createProcedureStatement(); } break; case 5: EnterOuterAlt(_localctx, 5); { - State = 197; + State = 199; alterTableStatement(); } break; case 6: EnterOuterAlt(_localctx, 6); { - State = 198; + State = 200; dropStatement(); } break; case 7: EnterOuterAlt(_localctx, 7); { - State = 199; + State = 201; insertStatement(); } break; case 8: EnterOuterAlt(_localctx, 8); { - State = 200; + State = 202; updateStatement(); } break; case 9: EnterOuterAlt(_localctx, 9); { - State = 201; + State = 203; deleteStatement(); } break; case 10: EnterOuterAlt(_localctx, 10); { - State = 202; + State = 204; executeStatement(); } break; case 11: EnterOuterAlt(_localctx, 11); { - State = 203; + State = 205; queryExpression(); } break; @@ -634,7 +635,7 @@ public ExecuteStatementContext executeStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 206; + State = 208; _la = TokenStream.LA(1); if ( !(_la==EXECUTE || _la==EXEC) ) { ErrorHandler.RecoverInline(this); @@ -643,28 +644,28 @@ public ExecuteStatementContext executeStatement() { ErrorHandler.ReportMatch(this); Consume(); } - State = 207; + State = 209; _localctx.name = identifier(); - State = 216; + State = 218; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775694237952L) != 0) || ((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & -283699838490247167L) != 0) || ((((_la - 149)) & ~0x3f) == 0 && ((1L << (_la - 149)) & 31L) != 0)) { { - State = 208; + State = 210; expression(0); - State = 213; + State = 215; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 209; + State = 211; Match(COMMA); - State = 210; + State = 212; expression(0); } } - State = 215; + State = 217; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -727,52 +728,52 @@ public UpdateStatementContext updateStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 218; + State = 220; Match(UPDATE); - State = 219; + State = 221; tableSource(); - State = 224; + State = 226; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 220; + State = 222; Match(COMMA); - State = 221; + State = 223; tableSource(); } } - State = 226; + State = 228; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 227; + State = 229; Match(SET); - State = 228; + State = 230; assignment(); - State = 233; + State = 235; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 229; + State = 231; Match(COMMA); - State = 230; + State = 232; assignment(); } } - State = 235; + State = 237; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 237; + State = 239; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 236; + State = 238; whereClause(); } } @@ -819,11 +820,11 @@ public AssignmentContext assignment() { try { EnterOuterAlt(_localctx, 1); { - State = 239; + State = 241; _localctx.target = columnRef(); - State = 240; + State = 242; Match(EQ); - State = 241; + State = 243; expression(0); } } @@ -881,9 +882,9 @@ public DeleteStatementContext deleteStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 243; + State = 245; Match(DELETE); - State = 249; + State = 251; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -906,17 +907,17 @@ public DeleteStatementContext deleteStatement() { case BACKTICK_ID: case IDENTIFIER: { - State = 244; + State = 246; _localctx.target = identifier(); - State = 245; + State = 247; Match(DOT); - State = 246; + State = 248; Match(STAR); } break; case STAR: { - State = 248; + State = 250; Match(STAR); } break; @@ -925,32 +926,32 @@ public DeleteStatementContext deleteStatement() { default: break; } - State = 251; + State = 253; Match(FROM); - State = 252; + State = 254; tableSource(); - State = 257; + State = 259; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 253; + State = 255; Match(COMMA); - State = 254; + State = 256; tableSource(); } } - State = 259; + State = 261; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 261; + State = 263; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 260; + State = 262; whereClause(); } } @@ -1001,23 +1002,23 @@ public SystemVariableSelectContext systemVariableSelect() { try { EnterOuterAlt(_localctx, 1); { - State = 263; + State = 265; Match(SELECT); - State = 264; + State = 266; sysVarItem(); - State = 269; + State = 271; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 265; + State = 267; Match(COMMA); - State = 266; + State = 268; sysVarItem(); } } - State = 271; + State = 273; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -1062,24 +1063,24 @@ public SysVarItemContext sysVarItem() { try { EnterOuterAlt(_localctx, 1); { - State = 272; + State = 274; Match(SYSVAR); - State = 277; + State = 279; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 274; + State = 276; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 273; + State = 275; Match(AS); } } - State = 276; + State = 278; _localctx.alias = identifier(); } } @@ -1131,27 +1132,27 @@ public ParametersClauseContext parametersClause() { try { EnterOuterAlt(_localctx, 1); { - State = 279; + State = 281; Match(PARAMETERS); - State = 280; + State = 282; procParam(); - State = 285; + State = 287; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 281; + State = 283; Match(COMMA); - State = 282; + State = 284; procParam(); } } - State = 287; + State = 289; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 288; + State = 290; Match(SEMI); } } @@ -1215,61 +1216,61 @@ public CreateTableStatementContext createTableStatement() { int _alt; EnterOuterAlt(_localctx, 1); { - State = 290; - Match(CREATE); State = 292; + Match(CREATE); + State = 294; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TEMPORARY) { { - State = 291; + State = 293; _localctx.temp = Match(TEMPORARY); } } - State = 294; + State = 296; Match(TABLE); - State = 295; + State = 297; _localctx.table = identifier(); - State = 296; + State = 298; Match(LPAREN); - State = 297; + State = 299; columnDefinition(); - State = 302; + State = 304; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,18,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 298; + State = 300; Match(COMMA); - State = 299; + State = 301; columnDefinition(); } } } - State = 304; + State = 306; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,18,Context); } - State = 309; + State = 311; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 305; + State = 307; Match(COMMA); - State = 306; + State = 308; tableConstraint(); } } - State = 311; + State = 313; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 312; + State = 314; Match(RPAREN); } } @@ -1328,47 +1329,47 @@ public CreateViewStatementContext createViewStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 314; + State = 316; Match(CREATE); - State = 315; + State = 317; Match(VIEW); - State = 316; + State = 318; _localctx.name = identifier(); - State = 328; + State = 330; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 317; + State = 319; Match(LPAREN); - State = 318; + State = 320; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); - State = 323; + State = 325; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 319; + State = 321; Match(COMMA); - State = 320; + State = 322; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); } } - State = 325; + State = 327; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 326; + State = 328; Match(RPAREN); } } - State = 330; + State = 332; Match(AS); - State = 331; + State = 333; _localctx.query = queryExpression(); } } @@ -1419,25 +1420,25 @@ public CreateProcedureStatementContext createProcedureStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 333; + State = 335; Match(CREATE); - State = 334; + State = 336; Match(PROCEDURE); - State = 335; - _localctx.name = identifier(); State = 337; + _localctx.name = identifier(); + State = 339; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123283815137275L) != 0)) { { - State = 336; + State = 338; procParamList(); } } - State = 339; + State = 341; Match(AS); - State = 340; + State = 342; _localctx.body = procedureBody(); } } @@ -1484,33 +1485,33 @@ public ProcParamListContext procParamList() { EnterRule(_localctx, 26, RULE_procParamList); int _la; try { - State = 361; + State = 363; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case LPAREN: EnterOuterAlt(_localctx, 1); { - State = 342; + State = 344; Match(LPAREN); - State = 343; + State = 345; procParam(); - State = 348; + State = 350; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 344; + State = 346; Match(COMMA); - State = 345; + State = 347; procParam(); } } - State = 350; + State = 352; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 351; + State = 353; Match(RPAREN); } break; @@ -1536,21 +1537,21 @@ public ProcParamListContext procParamList() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 353; + State = 355; procParam(); - State = 358; + State = 360; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 354; + State = 356; Match(COMMA); - State = 355; + State = 357; procParam(); } } - State = 360; + State = 362; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -1599,9 +1600,9 @@ public ProcParamContext procParam() { try { EnterOuterAlt(_localctx, 1); { - State = 363; + State = 365; _localctx.pname = procParamName(); - State = 364; + State = 366; dataType(); } } @@ -1639,7 +1640,7 @@ public ProcParamNameContext procParamName() { ProcParamNameContext _localctx = new ProcParamNameContext(Context, State); EnterRule(_localctx, 30, RULE_procParamName); try { - State = 368; + State = 370; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -1663,14 +1664,14 @@ public ProcParamNameContext procParamName() { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 366; + State = 368; columnRef(); } break; case PARAM: EnterOuterAlt(_localctx, 2); { - State = 367; + State = 369; Match(PARAM); } break; @@ -1719,13 +1720,13 @@ public AlterTableStatementContext alterTableStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 370; + State = 372; Match(ALTER); - State = 371; + State = 373; Match(TABLE); - State = 372; + State = 374; _localctx.table = identifier(); - State = 373; + State = 375; alterTableAction(); } } @@ -1932,26 +1933,26 @@ public AlterTableActionContext alterTableAction() { EnterRule(_localctx, 34, RULE_alterTableAction); int _la; try { - State = 432; + State = 434; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,32,Context) ) { case 1: _localctx = new AddColumnActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 375; - Match(ADD); State = 377; + Match(ADD); + State = 379; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 376; + State = 378; Match(COLUMN); } } - State = 379; + State = 381; columnDefinition(); } break; @@ -1959,9 +1960,9 @@ public AlterTableActionContext alterTableAction() { _localctx = new AddConstraintActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 380; + State = 382; Match(ADD); - State = 381; + State = 383; tableConstraint(); } break; @@ -1969,33 +1970,33 @@ public AlterTableActionContext alterTableAction() { _localctx = new AlterColumnActionContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 382; - Match(ALTER); State = 384; + Match(ALTER); + State = 386; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 383; + State = 385; Match(COLUMN); } } - State = 386; + State = 388; ((AlterColumnActionContext)_localctx).field = identifier(); - State = 387; + State = 389; dataType(); - State = 391; + State = 393; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 68736821269L) != 0)) { { { - State = 388; + State = 390; columnConstraint(); } } - State = 393; + State = 395; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -2005,25 +2006,25 @@ public AlterTableActionContext alterTableAction() { _localctx = new AlterColumnSetDefaultActionContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 394; - Match(ALTER); State = 396; + Match(ALTER); + State = 398; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 395; + State = 397; Match(COLUMN); } } - State = 398; + State = 400; ((AlterColumnSetDefaultActionContext)_localctx).field = identifier(); - State = 399; + State = 401; Match(SET); - State = 400; + State = 402; Match(DEFAULT); - State = 401; + State = 403; expression(0); } break; @@ -2031,23 +2032,23 @@ public AlterTableActionContext alterTableAction() { _localctx = new AlterColumnDropDefaultActionContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 403; - Match(ALTER); State = 405; + Match(ALTER); + State = 407; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 404; + State = 406; Match(COLUMN); } } - State = 407; + State = 409; ((AlterColumnDropDefaultActionContext)_localctx).field = identifier(); - State = 408; + State = 410; Match(DROP); - State = 409; + State = 411; Match(DEFAULT); } break; @@ -2055,11 +2056,11 @@ public AlterTableActionContext alterTableAction() { _localctx = new DropColumnActionContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 411; + State = 413; Match(DROP); - State = 412; + State = 414; Match(COLUMN); - State = 413; + State = 415; ((DropColumnActionContext)_localctx).field = identifier(); } break; @@ -2067,11 +2068,11 @@ public AlterTableActionContext alterTableAction() { _localctx = new DropConstraintActionContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 414; + State = 416; Match(DROP); - State = 415; + State = 417; Match(CONSTRAINT); - State = 416; + State = 418; ((DropConstraintActionContext)_localctx).cname = identifier(); } break; @@ -2079,11 +2080,11 @@ public AlterTableActionContext alterTableAction() { _localctx = new RenameTableActionContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 417; + State = 419; Match(RENAME); - State = 418; + State = 420; Match(TO); - State = 419; + State = 421; ((RenameTableActionContext)_localctx).newName = identifier(); } break; @@ -2091,15 +2092,15 @@ public AlterTableActionContext alterTableAction() { _localctx = new RenameColumnActionContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 420; + State = 422; Match(RENAME); - State = 421; + State = 423; Match(COLUMN); - State = 422; + State = 424; ((RenameColumnActionContext)_localctx).field = identifier(); - State = 423; + State = 425; Match(TO); - State = 424; + State = 426; ((RenameColumnActionContext)_localctx).newName = identifier(); } break; @@ -2107,15 +2108,15 @@ public AlterTableActionContext alterTableAction() { _localctx = new RenameIndexActionContext(_localctx); EnterOuterAlt(_localctx, 10); { - State = 426; + State = 428; Match(RENAME); - State = 427; + State = 429; Match(INDEX); - State = 428; + State = 430; ((RenameIndexActionContext)_localctx).index = identifier(); - State = 429; + State = 431; Match(TO); - State = 430; + State = 432; ((RenameIndexActionContext)_localctx).newName = identifier(); } break; @@ -2166,7 +2167,7 @@ public ProcedureBodyContext procedureBody() { ProcedureBodyContext _localctx = new ProcedureBodyContext(Context, State); EnterRule(_localctx, 36, RULE_procedureBody); try { - State = 439; + State = 441; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: @@ -2174,35 +2175,35 @@ public ProcedureBodyContext procedureBody() { case LPAREN: EnterOuterAlt(_localctx, 1); { - State = 434; + State = 436; queryExpression(); } break; case INSERT: EnterOuterAlt(_localctx, 2); { - State = 435; + State = 437; insertStatement(); } break; case UPDATE: EnterOuterAlt(_localctx, 3); { - State = 436; + State = 438; updateStatement(); } break; case DELETE: EnterOuterAlt(_localctx, 4); { - State = 437; + State = 439; deleteStatement(); } break; case CREATE: EnterOuterAlt(_localctx, 5); { - State = 438; + State = 440; createTableStatement(); } break; @@ -2272,56 +2273,56 @@ public CreateIndexStatementContext createIndexStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 441; - Match(CREATE); State = 443; + Match(CREATE); + State = 445; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==UNIQUE) { { - State = 442; + State = 444; _localctx.unique = Match(UNIQUE); } } - State = 445; + State = 447; Match(INDEX); - State = 446; + State = 448; _localctx.name = identifier(); - State = 447; + State = 449; Match(ON); - State = 448; + State = 450; _localctx.table = identifier(); - State = 449; + State = 451; Match(LPAREN); - State = 450; + State = 452; indexColumn(); - State = 455; + State = 457; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 451; + State = 453; Match(COMMA); - State = 452; + State = 454; indexColumn(); } } - State = 457; + State = 459; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 458; + State = 460; Match(RPAREN); - State = 461; + State = 463; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WITH) { { - State = 459; + State = 461; Match(WITH); - State = 460; + State = 462; withOption(); } } @@ -2422,18 +2423,18 @@ public DropStatementContext dropStatement() { DropStatementContext _localctx = new DropStatementContext(Context, State); EnterRule(_localctx, 40, RULE_dropStatement); try { - State = 478; + State = 480; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,37,Context) ) { case 1: _localctx = new DropTableStatementContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 463; + State = 465; Match(DROP); - State = 464; + State = 466; Match(TABLE); - State = 465; + State = 467; ((DropTableStatementContext)_localctx).table = identifier(); } break; @@ -2441,15 +2442,15 @@ public DropStatementContext dropStatement() { _localctx = new DropIndexStatementContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 466; + State = 468; Match(DROP); - State = 467; + State = 469; Match(INDEX); - State = 468; + State = 470; ((DropIndexStatementContext)_localctx).index = identifier(); - State = 469; + State = 471; Match(ON); - State = 470; + State = 472; ((DropIndexStatementContext)_localctx).table = identifier(); } break; @@ -2457,11 +2458,11 @@ public DropStatementContext dropStatement() { _localctx = new DropProcedureStatementContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 472; + State = 474; Match(DROP); - State = 473; + State = 475; Match(PROCEDURE); - State = 474; + State = 476; ((DropProcedureStatementContext)_localctx).proc = identifier(); } break; @@ -2469,11 +2470,11 @@ public DropStatementContext dropStatement() { _localctx = new DropViewStatementContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 475; + State = 477; Match(DROP); - State = 476; + State = 478; Match(VIEW); - State = 477; + State = 479; ((DropViewStatementContext)_localctx).view = identifier(); } break; @@ -2519,14 +2520,14 @@ public IndexColumnContext indexColumn() { try { EnterOuterAlt(_localctx, 1); { - State = 480; - _localctx.col = identifier(); State = 482; + _localctx.col = identifier(); + State = 484; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ASC || _la==DESC) { { - State = 481; + State = 483; _localctx.dir = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -2602,14 +2603,14 @@ public WithOptionContext withOption() { WithOptionContext _localctx = new WithOptionContext(Context, State); EnterRule(_localctx, 44, RULE_withOption); try { - State = 489; + State = 491; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case PRIMARY: _localctx = new WithPrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 484; + State = 486; Match(PRIMARY); } break; @@ -2617,9 +2618,9 @@ public WithOptionContext withOption() { _localctx = new WithDisallowNullContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 485; + State = 487; Match(DISALLOW); - State = 486; + State = 488; Match(NULL); } break; @@ -2627,9 +2628,9 @@ public WithOptionContext withOption() { _localctx = new WithIgnoreNullContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 487; + State = 489; Match(IGNORE); - State = 488; + State = 490; Match(NULL); } break; @@ -2686,31 +2687,31 @@ public ColumnDefinitionContext columnDefinition() { try { EnterOuterAlt(_localctx, 1); { - State = 491; + State = 493; _localctx.name = identifier(); - State = 492; - dataType(); State = 494; + dataType(); + State = 496; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 493; + State = 495; calculatedClause(); } } - State = 499; + State = 501; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 68736821269L) != 0)) { { { - State = 496; + State = 498; columnConstraint(); } } - State = 501; + State = 503; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -2754,13 +2755,13 @@ public CalculatedClauseContext calculatedClause() { try { EnterOuterAlt(_localctx, 1); { - State = 502; + State = 504; Match(AS); - State = 503; + State = 505; Match(LPAREN); - State = 504; + State = 506; expression(0); - State = 505; + State = 507; Match(RPAREN); } } @@ -2819,7 +2820,7 @@ public DataTypeContext dataType() { try { EnterOuterAlt(_localctx, 1); { - State = 509; + State = 511; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -2842,61 +2843,61 @@ public DataTypeContext dataType() { case BACKTICK_ID: case IDENTIFIER: { - State = 507; + State = 509; _localctx.typeName = identifier(); } break; case IDENTITY: { - State = 508; + State = 510; _localctx.identityType = Match(IDENTITY); } break; default: throw new NoViableAltException(this); } - State = 512; + State = 514; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,43,Context) ) { case 1: { - State = 511; + State = 513; _localctx.extra = identifier(); } break; } - State = 515; + State = 517; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 514; + State = 516; _localctx.extra2 = identifier(); } } - State = 525; + State = 527; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 517; + State = 519; Match(LPAREN); - State = 518; + State = 520; _localctx.size = signedInteger(); - State = 521; + State = 523; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COMMA) { { - State = 519; + State = 521; Match(COMMA); - State = 520; + State = 522; _localctx.scale = signedInteger(); } } - State = 523; + State = 525; Match(RPAREN); } } @@ -2938,17 +2939,17 @@ public SignedIntegerContext signedInteger() { try { EnterOuterAlt(_localctx, 1); { - State = 528; + State = 530; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==MINUS) { { - State = 527; + State = 529; Match(MINUS); } } - State = 530; + State = 532; Match(INTEGER_LITERAL); } } @@ -3139,16 +3140,16 @@ public ColumnConstraintContext columnConstraint() { EnterRule(_localctx, 54, RULE_columnConstraint); int _la; try { - State = 601; + State = 603; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,59,Context) ) { case 1: _localctx = new NotNullConstraintContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 532; + State = 534; Match(NOT); - State = 533; + State = 535; Match(NULL); } break; @@ -3156,7 +3157,7 @@ public ColumnConstraintContext columnConstraint() { _localctx = new NullableConstraintContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 534; + State = 536; Match(NULL); } break; @@ -3164,9 +3165,9 @@ public ColumnConstraintContext columnConstraint() { _localctx = new DefaultConstraintContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 535; + State = 537; Match(DEFAULT); - State = 536; + State = 538; expression(0); } break; @@ -3174,9 +3175,9 @@ public ColumnConstraintContext columnConstraint() { _localctx = new CompressionConstraintContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 537; + State = 539; Match(WITH); - State = 538; + State = 540; _la = TokenStream.LA(1); if ( !(_la==COMPRESSION || _la==COMP) ) { ErrorHandler.RecoverInline(this); @@ -3191,25 +3192,25 @@ public ColumnConstraintContext columnConstraint() { _localctx = new CheckColumnConstraintContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 541; + State = 543; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 539; + State = 541; Match(CONSTRAINT); - State = 540; + State = 542; ((CheckColumnConstraintContext)_localctx).cname = identifier(); } } - State = 543; + State = 545; Match(CHECK); - State = 544; + State = 546; Match(LPAREN); - State = 545; + State = 547; checkBody(); - State = 546; + State = 548; Match(RPAREN); } break; @@ -3217,28 +3218,28 @@ public ColumnConstraintContext columnConstraint() { _localctx = new PrimaryKeyConstraintContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 550; + State = 552; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 548; + State = 550; Match(CONSTRAINT); - State = 549; + State = 551; ((PrimaryKeyConstraintContext)_localctx).cname = identifier(); } } - State = 552; + State = 554; Match(PRIMARY); - State = 553; - Match(KEY); State = 555; + Match(KEY); + State = 557; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 554; + State = 556; clusteredOption(); } } @@ -3249,26 +3250,26 @@ public ColumnConstraintContext columnConstraint() { _localctx = new UniqueColumnConstraintContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 559; + State = 561; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 557; + State = 559; Match(CONSTRAINT); - State = 558; + State = 560; ((UniqueColumnConstraintContext)_localctx).cname = identifier(); } } - State = 561; - Match(UNIQUE); State = 563; + Match(UNIQUE); + State = 565; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 562; + State = 564; clusteredOption(); } } @@ -3279,65 +3280,65 @@ public ColumnConstraintContext columnConstraint() { _localctx = new ColumnReferencesConstraintContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 567; + State = 569; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 565; + State = 567; Match(CONSTRAINT); - State = 566; + State = 568; ((ColumnReferencesConstraintContext)_localctx).cname = identifier(); } } - State = 569; + State = 571; Match(REFERENCES); - State = 570; + State = 572; ((ColumnReferencesConstraintContext)_localctx).refTable = identifier(); - State = 582; + State = 584; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 571; + State = 573; Match(LPAREN); - State = 572; + State = 574; ((ColumnReferencesConstraintContext)_localctx)._identifier = identifier(); ((ColumnReferencesConstraintContext)_localctx)._refColumns.Add(((ColumnReferencesConstraintContext)_localctx)._identifier); - State = 577; + State = 579; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 573; + State = 575; Match(COMMA); - State = 574; + State = 576; ((ColumnReferencesConstraintContext)_localctx)._identifier = identifier(); ((ColumnReferencesConstraintContext)_localctx)._refColumns.Add(((ColumnReferencesConstraintContext)_localctx)._identifier); } } - State = 579; + State = 581; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 580; + State = 582; Match(RPAREN); } } - State = 587; + State = 589; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==ON) { { { - State = 584; + State = 586; foreignKeyAction(); } } - State = 589; + State = 591; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -3347,30 +3348,30 @@ public ColumnConstraintContext columnConstraint() { _localctx = new IdentityConstraintContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 590; + State = 592; Match(IDENTITY); - State = 599; + State = 601; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 591; + State = 593; Match(LPAREN); - State = 592; + State = 594; ((IdentityConstraintContext)_localctx).seed = signedInteger(); - State = 595; + State = 597; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COMMA) { { - State = 593; + State = 595; Match(COMMA); - State = 594; + State = 596; ((IdentityConstraintContext)_localctx).increment = signedInteger(); } } - State = 597; + State = 599; Match(RPAREN); } } @@ -3533,62 +3534,62 @@ public TableConstraintContext tableConstraint() { EnterRule(_localctx, 56, RULE_tableConstraint); int _la; try { - State = 692; + State = 694; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,73,Context) ) { case 1: _localctx = new PrimaryKeyTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 605; + State = 607; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 603; + State = 605; Match(CONSTRAINT); - State = 604; + State = 606; ((PrimaryKeyTableConstraintContext)_localctx).name = identifier(); } } - State = 607; + State = 609; Match(PRIMARY); - State = 608; - Match(KEY); State = 610; + Match(KEY); + State = 612; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 609; + State = 611; clusteredOption(); } } - State = 612; + State = 614; Match(LPAREN); - State = 613; + State = 615; ((PrimaryKeyTableConstraintContext)_localctx)._identifier = identifier(); ((PrimaryKeyTableConstraintContext)_localctx)._columns.Add(((PrimaryKeyTableConstraintContext)_localctx)._identifier); - State = 618; + State = 620; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 614; + State = 616; Match(COMMA); - State = 615; + State = 617; ((PrimaryKeyTableConstraintContext)_localctx)._identifier = identifier(); ((PrimaryKeyTableConstraintContext)_localctx)._columns.Add(((PrimaryKeyTableConstraintContext)_localctx)._identifier); } } - State = 620; + State = 622; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 621; + State = 623; Match(RPAREN); } break; @@ -3596,53 +3597,53 @@ public TableConstraintContext tableConstraint() { _localctx = new UniqueTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 625; + State = 627; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 623; + State = 625; Match(CONSTRAINT); - State = 624; + State = 626; ((UniqueTableConstraintContext)_localctx).name = identifier(); } } - State = 627; - Match(UNIQUE); State = 629; + Match(UNIQUE); + State = 631; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 628; + State = 630; clusteredOption(); } } - State = 631; + State = 633; Match(LPAREN); - State = 632; + State = 634; ((UniqueTableConstraintContext)_localctx)._identifier = identifier(); ((UniqueTableConstraintContext)_localctx)._columns.Add(((UniqueTableConstraintContext)_localctx)._identifier); - State = 637; + State = 639; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 633; + State = 635; Match(COMMA); - State = 634; + State = 636; ((UniqueTableConstraintContext)_localctx)._identifier = identifier(); ((UniqueTableConstraintContext)_localctx)._columns.Add(((UniqueTableConstraintContext)_localctx)._identifier); } } - State = 639; + State = 641; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 640; + State = 642; Match(RPAREN); } break; @@ -3650,105 +3651,105 @@ public TableConstraintContext tableConstraint() { _localctx = new ForeignKeyTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 644; + State = 646; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 642; + State = 644; Match(CONSTRAINT); - State = 643; + State = 645; ((ForeignKeyTableConstraintContext)_localctx).name = identifier(); } } - State = 646; + State = 648; Match(FOREIGN); - State = 647; + State = 649; Match(KEY); - State = 650; + State = 652; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NO) { { - State = 648; + State = 650; ((ForeignKeyTableConstraintContext)_localctx).noIndex = Match(NO); - State = 649; + State = 651; Match(INDEX); } } - State = 652; + State = 654; Match(LPAREN); - State = 653; + State = 655; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._columns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); - State = 658; + State = 660; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 654; + State = 656; Match(COMMA); - State = 655; + State = 657; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._columns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); } } - State = 660; + State = 662; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 661; + State = 663; Match(RPAREN); - State = 662; + State = 664; Match(REFERENCES); - State = 663; + State = 665; ((ForeignKeyTableConstraintContext)_localctx).refTable = identifier(); - State = 675; + State = 677; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 664; + State = 666; Match(LPAREN); - State = 665; + State = 667; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._refColumns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); - State = 670; + State = 672; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 666; + State = 668; Match(COMMA); - State = 667; + State = 669; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._refColumns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); } } - State = 672; + State = 674; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 673; + State = 675; Match(RPAREN); } } - State = 680; + State = 682; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==ON) { { { - State = 677; + State = 679; foreignKeyAction(); } } - State = 682; + State = 684; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -3758,25 +3759,25 @@ public TableConstraintContext tableConstraint() { _localctx = new CheckTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 685; + State = 687; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 683; + State = 685; Match(CONSTRAINT); - State = 684; + State = 686; ((CheckTableConstraintContext)_localctx).name = identifier(); } } - State = 687; + State = 689; Match(CHECK); - State = 688; + State = 690; Match(LPAREN); - State = 689; + State = 691; checkBody(); - State = 690; + State = 692; Match(RPAREN); } break; @@ -3829,12 +3830,12 @@ public CheckBodyContext checkBody() { try { EnterOuterAlt(_localctx, 1); { - State = 701; + State = 703; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & -1L) != 0) || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 536869887L) != 0)) { { - State = 699; + State = 701; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: @@ -3992,7 +3993,7 @@ public CheckBodyContext checkBody() { case LINE_COMMENT: case BLOCK_COMMENT: { - State = 694; + State = 696; _la = TokenStream.LA(1); if ( _la <= 0 || (_la==LPAREN || _la==RPAREN) ) { ErrorHandler.RecoverInline(this); @@ -4005,11 +4006,11 @@ public CheckBodyContext checkBody() { break; case LPAREN: { - State = 695; + State = 697; Match(LPAREN); - State = 696; + State = 698; checkBody(); - State = 697; + State = 699; Match(RPAREN); } break; @@ -4017,7 +4018,7 @@ public CheckBodyContext checkBody() { throw new NoViableAltException(this); } } - State = 703; + State = 705; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4080,18 +4081,18 @@ public ForeignKeyActionContext foreignKeyAction() { ForeignKeyActionContext _localctx = new ForeignKeyActionContext(Context, State); EnterRule(_localctx, 60, RULE_foreignKeyAction); try { - State = 710; + State = 712; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,76,Context) ) { case 1: _localctx = new OnUpdateActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 704; + State = 706; Match(ON); - State = 705; + State = 707; Match(UPDATE); - State = 706; + State = 708; referentialAction(); } break; @@ -4099,11 +4100,11 @@ public ForeignKeyActionContext foreignKeyAction() { _localctx = new OnDeleteActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 707; + State = 709; Match(ON); - State = 708; + State = 710; Match(DELETE); - State = 709; + State = 711; referentialAction(); } break; @@ -4191,14 +4192,14 @@ public ReferentialActionContext referentialAction() { ReferentialActionContext _localctx = new ReferentialActionContext(Context, State); EnterRule(_localctx, 62, RULE_referentialAction); try { - State = 720; + State = 722; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,77,Context) ) { case 1: _localctx = new CascadeActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 712; + State = 714; Match(CASCADE); } break; @@ -4206,9 +4207,9 @@ public ReferentialActionContext referentialAction() { _localctx = new NoActionActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 713; + State = 715; Match(NO); - State = 714; + State = 716; Match(ACTION); } break; @@ -4216,7 +4217,7 @@ public ReferentialActionContext referentialAction() { _localctx = new RestrictActionContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 715; + State = 717; Match(RESTRICT); } break; @@ -4224,9 +4225,9 @@ public ReferentialActionContext referentialAction() { _localctx = new SetNullActionContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 716; + State = 718; Match(SET); - State = 717; + State = 719; Match(NULL); } break; @@ -4234,9 +4235,9 @@ public ReferentialActionContext referentialAction() { _localctx = new SetDefaultActionContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 718; + State = 720; Match(SET); - State = 719; + State = 721; Match(DEFAULT); } break; @@ -4298,60 +4299,60 @@ public InsertStatementContext insertStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 722; + State = 724; Match(INSERT); - State = 723; + State = 725; Match(INTO); - State = 724; + State = 726; _localctx.table = identifier(); - State = 741; + State = 743; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: case VALUES: case LPAREN: { - State = 736; + State = 738; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,79,Context) ) { case 1: { - State = 725; + State = 727; Match(LPAREN); - State = 726; + State = 728; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); - State = 731; + State = 733; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 727; + State = 729; Match(COMMA); - State = 728; + State = 730; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); } } - State = 733; + State = 735; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 734; + State = 736; Match(RPAREN); } break; } - State = 738; + State = 740; _localctx.source = queryExpression(); } break; case DEFAULT: { - State = 739; + State = 741; Match(DEFAULT); - State = 740; + State = 742; Match(VALUES); } break; @@ -4405,27 +4406,27 @@ public RowValuesContext rowValues() { try { EnterOuterAlt(_localctx, 1); { - State = 743; + State = 745; Match(LPAREN); - State = 744; + State = 746; rowValue(); - State = 749; + State = 751; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 745; + State = 747; Match(COMMA); - State = 746; + State = 748; rowValue(); } } - State = 751; + State = 753; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 752; + State = 754; Match(RPAREN); } } @@ -4463,13 +4464,13 @@ public RowValueContext rowValue() { RowValueContext _localctx = new RowValueContext(Context, State); EnterRule(_localctx, 68, RULE_rowValue); try { - State = 756; + State = 758; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case DEFAULT: EnterOuterAlt(_localctx, 1); { - State = 754; + State = 756; Match(DEFAULT); } break; @@ -4519,7 +4520,7 @@ public RowValueContext rowValue() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 755; + State = 757; expression(0); } break; @@ -4578,40 +4579,40 @@ public QueryExpressionContext queryExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 758; + State = 760; queryTerm(); - State = 764; + State = 766; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 117093590311632896L) != 0)) { { { - State = 759; + State = 761; setOperator(); - State = 760; + State = 762; queryTerm(); } } - State = 766; + State = 768; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 768; + State = 770; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ORDER) { { - State = 767; + State = 769; orderByClause(); } } - State = 771; + State = 773; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OFFSET || _la==FETCH) { { - State = 770; + State = 772; offsetFetchClause(); } } @@ -4694,14 +4695,14 @@ public QueryTermContext queryTerm() { EnterRule(_localctx, 72, RULE_queryTerm); int _la; try { - State = 787; + State = 789; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: _localctx = new SelectTermContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 773; + State = 775; querySpecification(); } break; @@ -4709,11 +4710,11 @@ public QueryTermContext queryTerm() { _localctx = new ParenTermContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 774; + State = 776; Match(LPAREN); - State = 775; + State = 777; queryExpression(); - State = 776; + State = 778; Match(RPAREN); } break; @@ -4721,23 +4722,23 @@ public QueryTermContext queryTerm() { _localctx = new ValuesTermContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 778; + State = 780; Match(VALUES); - State = 779; + State = 781; rowValues(); - State = 784; + State = 786; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 780; + State = 782; Match(COMMA); - State = 781; + State = 783; rowValues(); } } - State = 786; + State = 788; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4782,20 +4783,20 @@ public SetOperatorContext setOperator() { EnterRule(_localctx, 74, RULE_setOperator); int _la; try { - State = 795; + State = 797; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case UNION: EnterOuterAlt(_localctx, 1); { - State = 789; - Match(UNION); State = 791; + Match(UNION); + State = 793; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ALL) { { - State = 790; + State = 792; Match(ALL); } } @@ -4805,14 +4806,14 @@ public SetOperatorContext setOperator() { case INTERSECT: EnterOuterAlt(_localctx, 2); { - State = 793; + State = 795; Match(INTERSECT); } break; case EXCEPT: EnterOuterAlt(_localctx, 3); { - State = 794; + State = 796; Match(EXCEPT); } break; @@ -4881,78 +4882,78 @@ public QuerySpecificationContext querySpecification() { try { EnterOuterAlt(_localctx, 1); { - State = 797; - Match(SELECT); State = 799; + Match(SELECT); + State = 801; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 18014450049089536L) != 0)) { { - State = 798; + State = 800; _localctx.predicate = selectPredicate(); } } - State = 802; + State = 804; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TOP) { { - State = 801; + State = 803; topClause(); } } - State = 804; + State = 806; selectList(); - State = 807; + State = 809; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==INTO) { { - State = 805; + State = 807; Match(INTO); - State = 806; + State = 808; _localctx.into = identifier(); } } - State = 810; + State = 812; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FROM) { { - State = 809; + State = 811; fromClause(); } } - State = 813; + State = 815; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 812; + State = 814; whereClause(); } } - State = 816; + State = 818; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==GROUP) { { - State = 815; + State = 817; groupByClause(); } } - State = 819; + State = 821; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==HAVING) { { - State = 818; + State = 820; havingClause(); } } @@ -4995,7 +4996,7 @@ public SelectPredicateContext selectPredicate() { try { EnterOuterAlt(_localctx, 1); { - State = 821; + State = 823; _la = TokenStream.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 18014450049089536L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -5051,25 +5052,25 @@ public GroupByClauseContext groupByClause() { try { EnterOuterAlt(_localctx, 1); { - State = 823; + State = 825; Match(GROUP); - State = 824; + State = 826; Match(BY); - State = 825; + State = 827; expression(0); - State = 830; + State = 832; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 826; + State = 828; Match(COMMA); - State = 827; + State = 829; expression(0); } } - State = 832; + State = 834; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5111,9 +5112,9 @@ public HavingClauseContext havingClause() { try { EnterOuterAlt(_localctx, 1); { - State = 833; + State = 835; Match(HAVING); - State = 834; + State = 836; expression(0); } } @@ -5171,18 +5172,18 @@ public TopClauseContext topClause() { int _alt; EnterOuterAlt(_localctx, 1); { - State = 836; + State = 838; Match(TOP); - State = 837; + State = 839; topOperand(); - State = 842; + State = 844; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,98,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 838; + State = 840; _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { ErrorHandler.RecoverInline(this); @@ -5191,33 +5192,33 @@ public TopClauseContext topClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 839; + State = 841; topOperand(); } } } - State = 844; + State = 846; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,98,Context); } - State = 846; + State = 848; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PERCENT) { { - State = 845; + State = 847; _localctx.percent = Match(PERCENT); } } - State = 850; + State = 852; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WITH) { { - State = 848; + State = 850; Match(WITH); - State = 849; + State = 851; _localctx.ties = Match(TIES); } } @@ -5261,31 +5262,31 @@ public TopOperandContext topOperand() { TopOperandContext _localctx = new TopOperandContext(Context, State); EnterRule(_localctx, 86, RULE_topOperand); try { - State = 858; + State = 860; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: EnterOuterAlt(_localctx, 1); { - State = 852; + State = 854; Match(INTEGER_LITERAL); } break; case PARAM: EnterOuterAlt(_localctx, 2); { - State = 853; + State = 855; Match(PARAM); } break; case LPAREN: EnterOuterAlt(_localctx, 3); { - State = 854; + State = 856; Match(LPAREN); - State = 855; + State = 857; expression(0); - State = 856; + State = 858; Match(RPAREN); } break; @@ -5346,26 +5347,26 @@ public OffsetFetchClauseContext offsetFetchClause() { EnterRule(_localctx, 88, RULE_offsetFetchClause); int _la; try { - State = 883; + State = 885; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case OFFSET: EnterOuterAlt(_localctx, 1); { - State = 860; + State = 862; Match(OFFSET); - State = 861; + State = 863; _localctx.offset = expression(0); - State = 862; + State = 864; rowKeyword(); - State = 872; + State = 874; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FETCH) { { - State = 863; + State = 865; Match(FETCH); - State = 864; + State = 866; _la = TokenStream.LA(1); if ( !(_la==NEXT || _la==FIRST) ) { ErrorHandler.RecoverInline(this); @@ -5374,24 +5375,24 @@ public OffsetFetchClauseContext offsetFetchClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 865; + State = 867; _localctx.limit = expression(0); - State = 866; + State = 868; rowKeyword(); - State = 870; + State = 872; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case ONLY: { - State = 867; + State = 869; Match(ONLY); } break; case WITH: { - State = 868; + State = 870; Match(WITH); - State = 869; + State = 871; _localctx.ties = Match(TIES); } break; @@ -5406,9 +5407,9 @@ public OffsetFetchClauseContext offsetFetchClause() { case FETCH: EnterOuterAlt(_localctx, 2); { - State = 874; + State = 876; Match(FETCH); - State = 875; + State = 877; _la = TokenStream.LA(1); if ( !(_la==NEXT || _la==FIRST) ) { ErrorHandler.RecoverInline(this); @@ -5417,24 +5418,24 @@ public OffsetFetchClauseContext offsetFetchClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 876; + State = 878; _localctx.limit = expression(0); - State = 877; + State = 879; rowKeyword(); - State = 881; + State = 883; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case ONLY: { - State = 878; + State = 880; Match(ONLY); } break; case WITH: { - State = 879; + State = 881; Match(WITH); - State = 880; + State = 882; _localctx.ties = Match(TIES); } break; @@ -5482,7 +5483,7 @@ public RowKeywordContext rowKeyword() { try { EnterOuterAlt(_localctx, 1); { - State = 885; + State = 887; _la = TokenStream.LA(1); if ( !(_la==ROWS || _la==ROW) ) { ErrorHandler.RecoverInline(this); @@ -5535,13 +5536,13 @@ public SelectListContext selectList() { EnterRule(_localctx, 92, RULE_selectList); int _la; try { - State = 896; + State = 898; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: EnterOuterAlt(_localctx, 1); { - State = 887; + State = 889; Match(STAR); } break; @@ -5591,21 +5592,21 @@ public SelectListContext selectList() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 888; + State = 890; selectItem(); - State = 893; + State = 895; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 889; + State = 891; Match(COMMA); - State = 890; + State = 892; selectItem(); } } - State = 895; + State = 897; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5677,18 +5678,18 @@ public SelectItemContext selectItem() { EnterRule(_localctx, 94, RULE_selectItem); int _la; try { - State = 909; + State = 911; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,110,Context) ) { case 1: _localctx = new QualifiedStarSelectItemContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 898; + State = 900; ((QualifiedStarSelectItemContext)_localctx).qualifier = identifier(); - State = 899; + State = 901; Match(DOT); - State = 900; + State = 902; Match(STAR); } break; @@ -5696,24 +5697,24 @@ public SelectItemContext selectItem() { _localctx = new ExpressionSelectItemContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 902; + State = 904; expression(0); - State = 907; + State = 909; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 904; + State = 906; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 903; + State = 905; Match(AS); } } - State = 906; + State = 908; ((ExpressionSelectItemContext)_localctx).alias = identifier(); } } @@ -5766,23 +5767,23 @@ public FromClauseContext fromClause() { try { EnterOuterAlt(_localctx, 1); { - State = 911; + State = 913; Match(FROM); - State = 912; + State = 914; tableSource(); - State = 917; + State = 919; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 913; + State = 915; Match(COMMA); - State = 914; + State = 916; tableSource(); } } - State = 919; + State = 921; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5830,19 +5831,19 @@ public TableSourceContext tableSource() { try { EnterOuterAlt(_localctx, 1); { - State = 920; + State = 922; tablePrimary(); - State = 924; + State = 926; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 137455468544L) != 0)) { { { - State = 921; + State = 923; joinClause(); } } - State = 926; + State = 928; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5914,6 +5915,9 @@ [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() { return GetRuleContext(0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode AS() { return GetToken(AccessSqlParser.AS, 0); } + [System.Diagnostics.DebuggerNonUserCode] public DerivedColumnsContext derivedColumns() { + return GetRuleContext(0); + } public SubqueryPrimaryContext(TablePrimaryContext context) { CopyFrom(context); } [System.Diagnostics.DebuggerNonUserCode] public override TResult Accept(IParseTreeVisitor visitor) { @@ -5929,31 +5933,31 @@ public TablePrimaryContext tablePrimary() { EnterRule(_localctx, 100, RULE_tablePrimary); int _la; try { - State = 947; + State = 952; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,117,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,118,Context) ) { case 1: _localctx = new NamedTablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 927; + State = 929; ((NamedTablePrimaryContext)_localctx).table = identifier(); - State = 932; + State = 934; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 929; + State = 931; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 928; + State = 930; Match(AS); } } - State = 931; + State = 933; ((NamedTablePrimaryContext)_localctx).alias = identifier(); } } @@ -5964,29 +5968,39 @@ public TablePrimaryContext tablePrimary() { _localctx = new SubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 934; + State = 936; Match(LPAREN); - State = 935; + State = 937; queryExpression(); - State = 936; + State = 938; Match(RPAREN); - State = 941; + State = 946; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { { - State = 938; + State = 940; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 937; + State = 939; Match(AS); } } - State = 940; + State = 942; ((SubqueryPrimaryContext)_localctx).alias = identifier(); + State = 944; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + if (_la==LPAREN) { + { + State = 943; + derivedColumns(); + } + } + } } @@ -5996,11 +6010,11 @@ public TablePrimaryContext tablePrimary() { _localctx = new ParenJoinPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 943; + State = 948; Match(LPAREN); - State = 944; + State = 949; tableSource(); - State = 945; + State = 950; Match(RPAREN); } break; @@ -6097,22 +6111,22 @@ public JoinClauseContext joinClause() { JoinClauseContext _localctx = new JoinClauseContext(Context, State); EnterRule(_localctx, 102, RULE_joinClause); try { - State = 964; + State = 969; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,118,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,119,Context) ) { case 1: _localctx = new ConditionalJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 949; + State = 954; joinType(); - State = 950; + State = 955; Match(JOIN); - State = 951; + State = 956; tablePrimary(); - State = 952; + State = 957; Match(ON); - State = 953; + State = 958; expression(0); } break; @@ -6120,11 +6134,11 @@ public JoinClauseContext joinClause() { _localctx = new CrossJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 955; + State = 960; Match(CROSS); - State = 956; + State = 961; Match(JOIN); - State = 957; + State = 962; tablePrimary(); } break; @@ -6132,11 +6146,11 @@ public JoinClauseContext joinClause() { _localctx = new CrossApplyContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 958; + State = 963; Match(CROSS); - State = 959; + State = 964; Match(APPLY); - State = 960; + State = 965; tablePrimary(); } break; @@ -6144,11 +6158,11 @@ public JoinClauseContext joinClause() { _localctx = new OuterApplyContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 961; + State = 966; Match(OUTER); - State = 962; + State = 967; Match(APPLY); - State = 963; + State = 968; tablePrimary(); } break; @@ -6227,7 +6241,7 @@ public JoinTypeContext joinType() { EnterRule(_localctx, 104, RULE_joinType); int _la; try { - State = 981; + State = 986; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INNER: @@ -6235,12 +6249,12 @@ public JoinTypeContext joinType() { _localctx = new InnerJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 967; + State = 972; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==INNER) { { - State = 966; + State = 971; Match(INNER); } } @@ -6251,14 +6265,14 @@ public JoinTypeContext joinType() { _localctx = new LeftJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 969; + State = 974; Match(LEFT); - State = 971; + State = 976; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 970; + State = 975; Match(OUTER); } } @@ -6269,14 +6283,14 @@ public JoinTypeContext joinType() { _localctx = new RightJoinContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 973; + State = 978; Match(RIGHT); - State = 975; + State = 980; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 974; + State = 979; Match(OUTER); } } @@ -6287,14 +6301,14 @@ public JoinTypeContext joinType() { _localctx = new FullJoinContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 977; + State = 982; Match(FULL); - State = 979; + State = 984; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 978; + State = 983; Match(OUTER); } } @@ -6341,9 +6355,9 @@ public WhereClauseContext whereClause() { try { EnterOuterAlt(_localctx, 1); { - State = 983; + State = 988; Match(WHERE); - State = 984; + State = 989; expression(0); } } @@ -6392,25 +6406,25 @@ public OrderByClauseContext orderByClause() { try { EnterOuterAlt(_localctx, 1); { - State = 986; + State = 991; Match(ORDER); - State = 987; + State = 992; Match(BY); - State = 988; - orderByItem(); State = 993; + orderByItem(); + State = 998; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 989; + State = 994; Match(COMMA); - State = 990; + State = 995; orderByItem(); } } - State = 995; + State = 1000; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -6455,14 +6469,14 @@ public OrderByItemContext orderByItem() { try { EnterOuterAlt(_localctx, 1); { - State = 996; + State = 1001; expression(0); - State = 998; + State = 1003; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ASC || _la==DESC) { { - State = 997; + State = 1002; _localctx.dir = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -6946,7 +6960,7 @@ private ExpressionContext expression(int _p) { int _alt; EnterOuterAlt(_localctx, 1); { - State = 1006; + State = 1011; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case PLUS: @@ -6956,7 +6970,7 @@ private ExpressionContext expression(int _p) { Context = _localctx; _prevctx = _localctx; - State = 1001; + State = 1006; ((NegateExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -6966,7 +6980,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1002; + State = 1007; expression(21); } break; @@ -6976,7 +6990,7 @@ private ExpressionContext expression(int _p) { _localctx = new NotExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 1003; + State = 1008; ((NotExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==NOT || _la==BNOT) ) { @@ -6986,7 +7000,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1004; + State = 1009; expression(7); } break; @@ -7034,7 +7048,7 @@ private ExpressionContext expression(int _p) { _localctx = new PrimaryExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 1005; + State = 1010; primary(); } break; @@ -7042,28 +7056,28 @@ private ExpressionContext expression(int _p) { throw new NoViableAltException(this); } Context.Stop = TokenStream.LT(-1); - State = 1106; + State = 1111; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,136,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,137,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { if ( ParseListeners!=null ) TriggerExitRuleEvent(); _prevctx = _localctx; { - State = 1104; + State = 1109; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,135,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,136,Context) ) { case 1: { _localctx = new PowExprContext(new ExpressionContext(_parentctx, _parentState)); ((PowExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1008; + State = 1013; if (!(Precpred(Context, 22))) throw new FailedPredicateException(this, "Precpred(Context, 22)"); - State = 1009; + State = 1014; Match(CARET); - State = 1010; + State = 1015; ((PowExprContext)_localctx).right = expression(23); } break; @@ -7072,9 +7086,9 @@ private ExpressionContext expression(int _p) { _localctx = new MulDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((MulDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1011; + State = 1016; if (!(Precpred(Context, 20))) throw new FailedPredicateException(this, "Precpred(Context, 20)"); - State = 1012; + State = 1017; ((MulDivExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==STAR || _la==SLASH) ) { @@ -7084,7 +7098,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1013; + State = 1018; ((MulDivExprContext)_localctx).right = expression(21); } break; @@ -7093,11 +7107,11 @@ private ExpressionContext expression(int _p) { _localctx = new IntDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((IntDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1014; + State = 1019; if (!(Precpred(Context, 19))) throw new FailedPredicateException(this, "Precpred(Context, 19)"); - State = 1015; + State = 1020; ((IntDivExprContext)_localctx).op = Match(BACKSLASH); - State = 1016; + State = 1021; ((IntDivExprContext)_localctx).right = expression(20); } break; @@ -7106,11 +7120,11 @@ private ExpressionContext expression(int _p) { _localctx = new ModExprContext(new ExpressionContext(_parentctx, _parentState)); ((ModExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1017; + State = 1022; if (!(Precpred(Context, 18))) throw new FailedPredicateException(this, "Precpred(Context, 18)"); - State = 1018; + State = 1023; ((ModExprContext)_localctx).op = Match(MOD); - State = 1019; + State = 1024; ((ModExprContext)_localctx).right = expression(19); } break; @@ -7119,9 +7133,9 @@ private ExpressionContext expression(int _p) { _localctx = new AddSubExprContext(new ExpressionContext(_parentctx, _parentState)); ((AddSubExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1020; + State = 1025; if (!(Precpred(Context, 17))) throw new FailedPredicateException(this, "Precpred(Context, 17)"); - State = 1021; + State = 1026; ((AddSubExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -7131,7 +7145,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1022; + State = 1027; ((AddSubExprContext)_localctx).right = expression(18); } break; @@ -7140,11 +7154,11 @@ private ExpressionContext expression(int _p) { _localctx = new ConcatExprContext(new ExpressionContext(_parentctx, _parentState)); ((ConcatExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1023; + State = 1028; if (!(Precpred(Context, 16))) throw new FailedPredicateException(this, "Precpred(Context, 16)"); - State = 1024; + State = 1029; ((ConcatExprContext)_localctx).op = Match(AMP); - State = 1025; + State = 1030; ((ConcatExprContext)_localctx).right = expression(17); } break; @@ -7153,9 +7167,9 @@ private ExpressionContext expression(int _p) { _localctx = new ComparisonExprContext(new ExpressionContext(_parentctx, _parentState)); ((ComparisonExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1026; + State = 1031; if (!(Precpred(Context, 15))) throw new FailedPredicateException(this, "Precpred(Context, 15)"); - State = 1027; + State = 1032; ((ComparisonExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(((((_la - 131)) & ~0x3f) == 0 && ((1L << (_la - 131)) & 63L) != 0)) ) { @@ -7165,7 +7179,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1028; + State = 1033; ((ComparisonExprContext)_localctx).right = expression(16); } break; @@ -7174,25 +7188,25 @@ private ExpressionContext expression(int _p) { _localctx = new BetweenExprContext(new ExpressionContext(_parentctx, _parentState)); ((BetweenExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1029; + State = 1034; if (!(Precpred(Context, 14))) throw new FailedPredicateException(this, "Precpred(Context, 14)"); - State = 1031; + State = 1036; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1030; + State = 1035; ((BetweenExprContext)_localctx).not = Match(NOT); } } - State = 1033; + State = 1038; Match(BETWEEN); - State = 1034; + State = 1039; ((BetweenExprContext)_localctx).lo = expression(0); - State = 1035; + State = 1040; Match(AND); - State = 1036; + State = 1041; ((BetweenExprContext)_localctx).hi = expression(15); } break; @@ -7201,21 +7215,21 @@ private ExpressionContext expression(int _p) { _localctx = new LikeExprContext(new ExpressionContext(_parentctx, _parentState)); ((LikeExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1038; + State = 1043; if (!(Precpred(Context, 13))) throw new FailedPredicateException(this, "Precpred(Context, 13)"); - State = 1040; + State = 1045; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1039; + State = 1044; ((LikeExprContext)_localctx).not = Match(NOT); } } - State = 1042; + State = 1047; Match(LIKE); - State = 1043; + State = 1048; ((LikeExprContext)_localctx).right = expression(14); } break; @@ -7224,25 +7238,25 @@ private ExpressionContext expression(int _p) { _localctx = new IsDistinctFromExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsDistinctFromExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1044; + State = 1049; if (!(Precpred(Context, 8))) throw new FailedPredicateException(this, "Precpred(Context, 8)"); - State = 1045; + State = 1050; Match(IS); - State = 1047; + State = 1052; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1046; + State = 1051; ((IsDistinctFromExprContext)_localctx).not = Match(NOT); } } - State = 1049; + State = 1054; Match(DISTINCT); - State = 1050; + State = 1055; Match(FROM); - State = 1051; + State = 1056; ((IsDistinctFromExprContext)_localctx).right = expression(9); } break; @@ -7251,9 +7265,9 @@ private ExpressionContext expression(int _p) { _localctx = new AndExprContext(new ExpressionContext(_parentctx, _parentState)); ((AndExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1052; + State = 1057; if (!(Precpred(Context, 6))) throw new FailedPredicateException(this, "Precpred(Context, 6)"); - State = 1053; + State = 1058; ((AndExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==AND || _la==BAND) ) { @@ -7263,7 +7277,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1054; + State = 1059; ((AndExprContext)_localctx).right = expression(7); } break; @@ -7272,9 +7286,9 @@ private ExpressionContext expression(int _p) { _localctx = new OrExprContext(new ExpressionContext(_parentctx, _parentState)); ((OrExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1055; + State = 1060; if (!(Precpred(Context, 5))) throw new FailedPredicateException(this, "Precpred(Context, 5)"); - State = 1056; + State = 1061; ((OrExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==OR || _la==BOR) ) { @@ -7284,7 +7298,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1057; + State = 1062; ((OrExprContext)_localctx).right = expression(6); } break; @@ -7293,9 +7307,9 @@ private ExpressionContext expression(int _p) { _localctx = new XorExprContext(new ExpressionContext(_parentctx, _parentState)); ((XorExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1058; + State = 1063; if (!(Precpred(Context, 4))) throw new FailedPredicateException(this, "Precpred(Context, 4)"); - State = 1059; + State = 1064; ((XorExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==XOR || _la==BXOR) ) { @@ -7305,7 +7319,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1060; + State = 1065; ((XorExprContext)_localctx).right = expression(5); } break; @@ -7314,11 +7328,11 @@ private ExpressionContext expression(int _p) { _localctx = new EqvExprContext(new ExpressionContext(_parentctx, _parentState)); ((EqvExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1061; + State = 1066; if (!(Precpred(Context, 3))) throw new FailedPredicateException(this, "Precpred(Context, 3)"); - State = 1062; + State = 1067; ((EqvExprContext)_localctx).op = Match(EQV); - State = 1063; + State = 1068; ((EqvExprContext)_localctx).right = expression(4); } break; @@ -7327,11 +7341,11 @@ private ExpressionContext expression(int _p) { _localctx = new ImpExprContext(new ExpressionContext(_parentctx, _parentState)); ((ImpExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1064; + State = 1069; if (!(Precpred(Context, 2))) throw new FailedPredicateException(this, "Precpred(Context, 2)"); - State = 1065; + State = 1070; ((ImpExprContext)_localctx).op = Match(IMP); - State = 1066; + State = 1071; ((ImpExprContext)_localctx).right = expression(3); } break; @@ -7340,25 +7354,25 @@ private ExpressionContext expression(int _p) { _localctx = new InSubqueryExprContext(new ExpressionContext(_parentctx, _parentState)); ((InSubqueryExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1067; + State = 1072; if (!(Precpred(Context, 12))) throw new FailedPredicateException(this, "Precpred(Context, 12)"); - State = 1069; + State = 1074; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1068; + State = 1073; ((InSubqueryExprContext)_localctx).not = Match(NOT); } } - State = 1071; + State = 1076; Match(IN); - State = 1072; + State = 1077; Match(LPAREN); - State = 1073; + State = 1078; ((InSubqueryExprContext)_localctx).sub = queryExpression(); - State = 1074; + State = 1079; Match(RPAREN); } break; @@ -7367,43 +7381,43 @@ private ExpressionContext expression(int _p) { _localctx = new InExprContext(new ExpressionContext(_parentctx, _parentState)); ((InExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1076; + State = 1081; if (!(Precpred(Context, 11))) throw new FailedPredicateException(this, "Precpred(Context, 11)"); - State = 1078; + State = 1083; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1077; + State = 1082; ((InExprContext)_localctx).not = Match(NOT); } } - State = 1080; + State = 1085; Match(IN); - State = 1081; + State = 1086; Match(LPAREN); - State = 1082; + State = 1087; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); - State = 1087; + State = 1092; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1083; + State = 1088; Match(COMMA); - State = 1084; + State = 1089; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); } } - State = 1089; + State = 1094; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 1090; + State = 1095; Match(RPAREN); } break; @@ -7412,21 +7426,21 @@ private ExpressionContext expression(int _p) { _localctx = new IsNullExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsNullExprContext)_localctx).operand = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1092; + State = 1097; if (!(Precpred(Context, 10))) throw new FailedPredicateException(this, "Precpred(Context, 10)"); - State = 1093; + State = 1098; Match(IS); - State = 1095; + State = 1100; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1094; + State = 1099; ((IsNullExprContext)_localctx).not = Match(NOT); } } - State = 1097; + State = 1102; Match(NULL); } break; @@ -7435,21 +7449,21 @@ private ExpressionContext expression(int _p) { _localctx = new IsTruthExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsTruthExprContext)_localctx).operand = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1098; + State = 1103; if (!(Precpred(Context, 9))) throw new FailedPredicateException(this, "Precpred(Context, 9)"); - State = 1099; + State = 1104; Match(IS); - State = 1101; + State = 1106; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1100; + State = 1105; ((IsTruthExprContext)_localctx).not = Match(NOT); } } - State = 1103; + State = 1108; ((IsTruthExprContext)_localctx).truth = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==TRUE || _la==FALSE) ) { @@ -7464,9 +7478,9 @@ private ExpressionContext expression(int _p) { } } } - State = 1108; + State = 1113; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,136,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,137,Context); } } } @@ -7610,14 +7624,14 @@ public PrimaryContext primary() { PrimaryContext _localctx = new PrimaryContext(Context, State); EnterRule(_localctx, 114, RULE_primary); try { - State = 1128; + State = 1133; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,137,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,138,Context) ) { case 1: _localctx = new LiteralPrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1109; + State = 1114; literal(); } break; @@ -7625,7 +7639,7 @@ public PrimaryContext primary() { _localctx = new CasePrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1110; + State = 1115; caseExpression(); } break; @@ -7633,7 +7647,7 @@ public PrimaryContext primary() { _localctx = new FunctionCallPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1111; + State = 1116; functionCall(); } break; @@ -7641,7 +7655,7 @@ public PrimaryContext primary() { _localctx = new ColumnPrimaryContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 1112; + State = 1117; columnRef(); } break; @@ -7649,7 +7663,7 @@ public PrimaryContext primary() { _localctx = new ParamPrimaryContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 1113; + State = 1118; Match(PARAM); } break; @@ -7657,7 +7671,7 @@ public PrimaryContext primary() { _localctx = new SystemVariablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 1114; + State = 1119; Match(SYSVAR); } break; @@ -7665,13 +7679,13 @@ public PrimaryContext primary() { _localctx = new ExistsPrimaryContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 1115; + State = 1120; Match(EXISTS); - State = 1116; + State = 1121; Match(LPAREN); - State = 1117; + State = 1122; queryExpression(); - State = 1118; + State = 1123; Match(RPAREN); } break; @@ -7679,11 +7693,11 @@ public PrimaryContext primary() { _localctx = new ScalarSubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 1120; + State = 1125; Match(LPAREN); - State = 1121; + State = 1126; queryExpression(); - State = 1122; + State = 1127; Match(RPAREN); } break; @@ -7691,11 +7705,11 @@ public PrimaryContext primary() { _localctx = new ParenPrimaryContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 1124; + State = 1129; Match(LPAREN); - State = 1125; + State = 1130; expression(0); - State = 1126; + State = 1131; Match(RPAREN); } break; @@ -7751,45 +7765,45 @@ public CaseExpressionContext caseExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 1130; + State = 1135; Match(CASE); - State = 1132; + State = 1137; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775694237952L) != 0) || ((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & -283699838490247167L) != 0) || ((((_la - 149)) & ~0x3f) == 0 && ((1L << (_la - 149)) & 31L) != 0)) { { - State = 1131; + State = 1136; _localctx.operand = expression(0); } } - State = 1135; + State = 1140; ErrorHandler.Sync(this); _la = TokenStream.LA(1); do { { { - State = 1134; + State = 1139; caseWhen(); } } - State = 1137; + State = 1142; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } while ( _la==WHEN ); - State = 1141; + State = 1146; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ELSE) { { - State = 1139; + State = 1144; Match(ELSE); - State = 1140; + State = 1145; _localctx.elseResult = expression(0); } } - State = 1143; + State = 1148; Match(END); } } @@ -7835,13 +7849,13 @@ public CaseWhenContext caseWhen() { try { EnterOuterAlt(_localctx, 1); { - State = 1145; + State = 1150; Match(WHEN); - State = 1146; + State = 1151; _localctx.condition = expression(0); - State = 1147; + State = 1152; Match(THEN); - State = 1148; + State = 1153; _localctx.result = expression(0); } } @@ -7914,16 +7928,16 @@ public FunctionCallContext functionCall() { try { EnterOuterAlt(_localctx, 1); { - State = 1150; + State = 1155; _localctx.name = functionName(); - State = 1151; + State = 1156; Match(LPAREN); - State = 1164; + State = 1169; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: { - State = 1152; + State = 1157; _localctx.star = Match(STAR); } break; @@ -7974,31 +7988,31 @@ public FunctionCallContext functionCall() { case IDENTIFIER: { { - State = 1154; + State = 1159; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==DISTINCT) { { - State = 1153; + State = 1158; _localctx.distinct = Match(DISTINCT); } } - State = 1156; - expression(0); State = 1161; + expression(0); + State = 1166; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1157; + State = 1162; Match(COMMA); - State = 1158; + State = 1163; expression(0); } } - State = 1163; + State = 1168; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -8010,56 +8024,56 @@ public FunctionCallContext functionCall() { default: break; } - State = 1166; + State = 1171; Match(RPAREN); - State = 1168; + State = 1173; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,144,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,145,Context) ) { case 1: { - State = 1167; + State = 1172; withinGroup(); } break; } - State = 1171; + State = 1176; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,145,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,146,Context) ) { case 1: { - State = 1170; + State = 1175; filterClause(); } break; } - State = 1181; + State = 1186; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,148,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,149,Context) ) { case 1: { - State = 1174; + State = 1179; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FROM) { { - State = 1173; + State = 1178; nthRowFrom(); } } - State = 1177; + State = 1182; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==IGNORE || _la==RESPECT) { { - State = 1176; + State = 1181; nullTreatment(); } } - State = 1179; + State = 1184; Match(OVER); - State = 1180; + State = 1185; windowSpecification(); } break; @@ -8104,7 +8118,7 @@ public FunctionNameContext functionName() { FunctionNameContext _localctx = new FunctionNameContext(Context, State); EnterRule(_localctx, 122, RULE_functionName); try { - State = 1189; + State = 1194; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -8128,42 +8142,42 @@ public FunctionNameContext functionName() { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1183; + State = 1188; identifier(); } break; case LEFT: EnterOuterAlt(_localctx, 2); { - State = 1184; + State = 1189; Match(LEFT); } break; case RIGHT: EnterOuterAlt(_localctx, 3); { - State = 1185; + State = 1190; Match(RIGHT); } break; case ASC: EnterOuterAlt(_localctx, 4); { - State = 1186; + State = 1191; Match(ASC); } break; case FIRST: EnterOuterAlt(_localctx, 5); { - State = 1187; + State = 1192; Match(FIRST); } break; case PARTITION: EnterOuterAlt(_localctx, 6); { - State = 1188; + State = 1193; Match(PARTITION); } break; @@ -8187,7 +8201,7 @@ public partial class ColumnRefContext : ParserRuleContext { public IToken _DOT; public IList _separators = new List(); public IToken _BANG; - public IToken _tset3289; + public IToken _tset3292; public MemberNameContext _memberName; public IList _rest = new List(); [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() { @@ -8229,35 +8243,35 @@ public ColumnRefContext columnRef() { int _alt; EnterOuterAlt(_localctx, 1); { - State = 1191; - _localctx.first = identifier(); State = 1196; + _localctx.first = identifier(); + State = 1201; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,150,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,151,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 1192; - _localctx._tset3289 = TokenStream.LT(1); + State = 1197; + _localctx._tset3292 = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==DOT || _la==BANG) ) { - _localctx._tset3289 = ErrorHandler.RecoverInline(this); + _localctx._tset3292 = ErrorHandler.RecoverInline(this); } else { ErrorHandler.ReportMatch(this); Consume(); } - _localctx._separators.Add(_localctx._tset3289); - State = 1193; + _localctx._separators.Add(_localctx._tset3292); + State = 1198; _localctx._memberName = memberName(); _localctx._rest.Add(_localctx._memberName); } } } - State = 1198; + State = 1203; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,150,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,151,Context); } } } @@ -8297,7 +8311,7 @@ public MemberNameContext memberName() { MemberNameContext _localctx = new MemberNameContext(Context, State); EnterRule(_localctx, 126, RULE_memberName); try { - State = 1201; + State = 1206; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -8321,7 +8335,7 @@ public MemberNameContext memberName() { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1199; + State = 1204; identifier(); } break; @@ -8434,7 +8448,7 @@ public MemberNameContext memberName() { case NULL: EnterOuterAlt(_localctx, 2); { - State = 1200; + State = 1205; reservedKeyword(); } break; @@ -8478,27 +8492,27 @@ public IdentifierContext identifier() { IdentifierContext _localctx = new IdentifierContext(Context, State); EnterRule(_localctx, 128, RULE_identifier); try { - State = 1207; + State = 1212; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1203; + State = 1208; Match(IDENTIFIER); } break; case BRACKET_ID: EnterOuterAlt(_localctx, 2); { - State = 1204; + State = 1209; Match(BRACKET_ID); } break; case BACKTICK_ID: EnterOuterAlt(_localctx, 3); { - State = 1205; + State = 1210; Match(BACKTICK_ID); } break; @@ -8520,7 +8534,7 @@ public IdentifierContext identifier() { case FILTER: EnterOuterAlt(_localctx, 4); { - State = 1206; + State = 1211; nonReservedKeyword(); } break; @@ -8652,14 +8666,14 @@ public LiteralContext literal() { EnterRule(_localctx, 130, RULE_literal); int _la; try { - State = 1218; + State = 1223; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: _localctx = new IntLiteralContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1209; + State = 1214; Match(INTEGER_LITERAL); } break; @@ -8667,7 +8681,7 @@ public LiteralContext literal() { _localctx = new NumberLiteralContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1210; + State = 1215; Match(NUMBER_LITERAL); } break; @@ -8675,7 +8689,7 @@ public LiteralContext literal() { _localctx = new HexLiteralContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1211; + State = 1216; Match(HEX_LITERAL); } break; @@ -8683,7 +8697,7 @@ public LiteralContext literal() { _localctx = new StringLiteralContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 1212; + State = 1217; Match(STRING_LITERAL); } break; @@ -8691,7 +8705,7 @@ public LiteralContext literal() { _localctx = new DateLiteralContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 1213; + State = 1218; Match(DATE_LITERAL); } break; @@ -8699,7 +8713,7 @@ public LiteralContext literal() { _localctx = new GuidLiteralContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 1214; + State = 1219; Match(GUID_LITERAL); } break; @@ -8709,7 +8723,7 @@ public LiteralContext literal() { _localctx = new TrueLiteralContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 1215; + State = 1220; _la = TokenStream.LA(1); if ( !(_la==ON || _la==TRUE || _la==YES) ) { ErrorHandler.RecoverInline(this); @@ -8726,7 +8740,7 @@ public LiteralContext literal() { _localctx = new FalseLiteralContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 1216; + State = 1221; _la = TokenStream.LA(1); if ( !(((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & 10485761L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -8741,7 +8755,7 @@ public LiteralContext literal() { _localctx = new NullLiteralContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 1217; + State = 1222; Match(NULL); } break; @@ -8815,21 +8829,21 @@ public TransactionStatementContext transactionStatement() { EnterRule(_localctx, 132, RULE_transactionStatement); int _la; try { - State = 1232; + State = 1237; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BEGIN: _localctx = new BeginTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1220; + State = 1225; Match(BEGIN); - State = 1222; + State = 1227; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1221; + State = 1226; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8847,14 +8861,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new CommitTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1224; + State = 1229; Match(COMMIT); - State = 1226; + State = 1231; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1225; + State = 1230; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8872,14 +8886,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new RollbackTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1228; + State = 1233; Match(ROLLBACK); - State = 1230; + State = 1235; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1229; + State = 1234; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8933,9 +8947,9 @@ public StandaloneExpressionContext standaloneExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 1234; + State = 1239; expression(0); - State = 1235; + State = 1240; Match(Eof); } } @@ -8994,61 +9008,61 @@ public WindowSpecificationContext windowSpecification() { try { EnterOuterAlt(_localctx, 1); { - State = 1237; + State = 1242; Match(LPAREN); - State = 1248; + State = 1253; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PARTITION) { { - State = 1238; + State = 1243; Match(PARTITION); - State = 1239; + State = 1244; Match(BY); - State = 1240; + State = 1245; _localctx._expression = expression(0); _localctx._partition.Add(_localctx._expression); - State = 1245; + State = 1250; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1241; + State = 1246; Match(COMMA); - State = 1242; + State = 1247; _localctx._expression = expression(0); _localctx._partition.Add(_localctx._expression); } } - State = 1247; + State = 1252; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } } } - State = 1251; + State = 1256; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ORDER) { { - State = 1250; + State = 1255; orderByClause(); } } - State = 1254; + State = 1259; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 6917529027641081857L) != 0)) { { - State = 1253; + State = 1258; windowFrame(); } } - State = 1256; + State = 1261; Match(RPAREN); } } @@ -9087,7 +9101,7 @@ public ClusteredOptionContext clusteredOption() { try { EnterOuterAlt(_localctx, 1); { - State = 1258; + State = 1263; _la = TokenStream.LA(1); if ( !(_la==CLUSTERED || _la==NONCLUSTERED) ) { ErrorHandler.RecoverInline(this); @@ -9150,7 +9164,7 @@ public WindowFrameContext windowFrame() { try { EnterOuterAlt(_localctx, 1); { - State = 1260; + State = 1265; _localctx.unit = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 6917529027641081857L) != 0)) ) { @@ -9160,18 +9174,18 @@ public WindowFrameContext windowFrame() { ErrorHandler.ReportMatch(this); Consume(); } - State = 1267; + State = 1272; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BETWEEN: { - State = 1261; + State = 1266; Match(BETWEEN); - State = 1262; + State = 1267; _localctx.start = frameBound(); - State = 1263; + State = 1268; Match(AND); - State = 1264; + State = 1269; _localctx.end = frameBound(); } break; @@ -9220,21 +9234,21 @@ public WindowFrameContext windowFrame() { case BACKTICK_ID: case IDENTIFIER: { - State = 1266; + State = 1271; _localctx.start = frameBound(); } break; default: throw new NoViableAltException(this); } - State = 1271; + State = 1276; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==EXCLUDE) { { - State = 1269; + State = 1274; Match(EXCLUDE); - State = 1270; + State = 1275; _localctx.exclusion = frameExclusion(); } } @@ -9282,15 +9296,15 @@ public FrameBoundContext frameBound() { EnterRule(_localctx, 142, RULE_frameBound); int _la; try { - State = 1280; + State = 1285; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,164,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,165,Context) ) { case 1: EnterOuterAlt(_localctx, 1); { - State = 1273; + State = 1278; Match(UNBOUNDED); - State = 1274; + State = 1279; _localctx.direction = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PRECEDING || _la==FOLLOWING) ) { @@ -9305,18 +9319,18 @@ public FrameBoundContext frameBound() { case 2: EnterOuterAlt(_localctx, 2); { - State = 1275; + State = 1280; Match(CURRENT); - State = 1276; + State = 1281; Match(ROW); } break; case 3: EnterOuterAlt(_localctx, 3); { - State = 1277; + State = 1282; _localctx.offset = expression(0); - State = 1278; + State = 1283; _localctx.direction = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PRECEDING || _la==FOLLOWING) ) { @@ -9366,38 +9380,38 @@ public FrameExclusionContext frameExclusion() { FrameExclusionContext _localctx = new FrameExclusionContext(Context, State); EnterRule(_localctx, 144, RULE_frameExclusion); try { - State = 1288; + State = 1293; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case CURRENT: EnterOuterAlt(_localctx, 1); { - State = 1282; + State = 1287; Match(CURRENT); - State = 1283; + State = 1288; Match(ROW); } break; case GROUP: EnterOuterAlt(_localctx, 2); { - State = 1284; + State = 1289; Match(GROUP); } break; case TIES: EnterOuterAlt(_localctx, 3); { - State = 1285; + State = 1290; Match(TIES); } break; case NO: EnterOuterAlt(_localctx, 4); { - State = 1286; + State = 1291; Match(NO); - State = 1287; + State = 1292; Match(OTHERS); } break; @@ -9454,7 +9468,7 @@ public NonReservedKeywordContext nonReservedKeyword() { try { EnterOuterAlt(_localctx, 1); { - State = 1290; + State = 1295; _la = TokenStream.LA(1); if ( !(((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 131067L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -9605,7 +9619,7 @@ public ReservedKeywordContext reservedKeyword() { try { EnterOuterAlt(_localctx, 1); { - State = 1292; + State = 1297; _la = TokenStream.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & 43980465111039L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -9656,15 +9670,15 @@ public FilterClauseContext filterClause() { try { EnterOuterAlt(_localctx, 1); { - State = 1294; + State = 1299; Match(FILTER); - State = 1295; + State = 1300; Match(LPAREN); - State = 1296; + State = 1301; Match(WHERE); - State = 1297; + State = 1302; _localctx.condition = expression(0); - State = 1298; + State = 1303; Match(RPAREN); } } @@ -9707,15 +9721,15 @@ public WithinGroupContext withinGroup() { try { EnterOuterAlt(_localctx, 1); { - State = 1300; + State = 1305; Match(WITHIN); - State = 1301; + State = 1306; Match(GROUP); - State = 1302; + State = 1307; Match(LPAREN); - State = 1303; + State = 1308; orderByClause(); - State = 1304; + State = 1309; Match(RPAREN); } } @@ -9756,9 +9770,9 @@ public NthRowFromContext nthRowFrom() { try { EnterOuterAlt(_localctx, 1); { - State = 1306; + State = 1311; Match(FROM); - State = 1307; + State = 1312; _localctx.edge = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==FIRST || _la==LAST) ) { @@ -9807,7 +9821,7 @@ public NullTreatmentContext nullTreatment() { try { EnterOuterAlt(_localctx, 1); { - State = 1309; + State = 1314; _localctx.treatment = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==IGNORE || _la==RESPECT) ) { @@ -9817,7 +9831,7 @@ public NullTreatmentContext nullTreatment() { ErrorHandler.ReportMatch(this); Consume(); } - State = 1310; + State = 1315; Match(NULLS); } } @@ -9832,6 +9846,79 @@ public NullTreatmentContext nullTreatment() { return _localctx; } + public partial class DerivedColumnsContext : ParserRuleContext { + public IdentifierContext _identifier; + public IList _names = new List(); + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode LPAREN() { return GetToken(AccessSqlParser.LPAREN, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode RPAREN() { return GetToken(AccessSqlParser.RPAREN, 0); } + [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext[] identifier() { + return GetRuleContexts(); + } + [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier(int i) { + return GetRuleContext(i); + } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode[] COMMA() { return GetTokens(AccessSqlParser.COMMA); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMMA(int i) { + return GetToken(AccessSqlParser.COMMA, i); + } + public DerivedColumnsContext(ParserRuleContext parent, int invokingState) + : base(parent, invokingState) + { + } + public override int RuleIndex { get { return RULE_derivedColumns; } } + [System.Diagnostics.DebuggerNonUserCode] + public override TResult Accept(IParseTreeVisitor visitor) { + IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor; + if (typedVisitor != null) return typedVisitor.VisitDerivedColumns(this); + else return visitor.VisitChildren(this); + } + } + + [RuleVersion(0)] + public DerivedColumnsContext derivedColumns() { + DerivedColumnsContext _localctx = new DerivedColumnsContext(Context, State); + EnterRule(_localctx, 158, RULE_derivedColumns); + int _la; + try { + EnterOuterAlt(_localctx, 1); + { + State = 1317; + Match(LPAREN); + State = 1318; + _localctx._identifier = identifier(); + _localctx._names.Add(_localctx._identifier); + State = 1323; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + while (_la==COMMA) { + { + { + State = 1319; + Match(COMMA); + State = 1320; + _localctx._identifier = identifier(); + _localctx._names.Add(_localctx._identifier); + } + } + State = 1325; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + } + State = 1326; + Match(RPAREN); + } + } + catch (RecognitionException re) { + _localctx.exception = re; + ErrorHandler.ReportError(this, re); + ErrorHandler.Recover(this, re); + } + finally { + ExitRule(); + } + return _localctx; + } + public override bool Sempred(RuleContext _localctx, int ruleIndex, int predIndex) { switch (ruleIndex) { case 56: return expression_sempred((ExpressionContext)_localctx, predIndex); @@ -9864,7 +9951,7 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { } private static int[] _serializedATN = { - 4,1,156,1313,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,156,1329,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, @@ -9875,482 +9962,488 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { 2,57,7,57,2,58,7,58,2,59,7,59,2,60,7,60,2,61,7,61,2,62,7,62,2,63,7,63, 2,64,7,64,2,65,7,65,2,66,7,66,2,67,7,67,2,68,7,68,2,69,7,69,2,70,7,70, 2,71,7,71,2,72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77, - 2,78,7,78,1,0,3,0,160,8,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0, - 1,0,1,0,1,0,3,0,176,8,0,1,0,3,0,179,8,0,1,0,1,0,1,1,1,1,3,1,185,8,1,1, - 1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2, - 3,2,205,8,2,1,3,1,3,1,3,1,3,1,3,5,3,212,8,3,10,3,12,3,215,9,3,3,3,217, - 8,3,1,4,1,4,1,4,1,4,5,4,223,8,4,10,4,12,4,226,9,4,1,4,1,4,1,4,1,4,5,4, - 232,8,4,10,4,12,4,235,9,4,1,4,3,4,238,8,4,1,5,1,5,1,5,1,5,1,6,1,6,1,6, - 1,6,1,6,1,6,3,6,250,8,6,1,6,1,6,1,6,1,6,5,6,256,8,6,10,6,12,6,259,9,6, - 1,6,3,6,262,8,6,1,7,1,7,1,7,1,7,5,7,268,8,7,10,7,12,7,271,9,7,1,8,1,8, - 3,8,275,8,8,1,8,3,8,278,8,8,1,9,1,9,1,9,1,9,5,9,284,8,9,10,9,12,9,287, - 9,9,1,9,1,9,1,10,1,10,3,10,293,8,10,1,10,1,10,1,10,1,10,1,10,1,10,5,10, - 301,8,10,10,10,12,10,304,9,10,1,10,1,10,5,10,308,8,10,10,10,12,10,311, - 9,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,11,1,11,5,11,322,8,11,10,11, - 12,11,325,9,11,1,11,1,11,3,11,329,8,11,1,11,1,11,1,11,1,12,1,12,1,12,1, - 12,3,12,338,8,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,5,13,347,8,13,10,13, - 12,13,350,9,13,1,13,1,13,1,13,1,13,1,13,5,13,357,8,13,10,13,12,13,360, - 9,13,3,13,362,8,13,1,14,1,14,1,14,1,15,1,15,3,15,369,8,15,1,16,1,16,1, - 16,1,16,1,16,1,17,1,17,3,17,378,8,17,1,17,1,17,1,17,1,17,1,17,3,17,385, - 8,17,1,17,1,17,1,17,5,17,390,8,17,10,17,12,17,393,9,17,1,17,1,17,3,17, - 397,8,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,406,8,17,1,17,1,17,1, - 17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1, - 17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,433,8,17,1,18,1,18,1,18, - 1,18,1,18,3,18,440,8,18,1,19,1,19,3,19,444,8,19,1,19,1,19,1,19,1,19,1, - 19,1,19,1,19,1,19,5,19,454,8,19,10,19,12,19,457,9,19,1,19,1,19,1,19,3, - 19,462,8,19,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20, - 1,20,1,20,1,20,3,20,479,8,20,1,21,1,21,3,21,483,8,21,1,22,1,22,1,22,1, - 22,1,22,3,22,490,8,22,1,23,1,23,1,23,3,23,495,8,23,1,23,5,23,498,8,23, - 10,23,12,23,501,9,23,1,24,1,24,1,24,1,24,1,24,1,25,1,25,3,25,510,8,25, - 1,25,3,25,513,8,25,1,25,3,25,516,8,25,1,25,1,25,1,25,1,25,3,25,522,8,25, - 1,25,1,25,3,25,526,8,25,1,26,3,26,529,8,26,1,26,1,26,1,27,1,27,1,27,1, - 27,1,27,1,27,1,27,1,27,1,27,3,27,542,8,27,1,27,1,27,1,27,1,27,1,27,1,27, - 1,27,3,27,551,8,27,1,27,1,27,1,27,3,27,556,8,27,1,27,1,27,3,27,560,8,27, - 1,27,1,27,3,27,564,8,27,1,27,1,27,3,27,568,8,27,1,27,1,27,1,27,1,27,1, - 27,1,27,5,27,576,8,27,10,27,12,27,579,9,27,1,27,1,27,3,27,583,8,27,1,27, - 5,27,586,8,27,10,27,12,27,589,9,27,1,27,1,27,1,27,1,27,1,27,3,27,596,8, - 27,1,27,1,27,3,27,600,8,27,3,27,602,8,27,1,28,1,28,3,28,606,8,28,1,28, - 1,28,1,28,3,28,611,8,28,1,28,1,28,1,28,1,28,5,28,617,8,28,10,28,12,28, - 620,9,28,1,28,1,28,1,28,1,28,3,28,626,8,28,1,28,1,28,3,28,630,8,28,1,28, - 1,28,1,28,1,28,5,28,636,8,28,10,28,12,28,639,9,28,1,28,1,28,1,28,1,28, - 3,28,645,8,28,1,28,1,28,1,28,1,28,3,28,651,8,28,1,28,1,28,1,28,1,28,5, - 28,657,8,28,10,28,12,28,660,9,28,1,28,1,28,1,28,1,28,1,28,1,28,1,28,5, - 28,669,8,28,10,28,12,28,672,9,28,1,28,1,28,3,28,676,8,28,1,28,5,28,679, - 8,28,10,28,12,28,682,9,28,1,28,1,28,3,28,686,8,28,1,28,1,28,1,28,1,28, - 1,28,3,28,693,8,28,1,29,1,29,1,29,1,29,1,29,5,29,700,8,29,10,29,12,29, - 703,9,29,1,30,1,30,1,30,1,30,1,30,1,30,3,30,711,8,30,1,31,1,31,1,31,1, - 31,1,31,1,31,1,31,1,31,3,31,721,8,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32, - 5,32,730,8,32,10,32,12,32,733,9,32,1,32,1,32,3,32,737,8,32,1,32,1,32,1, - 32,3,32,742,8,32,1,33,1,33,1,33,1,33,5,33,748,8,33,10,33,12,33,751,9,33, - 1,33,1,33,1,34,1,34,3,34,757,8,34,1,35,1,35,1,35,1,35,5,35,763,8,35,10, - 35,12,35,766,9,35,1,35,3,35,769,8,35,1,35,3,35,772,8,35,1,36,1,36,1,36, - 1,36,1,36,1,36,1,36,1,36,1,36,5,36,783,8,36,10,36,12,36,786,9,36,3,36, - 788,8,36,1,37,1,37,3,37,792,8,37,1,37,1,37,3,37,796,8,37,1,38,1,38,3,38, - 800,8,38,1,38,3,38,803,8,38,1,38,1,38,1,38,3,38,808,8,38,1,38,3,38,811, - 8,38,1,38,3,38,814,8,38,1,38,3,38,817,8,38,1,38,3,38,820,8,38,1,39,1,39, - 1,40,1,40,1,40,1,40,1,40,5,40,829,8,40,10,40,12,40,832,9,40,1,41,1,41, - 1,41,1,42,1,42,1,42,1,42,5,42,841,8,42,10,42,12,42,844,9,42,1,42,3,42, - 847,8,42,1,42,1,42,3,42,851,8,42,1,43,1,43,1,43,1,43,1,43,1,43,3,43,859, - 8,43,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,871,8,44,3, - 44,873,8,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,882,8,44,3,44,884, - 8,44,1,45,1,45,1,46,1,46,1,46,1,46,5,46,892,8,46,10,46,12,46,895,9,46, - 3,46,897,8,46,1,47,1,47,1,47,1,47,1,47,1,47,3,47,905,8,47,1,47,3,47,908, - 8,47,3,47,910,8,47,1,48,1,48,1,48,1,48,5,48,916,8,48,10,48,12,48,919,9, - 48,1,49,1,49,5,49,923,8,49,10,49,12,49,926,9,49,1,50,1,50,3,50,930,8,50, - 1,50,3,50,933,8,50,1,50,1,50,1,50,1,50,3,50,939,8,50,1,50,3,50,942,8,50, - 1,50,1,50,1,50,1,50,3,50,948,8,50,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,1,51,3,51,965,8,51,1,52,3,52,968,8,52, - 1,52,1,52,3,52,972,8,52,1,52,1,52,3,52,976,8,52,1,52,1,52,3,52,980,8,52, - 3,52,982,8,52,1,53,1,53,1,53,1,54,1,54,1,54,1,54,1,54,5,54,992,8,54,10, - 54,12,54,995,9,54,1,55,1,55,3,55,999,8,55,1,56,1,56,1,56,1,56,1,56,1,56, - 3,56,1007,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56, - 1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1032, - 8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1041,8,56,1,56,1,56,1,56, - 1,56,1,56,3,56,1048,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56, - 1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1070,8,56, - 1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1079,8,56,1,56,1,56,1,56,1,56, - 1,56,5,56,1086,8,56,10,56,12,56,1089,9,56,1,56,1,56,1,56,1,56,1,56,3,56, - 1096,8,56,1,56,1,56,1,56,1,56,3,56,1102,8,56,1,56,5,56,1105,8,56,10,56, - 12,56,1108,9,56,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, - 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,3,57,1129,8,57,1,58,1,58,3,58, - 1133,8,58,1,58,4,58,1136,8,58,11,58,12,58,1137,1,58,1,58,3,58,1142,8,58, - 1,58,1,58,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60,1,60,3,60,1155,8,60, - 1,60,1,60,1,60,5,60,1160,8,60,10,60,12,60,1163,9,60,3,60,1165,8,60,1,60, - 1,60,3,60,1169,8,60,1,60,3,60,1172,8,60,1,60,3,60,1175,8,60,1,60,3,60, - 1178,8,60,1,60,1,60,3,60,1182,8,60,1,61,1,61,1,61,1,61,1,61,1,61,3,61, - 1190,8,61,1,62,1,62,1,62,5,62,1195,8,62,10,62,12,62,1198,9,62,1,63,1,63, - 3,63,1202,8,63,1,64,1,64,1,64,1,64,3,64,1208,8,64,1,65,1,65,1,65,1,65, - 1,65,1,65,1,65,1,65,1,65,3,65,1219,8,65,1,66,1,66,3,66,1223,8,66,1,66, - 1,66,3,66,1227,8,66,1,66,1,66,3,66,1231,8,66,3,66,1233,8,66,1,67,1,67, - 1,67,1,68,1,68,1,68,1,68,1,68,1,68,5,68,1244,8,68,10,68,12,68,1247,9,68, - 3,68,1249,8,68,1,68,3,68,1252,8,68,1,68,3,68,1255,8,68,1,68,1,68,1,69, - 1,69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,3,70,1268,8,70,1,70,1,70,3,70, - 1272,8,70,1,71,1,71,1,71,1,71,1,71,1,71,1,71,3,71,1281,8,71,1,72,1,72, - 1,72,1,72,1,72,1,72,3,72,1289,8,72,1,73,1,73,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,76,1,77,1,77,1,77,1,78,1,78, - 1,78,1,78,0,1,112,79,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,120,122, - 124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,0, - 26,1,0,101,102,1,0,103,104,1,0,93,94,1,0,137,138,2,0,34,35,54,54,1,0,128, - 129,1,0,47,48,1,0,49,50,2,0,8,8,15,15,1,0,124,125,1,0,131,136,2,0,6,6, - 12,12,2,0,7,7,13,13,2,0,9,9,14,14,1,0,105,106,1,0,140,141,3,0,25,25,105, - 105,107,107,3,0,85,85,106,106,108,108,1,0,62,63,2,0,87,87,89,89,2,0,49, - 49,110,111,1,0,113,114,2,0,107,108,110,123,2,0,1,106,109,109,2,0,48,48, - 120,120,2,0,96,96,121,121,1498,0,159,1,0,0,0,2,182,1,0,0,0,4,204,1,0,0, - 0,6,206,1,0,0,0,8,218,1,0,0,0,10,239,1,0,0,0,12,243,1,0,0,0,14,263,1,0, - 0,0,16,272,1,0,0,0,18,279,1,0,0,0,20,290,1,0,0,0,22,314,1,0,0,0,24,333, - 1,0,0,0,26,361,1,0,0,0,28,363,1,0,0,0,30,368,1,0,0,0,32,370,1,0,0,0,34, - 432,1,0,0,0,36,439,1,0,0,0,38,441,1,0,0,0,40,478,1,0,0,0,42,480,1,0,0, - 0,44,489,1,0,0,0,46,491,1,0,0,0,48,502,1,0,0,0,50,509,1,0,0,0,52,528,1, - 0,0,0,54,601,1,0,0,0,56,692,1,0,0,0,58,701,1,0,0,0,60,710,1,0,0,0,62,720, - 1,0,0,0,64,722,1,0,0,0,66,743,1,0,0,0,68,756,1,0,0,0,70,758,1,0,0,0,72, - 787,1,0,0,0,74,795,1,0,0,0,76,797,1,0,0,0,78,821,1,0,0,0,80,823,1,0,0, - 0,82,833,1,0,0,0,84,836,1,0,0,0,86,858,1,0,0,0,88,883,1,0,0,0,90,885,1, - 0,0,0,92,896,1,0,0,0,94,909,1,0,0,0,96,911,1,0,0,0,98,920,1,0,0,0,100, - 947,1,0,0,0,102,964,1,0,0,0,104,981,1,0,0,0,106,983,1,0,0,0,108,986,1, - 0,0,0,110,996,1,0,0,0,112,1006,1,0,0,0,114,1128,1,0,0,0,116,1130,1,0,0, - 0,118,1145,1,0,0,0,120,1150,1,0,0,0,122,1189,1,0,0,0,124,1191,1,0,0,0, - 126,1201,1,0,0,0,128,1207,1,0,0,0,130,1218,1,0,0,0,132,1232,1,0,0,0,134, - 1234,1,0,0,0,136,1237,1,0,0,0,138,1258,1,0,0,0,140,1260,1,0,0,0,142,1280, - 1,0,0,0,144,1288,1,0,0,0,146,1290,1,0,0,0,148,1292,1,0,0,0,150,1294,1, - 0,0,0,152,1300,1,0,0,0,154,1306,1,0,0,0,156,1309,1,0,0,0,158,160,3,18, - 9,0,159,158,1,0,0,0,159,160,1,0,0,0,160,175,1,0,0,0,161,176,3,2,1,0,162, - 176,3,20,10,0,163,176,3,38,19,0,164,176,3,22,11,0,165,176,3,24,12,0,166, - 176,3,32,16,0,167,176,3,40,20,0,168,176,3,64,32,0,169,176,3,8,4,0,170, - 176,3,12,6,0,171,176,3,132,66,0,172,176,3,6,3,0,173,176,3,14,7,0,174,176, - 3,70,35,0,175,161,1,0,0,0,175,162,1,0,0,0,175,163,1,0,0,0,175,164,1,0, - 0,0,175,165,1,0,0,0,175,166,1,0,0,0,175,167,1,0,0,0,175,168,1,0,0,0,175, - 169,1,0,0,0,175,170,1,0,0,0,175,171,1,0,0,0,175,172,1,0,0,0,175,173,1, - 0,0,0,175,174,1,0,0,0,176,178,1,0,0,0,177,179,5,142,0,0,178,177,1,0,0, - 0,178,179,1,0,0,0,179,180,1,0,0,0,180,181,5,0,0,1,181,1,1,0,0,0,182,184, - 5,32,0,0,183,185,5,8,0,0,184,183,1,0,0,0,184,185,1,0,0,0,185,186,1,0,0, - 0,186,187,5,31,0,0,187,188,5,137,0,0,188,189,3,70,35,0,189,190,5,138,0, - 0,190,191,5,33,0,0,191,192,3,4,2,0,192,3,1,0,0,0,193,205,3,20,10,0,194, - 205,3,38,19,0,195,205,3,22,11,0,196,205,3,24,12,0,197,205,3,32,16,0,198, - 205,3,40,20,0,199,205,3,64,32,0,200,205,3,8,4,0,201,205,3,12,6,0,202,205, - 3,6,3,0,203,205,3,70,35,0,204,193,1,0,0,0,204,194,1,0,0,0,204,195,1,0, - 0,0,204,196,1,0,0,0,204,197,1,0,0,0,204,198,1,0,0,0,204,199,1,0,0,0,204, - 200,1,0,0,0,204,201,1,0,0,0,204,202,1,0,0,0,204,203,1,0,0,0,205,5,1,0, - 0,0,206,207,7,0,0,0,207,216,3,128,64,0,208,213,3,112,56,0,209,210,5,139, - 0,0,210,212,3,112,56,0,211,209,1,0,0,0,212,215,1,0,0,0,213,211,1,0,0,0, - 213,214,1,0,0,0,214,217,1,0,0,0,215,213,1,0,0,0,216,208,1,0,0,0,216,217, - 1,0,0,0,217,7,1,0,0,0,218,219,5,79,0,0,219,224,3,98,49,0,220,221,5,139, - 0,0,221,223,3,98,49,0,222,220,1,0,0,0,223,226,1,0,0,0,224,222,1,0,0,0, - 224,225,1,0,0,0,225,227,1,0,0,0,226,224,1,0,0,0,227,228,5,83,0,0,228,233, - 3,10,5,0,229,230,5,139,0,0,230,232,3,10,5,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,237,1,0,0,0,235,233,1,0,0,0, - 236,238,3,106,53,0,237,236,1,0,0,0,237,238,1,0,0,0,238,9,1,0,0,0,239,240, - 3,124,62,0,240,241,5,131,0,0,241,242,3,112,56,0,242,11,1,0,0,0,243,249, - 5,78,0,0,244,245,3,128,64,0,245,246,5,140,0,0,246,247,5,124,0,0,247,250, - 1,0,0,0,248,250,5,124,0,0,249,244,1,0,0,0,249,248,1,0,0,0,249,250,1,0, - 0,0,250,251,1,0,0,0,251,252,5,2,0,0,252,257,3,98,49,0,253,254,5,139,0, - 0,254,256,3,98,49,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,261,1,0,0,0,259,257,1,0,0,0,260,262,3,106,53,0,261,260, - 1,0,0,0,261,262,1,0,0,0,262,13,1,0,0,0,263,264,5,1,0,0,264,269,3,16,8, - 0,265,266,5,139,0,0,266,268,3,16,8,0,267,265,1,0,0,0,268,271,1,0,0,0,269, - 267,1,0,0,0,269,270,1,0,0,0,270,15,1,0,0,0,271,269,1,0,0,0,272,277,5,143, - 0,0,273,275,5,5,0,0,274,273,1,0,0,0,274,275,1,0,0,0,275,276,1,0,0,0,276, - 278,3,128,64,0,277,274,1,0,0,0,277,278,1,0,0,0,278,17,1,0,0,0,279,280, - 5,100,0,0,280,285,3,28,14,0,281,282,5,139,0,0,282,284,3,28,14,0,283,281, - 1,0,0,0,284,287,1,0,0,0,285,283,1,0,0,0,285,286,1,0,0,0,286,288,1,0,0, - 0,287,285,1,0,0,0,288,289,5,142,0,0,289,19,1,0,0,0,290,292,5,57,0,0,291, - 293,5,91,0,0,292,291,1,0,0,0,292,293,1,0,0,0,293,294,1,0,0,0,294,295,5, - 58,0,0,295,296,3,128,64,0,296,297,5,137,0,0,297,302,3,46,23,0,298,299, - 5,139,0,0,299,301,3,46,23,0,300,298,1,0,0,0,301,304,1,0,0,0,302,300,1, - 0,0,0,302,303,1,0,0,0,303,309,1,0,0,0,304,302,1,0,0,0,305,306,5,139,0, - 0,306,308,3,56,28,0,307,305,1,0,0,0,308,311,1,0,0,0,309,307,1,0,0,0,309, - 310,1,0,0,0,310,312,1,0,0,0,311,309,1,0,0,0,312,313,5,138,0,0,313,21,1, - 0,0,0,314,315,5,57,0,0,315,316,5,98,0,0,316,328,3,128,64,0,317,318,5,137, - 0,0,318,323,3,128,64,0,319,320,5,139,0,0,320,322,3,128,64,0,321,319,1, - 0,0,0,322,325,1,0,0,0,323,321,1,0,0,0,323,324,1,0,0,0,324,326,1,0,0,0, - 325,323,1,0,0,0,326,327,5,138,0,0,327,329,1,0,0,0,328,317,1,0,0,0,328, - 329,1,0,0,0,329,330,1,0,0,0,330,331,5,5,0,0,331,332,3,70,35,0,332,23,1, - 0,0,0,333,334,5,57,0,0,334,335,5,99,0,0,335,337,3,128,64,0,336,338,3,26, - 13,0,337,336,1,0,0,0,337,338,1,0,0,0,338,339,1,0,0,0,339,340,5,5,0,0,340, - 341,3,36,18,0,341,25,1,0,0,0,342,343,5,137,0,0,343,348,3,28,14,0,344,345, - 5,139,0,0,345,347,3,28,14,0,346,344,1,0,0,0,347,350,1,0,0,0,348,346,1, - 0,0,0,348,349,1,0,0,0,349,351,1,0,0,0,350,348,1,0,0,0,351,352,5,138,0, - 0,352,362,1,0,0,0,353,358,3,28,14,0,354,355,5,139,0,0,355,357,3,28,14, - 0,356,354,1,0,0,0,357,360,1,0,0,0,358,356,1,0,0,0,358,359,1,0,0,0,359, - 362,1,0,0,0,360,358,1,0,0,0,361,342,1,0,0,0,361,353,1,0,0,0,362,27,1,0, - 0,0,363,364,3,30,15,0,364,365,3,50,25,0,365,29,1,0,0,0,366,369,3,124,62, - 0,367,369,5,144,0,0,368,366,1,0,0,0,368,367,1,0,0,0,369,31,1,0,0,0,370, - 371,5,64,0,0,371,372,5,58,0,0,372,373,3,128,64,0,373,374,3,34,17,0,374, - 33,1,0,0,0,375,377,5,67,0,0,376,378,5,69,0,0,377,376,1,0,0,0,377,378,1, - 0,0,0,378,379,1,0,0,0,379,433,3,46,23,0,380,381,5,67,0,0,381,433,3,56, - 28,0,382,384,5,64,0,0,383,385,5,69,0,0,384,383,1,0,0,0,384,385,1,0,0,0, - 385,386,1,0,0,0,386,387,3,128,64,0,387,391,3,50,25,0,388,390,3,54,27,0, - 389,388,1,0,0,0,390,393,1,0,0,0,391,389,1,0,0,0,391,392,1,0,0,0,392,433, - 1,0,0,0,393,391,1,0,0,0,394,396,5,64,0,0,395,397,5,69,0,0,396,395,1,0, - 0,0,396,397,1,0,0,0,397,398,1,0,0,0,398,399,3,128,64,0,399,400,5,83,0, - 0,400,401,5,84,0,0,401,402,3,112,56,0,402,433,1,0,0,0,403,405,5,64,0,0, - 404,406,5,69,0,0,405,404,1,0,0,0,405,406,1,0,0,0,406,407,1,0,0,0,407,408, - 3,128,64,0,408,409,5,68,0,0,409,410,5,84,0,0,410,433,1,0,0,0,411,412,5, - 68,0,0,412,413,5,69,0,0,413,433,3,128,64,0,414,415,5,68,0,0,415,416,5, - 75,0,0,416,433,3,128,64,0,417,418,5,65,0,0,418,419,5,66,0,0,419,433,3, - 128,64,0,420,421,5,65,0,0,421,422,5,69,0,0,422,423,3,128,64,0,423,424, - 5,66,0,0,424,425,3,128,64,0,425,433,1,0,0,0,426,427,5,65,0,0,427,428,5, - 90,0,0,428,429,3,128,64,0,429,430,5,66,0,0,430,431,3,128,64,0,431,433, - 1,0,0,0,432,375,1,0,0,0,432,380,1,0,0,0,432,382,1,0,0,0,432,394,1,0,0, - 0,432,403,1,0,0,0,432,411,1,0,0,0,432,414,1,0,0,0,432,417,1,0,0,0,432, - 420,1,0,0,0,432,426,1,0,0,0,433,35,1,0,0,0,434,440,3,70,35,0,435,440,3, - 64,32,0,436,440,3,8,4,0,437,440,3,12,6,0,438,440,3,20,10,0,439,434,1,0, - 0,0,439,435,1,0,0,0,439,436,1,0,0,0,439,437,1,0,0,0,439,438,1,0,0,0,440, - 37,1,0,0,0,441,443,5,57,0,0,442,444,5,86,0,0,443,442,1,0,0,0,443,444,1, - 0,0,0,444,445,1,0,0,0,445,446,5,90,0,0,446,447,3,128,64,0,447,448,5,25, - 0,0,448,449,3,128,64,0,449,450,5,137,0,0,450,455,3,42,21,0,451,452,5,139, - 0,0,452,454,3,42,21,0,453,451,1,0,0,0,454,457,1,0,0,0,455,453,1,0,0,0, - 455,456,1,0,0,0,456,458,1,0,0,0,457,455,1,0,0,0,458,461,5,138,0,0,459, - 460,5,92,0,0,460,462,3,44,22,0,461,459,1,0,0,0,461,462,1,0,0,0,462,39, - 1,0,0,0,463,464,5,68,0,0,464,465,5,58,0,0,465,479,3,128,64,0,466,467,5, - 68,0,0,467,468,5,90,0,0,468,469,3,128,64,0,469,470,5,25,0,0,470,471,3, - 128,64,0,471,479,1,0,0,0,472,473,5,68,0,0,473,474,5,99,0,0,474,479,3,128, - 64,0,475,476,5,68,0,0,476,477,5,98,0,0,477,479,3,128,64,0,478,463,1,0, - 0,0,478,466,1,0,0,0,478,472,1,0,0,0,478,475,1,0,0,0,479,41,1,0,0,0,480, - 482,3,128,64,0,481,483,7,1,0,0,482,481,1,0,0,0,482,483,1,0,0,0,483,43, - 1,0,0,0,484,490,5,73,0,0,485,486,5,95,0,0,486,490,5,109,0,0,487,488,5, - 96,0,0,488,490,5,109,0,0,489,484,1,0,0,0,489,485,1,0,0,0,489,487,1,0,0, - 0,490,45,1,0,0,0,491,492,3,128,64,0,492,494,3,50,25,0,493,495,3,48,24, - 0,494,493,1,0,0,0,494,495,1,0,0,0,495,499,1,0,0,0,496,498,3,54,27,0,497, - 496,1,0,0,0,498,501,1,0,0,0,499,497,1,0,0,0,499,500,1,0,0,0,500,47,1,0, - 0,0,501,499,1,0,0,0,502,503,5,5,0,0,503,504,5,137,0,0,504,505,3,112,56, - 0,505,506,5,138,0,0,506,49,1,0,0,0,507,510,3,128,64,0,508,510,5,88,0,0, - 509,507,1,0,0,0,509,508,1,0,0,0,510,512,1,0,0,0,511,513,3,128,64,0,512, - 511,1,0,0,0,512,513,1,0,0,0,513,515,1,0,0,0,514,516,3,128,64,0,515,514, - 1,0,0,0,515,516,1,0,0,0,516,525,1,0,0,0,517,518,5,137,0,0,518,521,3,52, - 26,0,519,520,5,139,0,0,520,522,3,52,26,0,521,519,1,0,0,0,521,522,1,0,0, - 0,522,523,1,0,0,0,523,524,5,138,0,0,524,526,1,0,0,0,525,517,1,0,0,0,525, - 526,1,0,0,0,526,51,1,0,0,0,527,529,5,129,0,0,528,527,1,0,0,0,528,529,1, - 0,0,0,529,530,1,0,0,0,530,531,5,146,0,0,531,53,1,0,0,0,532,533,5,8,0,0, - 533,602,5,109,0,0,534,602,5,109,0,0,535,536,5,84,0,0,536,602,3,112,56, - 0,537,538,5,92,0,0,538,602,7,2,0,0,539,540,5,75,0,0,540,542,3,128,64,0, - 541,539,1,0,0,0,541,542,1,0,0,0,542,543,1,0,0,0,543,544,5,97,0,0,544,545, - 5,137,0,0,545,546,3,58,29,0,546,547,5,138,0,0,547,602,1,0,0,0,548,549, - 5,75,0,0,549,551,3,128,64,0,550,548,1,0,0,0,550,551,1,0,0,0,551,552,1, - 0,0,0,552,553,5,73,0,0,553,555,5,74,0,0,554,556,3,138,69,0,555,554,1,0, - 0,0,555,556,1,0,0,0,556,602,1,0,0,0,557,558,5,75,0,0,558,560,3,128,64, - 0,559,557,1,0,0,0,559,560,1,0,0,0,560,561,1,0,0,0,561,563,5,86,0,0,562, - 564,3,138,69,0,563,562,1,0,0,0,563,564,1,0,0,0,564,602,1,0,0,0,565,566, - 5,75,0,0,566,568,3,128,64,0,567,565,1,0,0,0,567,568,1,0,0,0,568,569,1, - 0,0,0,569,570,5,77,0,0,570,582,3,128,64,0,571,572,5,137,0,0,572,577,3, - 128,64,0,573,574,5,139,0,0,574,576,3,128,64,0,575,573,1,0,0,0,576,579, - 1,0,0,0,577,575,1,0,0,0,577,578,1,0,0,0,578,580,1,0,0,0,579,577,1,0,0, - 0,580,581,5,138,0,0,581,583,1,0,0,0,582,571,1,0,0,0,582,583,1,0,0,0,583, - 587,1,0,0,0,584,586,3,60,30,0,585,584,1,0,0,0,586,589,1,0,0,0,587,585, - 1,0,0,0,587,588,1,0,0,0,588,602,1,0,0,0,589,587,1,0,0,0,590,599,5,88,0, - 0,591,592,5,137,0,0,592,595,3,52,26,0,593,594,5,139,0,0,594,596,3,52,26, - 0,595,593,1,0,0,0,595,596,1,0,0,0,596,597,1,0,0,0,597,598,5,138,0,0,598, - 600,1,0,0,0,599,591,1,0,0,0,599,600,1,0,0,0,600,602,1,0,0,0,601,532,1, - 0,0,0,601,534,1,0,0,0,601,535,1,0,0,0,601,537,1,0,0,0,601,541,1,0,0,0, - 601,550,1,0,0,0,601,559,1,0,0,0,601,567,1,0,0,0,601,590,1,0,0,0,602,55, - 1,0,0,0,603,604,5,75,0,0,604,606,3,128,64,0,605,603,1,0,0,0,605,606,1, - 0,0,0,606,607,1,0,0,0,607,608,5,73,0,0,608,610,5,74,0,0,609,611,3,138, - 69,0,610,609,1,0,0,0,610,611,1,0,0,0,611,612,1,0,0,0,612,613,5,137,0,0, - 613,618,3,128,64,0,614,615,5,139,0,0,615,617,3,128,64,0,616,614,1,0,0, - 0,617,620,1,0,0,0,618,616,1,0,0,0,618,619,1,0,0,0,619,621,1,0,0,0,620, - 618,1,0,0,0,621,622,5,138,0,0,622,693,1,0,0,0,623,624,5,75,0,0,624,626, - 3,128,64,0,625,623,1,0,0,0,625,626,1,0,0,0,626,627,1,0,0,0,627,629,5,86, - 0,0,628,630,3,138,69,0,629,628,1,0,0,0,629,630,1,0,0,0,630,631,1,0,0,0, - 631,632,5,137,0,0,632,637,3,128,64,0,633,634,5,139,0,0,634,636,3,128,64, - 0,635,633,1,0,0,0,636,639,1,0,0,0,637,635,1,0,0,0,637,638,1,0,0,0,638, - 640,1,0,0,0,639,637,1,0,0,0,640,641,5,138,0,0,641,693,1,0,0,0,642,643, - 5,75,0,0,643,645,3,128,64,0,644,642,1,0,0,0,644,645,1,0,0,0,645,646,1, - 0,0,0,646,647,5,76,0,0,647,650,5,74,0,0,648,649,5,85,0,0,649,651,5,90, - 0,0,650,648,1,0,0,0,650,651,1,0,0,0,651,652,1,0,0,0,652,653,5,137,0,0, - 653,658,3,128,64,0,654,655,5,139,0,0,655,657,3,128,64,0,656,654,1,0,0, - 0,657,660,1,0,0,0,658,656,1,0,0,0,658,659,1,0,0,0,659,661,1,0,0,0,660, - 658,1,0,0,0,661,662,5,138,0,0,662,663,5,77,0,0,663,675,3,128,64,0,664, - 665,5,137,0,0,665,670,3,128,64,0,666,667,5,139,0,0,667,669,3,128,64,0, - 668,666,1,0,0,0,669,672,1,0,0,0,670,668,1,0,0,0,670,671,1,0,0,0,671,673, - 1,0,0,0,672,670,1,0,0,0,673,674,5,138,0,0,674,676,1,0,0,0,675,664,1,0, - 0,0,675,676,1,0,0,0,676,680,1,0,0,0,677,679,3,60,30,0,678,677,1,0,0,0, - 679,682,1,0,0,0,680,678,1,0,0,0,680,681,1,0,0,0,681,693,1,0,0,0,682,680, - 1,0,0,0,683,684,5,75,0,0,684,686,3,128,64,0,685,683,1,0,0,0,685,686,1, - 0,0,0,686,687,1,0,0,0,687,688,5,97,0,0,688,689,5,137,0,0,689,690,3,58, - 29,0,690,691,5,138,0,0,691,693,1,0,0,0,692,605,1,0,0,0,692,625,1,0,0,0, - 692,644,1,0,0,0,692,685,1,0,0,0,693,57,1,0,0,0,694,700,8,3,0,0,695,696, - 5,137,0,0,696,697,3,58,29,0,697,698,5,138,0,0,698,700,1,0,0,0,699,694, - 1,0,0,0,699,695,1,0,0,0,700,703,1,0,0,0,701,699,1,0,0,0,701,702,1,0,0, - 0,702,59,1,0,0,0,703,701,1,0,0,0,704,705,5,25,0,0,705,706,5,79,0,0,706, - 711,3,62,31,0,707,708,5,25,0,0,708,709,5,78,0,0,709,711,3,62,31,0,710, - 704,1,0,0,0,710,707,1,0,0,0,711,61,1,0,0,0,712,721,5,80,0,0,713,714,5, - 85,0,0,714,721,5,82,0,0,715,721,5,81,0,0,716,717,5,83,0,0,717,721,5,109, - 0,0,718,719,5,83,0,0,719,721,5,84,0,0,720,712,1,0,0,0,720,713,1,0,0,0, - 720,715,1,0,0,0,720,716,1,0,0,0,720,718,1,0,0,0,721,63,1,0,0,0,722,723, - 5,70,0,0,723,724,5,71,0,0,724,741,3,128,64,0,725,726,5,137,0,0,726,731, - 3,128,64,0,727,728,5,139,0,0,728,730,3,128,64,0,729,727,1,0,0,0,730,733, - 1,0,0,0,731,729,1,0,0,0,731,732,1,0,0,0,732,734,1,0,0,0,733,731,1,0,0, - 0,734,735,5,138,0,0,735,737,1,0,0,0,736,725,1,0,0,0,736,737,1,0,0,0,737, - 738,1,0,0,0,738,742,3,70,35,0,739,740,5,84,0,0,740,742,5,72,0,0,741,736, - 1,0,0,0,741,739,1,0,0,0,742,65,1,0,0,0,743,744,5,137,0,0,744,749,3,68, - 34,0,745,746,5,139,0,0,746,748,3,68,34,0,747,745,1,0,0,0,748,751,1,0,0, - 0,749,747,1,0,0,0,749,750,1,0,0,0,750,752,1,0,0,0,751,749,1,0,0,0,752, - 753,5,138,0,0,753,67,1,0,0,0,754,757,5,84,0,0,755,757,3,112,56,0,756,754, - 1,0,0,0,756,755,1,0,0,0,757,69,1,0,0,0,758,764,3,72,36,0,759,760,3,74, - 37,0,760,761,3,72,36,0,761,763,1,0,0,0,762,759,1,0,0,0,763,766,1,0,0,0, - 764,762,1,0,0,0,764,765,1,0,0,0,765,768,1,0,0,0,766,764,1,0,0,0,767,769, - 3,108,54,0,768,767,1,0,0,0,768,769,1,0,0,0,769,771,1,0,0,0,770,772,3,88, - 44,0,771,770,1,0,0,0,771,772,1,0,0,0,772,71,1,0,0,0,773,788,3,76,38,0, - 774,775,5,137,0,0,775,776,3,70,35,0,776,777,5,138,0,0,777,788,1,0,0,0, - 778,779,5,72,0,0,779,784,3,66,33,0,780,781,5,139,0,0,781,783,3,66,33,0, - 782,780,1,0,0,0,783,786,1,0,0,0,784,782,1,0,0,0,784,785,1,0,0,0,785,788, - 1,0,0,0,786,784,1,0,0,0,787,773,1,0,0,0,787,774,1,0,0,0,787,778,1,0,0, - 0,788,73,1,0,0,0,789,791,5,53,0,0,790,792,5,54,0,0,791,790,1,0,0,0,791, - 792,1,0,0,0,792,796,1,0,0,0,793,796,5,55,0,0,794,796,5,56,0,0,795,789, - 1,0,0,0,795,793,1,0,0,0,795,794,1,0,0,0,796,75,1,0,0,0,797,799,5,1,0,0, - 798,800,3,78,39,0,799,798,1,0,0,0,799,800,1,0,0,0,800,802,1,0,0,0,801, - 803,3,84,42,0,802,801,1,0,0,0,802,803,1,0,0,0,803,804,1,0,0,0,804,807, - 3,92,46,0,805,806,5,71,0,0,806,808,3,128,64,0,807,805,1,0,0,0,807,808, - 1,0,0,0,808,810,1,0,0,0,809,811,3,96,48,0,810,809,1,0,0,0,810,811,1,0, - 0,0,811,813,1,0,0,0,812,814,3,106,53,0,813,812,1,0,0,0,813,814,1,0,0,0, - 814,816,1,0,0,0,815,817,3,80,40,0,816,815,1,0,0,0,816,817,1,0,0,0,817, - 819,1,0,0,0,818,820,3,82,41,0,819,818,1,0,0,0,819,820,1,0,0,0,820,77,1, - 0,0,0,821,822,7,4,0,0,822,79,1,0,0,0,823,824,5,27,0,0,824,825,5,29,0,0, - 825,830,3,112,56,0,826,827,5,139,0,0,827,829,3,112,56,0,828,826,1,0,0, - 0,829,832,1,0,0,0,830,828,1,0,0,0,830,831,1,0,0,0,831,81,1,0,0,0,832,830, - 1,0,0,0,833,834,5,30,0,0,834,835,3,112,56,0,835,83,1,0,0,0,836,837,5,4, - 0,0,837,842,3,86,43,0,838,839,7,5,0,0,839,841,3,86,43,0,840,838,1,0,0, - 0,841,844,1,0,0,0,842,840,1,0,0,0,842,843,1,0,0,0,843,846,1,0,0,0,844, - 842,1,0,0,0,845,847,5,36,0,0,846,845,1,0,0,0,846,847,1,0,0,0,847,850,1, - 0,0,0,848,849,5,92,0,0,849,851,5,117,0,0,850,848,1,0,0,0,850,851,1,0,0, - 0,851,85,1,0,0,0,852,859,5,146,0,0,853,859,5,144,0,0,854,855,5,137,0,0, - 855,856,3,112,56,0,856,857,5,138,0,0,857,859,1,0,0,0,858,852,1,0,0,0,858, - 853,1,0,0,0,858,854,1,0,0,0,859,87,1,0,0,0,860,861,5,45,0,0,861,862,3, - 112,56,0,862,872,3,90,45,0,863,864,5,46,0,0,864,865,7,6,0,0,865,866,3, - 112,56,0,866,870,3,90,45,0,867,871,5,51,0,0,868,869,5,92,0,0,869,871,5, - 117,0,0,870,867,1,0,0,0,870,868,1,0,0,0,871,873,1,0,0,0,872,863,1,0,0, - 0,872,873,1,0,0,0,873,884,1,0,0,0,874,875,5,46,0,0,875,876,7,6,0,0,876, - 877,3,112,56,0,877,881,3,90,45,0,878,882,5,51,0,0,879,880,5,92,0,0,880, - 882,5,117,0,0,881,878,1,0,0,0,881,879,1,0,0,0,882,884,1,0,0,0,883,860, - 1,0,0,0,883,874,1,0,0,0,884,89,1,0,0,0,885,886,7,7,0,0,886,91,1,0,0,0, - 887,897,5,124,0,0,888,893,3,94,47,0,889,890,5,139,0,0,890,892,3,94,47, - 0,891,889,1,0,0,0,892,895,1,0,0,0,893,891,1,0,0,0,893,894,1,0,0,0,894, - 897,1,0,0,0,895,893,1,0,0,0,896,887,1,0,0,0,896,888,1,0,0,0,897,93,1,0, - 0,0,898,899,3,128,64,0,899,900,5,140,0,0,900,901,5,124,0,0,901,910,1,0, - 0,0,902,907,3,112,56,0,903,905,5,5,0,0,904,903,1,0,0,0,904,905,1,0,0,0, - 905,906,1,0,0,0,906,908,3,128,64,0,907,904,1,0,0,0,907,908,1,0,0,0,908, - 910,1,0,0,0,909,898,1,0,0,0,909,902,1,0,0,0,910,95,1,0,0,0,911,912,5,2, - 0,0,912,917,3,98,49,0,913,914,5,139,0,0,914,916,3,98,49,0,915,913,1,0, - 0,0,916,919,1,0,0,0,917,915,1,0,0,0,917,918,1,0,0,0,918,97,1,0,0,0,919, - 917,1,0,0,0,920,924,3,100,50,0,921,923,3,102,51,0,922,921,1,0,0,0,923, - 926,1,0,0,0,924,922,1,0,0,0,924,925,1,0,0,0,925,99,1,0,0,0,926,924,1,0, - 0,0,927,932,3,128,64,0,928,930,5,5,0,0,929,928,1,0,0,0,929,930,1,0,0,0, - 930,931,1,0,0,0,931,933,3,128,64,0,932,929,1,0,0,0,932,933,1,0,0,0,933, - 948,1,0,0,0,934,935,5,137,0,0,935,936,3,70,35,0,936,941,5,138,0,0,937, - 939,5,5,0,0,938,937,1,0,0,0,938,939,1,0,0,0,939,940,1,0,0,0,940,942,3, - 128,64,0,941,938,1,0,0,0,941,942,1,0,0,0,942,948,1,0,0,0,943,944,5,137, - 0,0,944,945,3,98,49,0,945,946,5,138,0,0,946,948,1,0,0,0,947,927,1,0,0, - 0,947,934,1,0,0,0,947,943,1,0,0,0,948,101,1,0,0,0,949,950,3,104,52,0,950, - 951,5,23,0,0,951,952,3,100,50,0,952,953,5,25,0,0,953,954,3,112,56,0,954, - 965,1,0,0,0,955,956,5,37,0,0,956,957,5,23,0,0,957,965,3,100,50,0,958,959, - 5,37,0,0,959,960,5,38,0,0,960,965,3,100,50,0,961,962,5,22,0,0,962,963, - 5,38,0,0,963,965,3,100,50,0,964,949,1,0,0,0,964,955,1,0,0,0,964,958,1, - 0,0,0,964,961,1,0,0,0,965,103,1,0,0,0,966,968,5,18,0,0,967,966,1,0,0,0, - 967,968,1,0,0,0,968,982,1,0,0,0,969,971,5,19,0,0,970,972,5,22,0,0,971, - 970,1,0,0,0,971,972,1,0,0,0,972,982,1,0,0,0,973,975,5,20,0,0,974,976,5, - 22,0,0,975,974,1,0,0,0,975,976,1,0,0,0,976,982,1,0,0,0,977,979,5,21,0, - 0,978,980,5,22,0,0,979,978,1,0,0,0,979,980,1,0,0,0,980,982,1,0,0,0,981, - 967,1,0,0,0,981,969,1,0,0,0,981,973,1,0,0,0,981,977,1,0,0,0,982,105,1, - 0,0,0,983,984,5,3,0,0,984,985,3,112,56,0,985,107,1,0,0,0,986,987,5,26, - 0,0,987,988,5,29,0,0,988,993,3,110,55,0,989,990,5,139,0,0,990,992,3,110, - 55,0,991,989,1,0,0,0,992,995,1,0,0,0,993,991,1,0,0,0,993,994,1,0,0,0,994, - 109,1,0,0,0,995,993,1,0,0,0,996,998,3,112,56,0,997,999,7,1,0,0,998,997, - 1,0,0,0,998,999,1,0,0,0,999,111,1,0,0,0,1000,1001,6,56,-1,0,1001,1002, - 7,5,0,0,1002,1007,3,112,56,21,1003,1004,7,8,0,0,1004,1007,3,112,56,7,1005, - 1007,3,114,57,0,1006,1000,1,0,0,0,1006,1003,1,0,0,0,1006,1005,1,0,0,0, - 1007,1106,1,0,0,0,1008,1009,10,22,0,0,1009,1010,5,127,0,0,1010,1105,3, - 112,56,23,1011,1012,10,20,0,0,1012,1013,7,9,0,0,1013,1105,3,112,56,21, - 1014,1015,10,19,0,0,1015,1016,5,126,0,0,1016,1105,3,112,56,20,1017,1018, - 10,18,0,0,1018,1019,5,17,0,0,1019,1105,3,112,56,19,1020,1021,10,17,0,0, - 1021,1022,7,5,0,0,1022,1105,3,112,56,18,1023,1024,10,16,0,0,1024,1025, - 5,130,0,0,1025,1105,3,112,56,17,1026,1027,10,15,0,0,1027,1028,7,10,0,0, - 1028,1105,3,112,56,16,1029,1031,10,14,0,0,1030,1032,5,8,0,0,1031,1030, - 1,0,0,0,1031,1032,1,0,0,0,1032,1033,1,0,0,0,1033,1034,5,52,0,0,1034,1035, - 3,112,56,0,1035,1036,5,6,0,0,1036,1037,3,112,56,15,1037,1105,1,0,0,0,1038, - 1040,10,13,0,0,1039,1041,5,8,0,0,1040,1039,1,0,0,0,1040,1041,1,0,0,0,1041, - 1042,1,0,0,0,1042,1043,5,16,0,0,1043,1105,3,112,56,14,1044,1045,10,8,0, - 0,1045,1047,5,28,0,0,1046,1048,5,8,0,0,1047,1046,1,0,0,0,1047,1048,1,0, - 0,0,1048,1049,1,0,0,0,1049,1050,5,35,0,0,1050,1051,5,2,0,0,1051,1105,3, - 112,56,9,1052,1053,10,6,0,0,1053,1054,7,11,0,0,1054,1105,3,112,56,7,1055, - 1056,10,5,0,0,1056,1057,7,12,0,0,1057,1105,3,112,56,6,1058,1059,10,4,0, - 0,1059,1060,7,13,0,0,1060,1105,3,112,56,5,1061,1062,10,3,0,0,1062,1063, - 5,10,0,0,1063,1105,3,112,56,4,1064,1065,10,2,0,0,1065,1066,5,11,0,0,1066, - 1105,3,112,56,3,1067,1069,10,12,0,0,1068,1070,5,8,0,0,1069,1068,1,0,0, - 0,1069,1070,1,0,0,0,1070,1071,1,0,0,0,1071,1072,5,24,0,0,1072,1073,5,137, - 0,0,1073,1074,3,70,35,0,1074,1075,5,138,0,0,1075,1105,1,0,0,0,1076,1078, - 10,11,0,0,1077,1079,5,8,0,0,1078,1077,1,0,0,0,1078,1079,1,0,0,0,1079,1080, - 1,0,0,0,1080,1081,5,24,0,0,1081,1082,5,137,0,0,1082,1087,3,112,56,0,1083, - 1084,5,139,0,0,1084,1086,3,112,56,0,1085,1083,1,0,0,0,1086,1089,1,0,0, - 0,1087,1085,1,0,0,0,1087,1088,1,0,0,0,1088,1090,1,0,0,0,1089,1087,1,0, - 0,0,1090,1091,5,138,0,0,1091,1105,1,0,0,0,1092,1093,10,10,0,0,1093,1095, - 5,28,0,0,1094,1096,5,8,0,0,1095,1094,1,0,0,0,1095,1096,1,0,0,0,1096,1097, - 1,0,0,0,1097,1105,5,109,0,0,1098,1099,10,9,0,0,1099,1101,5,28,0,0,1100, - 1102,5,8,0,0,1101,1100,1,0,0,0,1101,1102,1,0,0,0,1102,1103,1,0,0,0,1103, - 1105,7,14,0,0,1104,1008,1,0,0,0,1104,1011,1,0,0,0,1104,1014,1,0,0,0,1104, - 1017,1,0,0,0,1104,1020,1,0,0,0,1104,1023,1,0,0,0,1104,1026,1,0,0,0,1104, - 1029,1,0,0,0,1104,1038,1,0,0,0,1104,1044,1,0,0,0,1104,1052,1,0,0,0,1104, - 1055,1,0,0,0,1104,1058,1,0,0,0,1104,1061,1,0,0,0,1104,1064,1,0,0,0,1104, - 1067,1,0,0,0,1104,1076,1,0,0,0,1104,1092,1,0,0,0,1104,1098,1,0,0,0,1105, - 1108,1,0,0,0,1106,1104,1,0,0,0,1106,1107,1,0,0,0,1107,113,1,0,0,0,1108, - 1106,1,0,0,0,1109,1129,3,130,65,0,1110,1129,3,116,58,0,1111,1129,3,120, - 60,0,1112,1129,3,124,62,0,1113,1129,5,144,0,0,1114,1129,5,143,0,0,1115, - 1116,5,31,0,0,1116,1117,5,137,0,0,1117,1118,3,70,35,0,1118,1119,5,138, - 0,0,1119,1129,1,0,0,0,1120,1121,5,137,0,0,1121,1122,3,70,35,0,1122,1123, - 5,138,0,0,1123,1129,1,0,0,0,1124,1125,5,137,0,0,1125,1126,3,112,56,0,1126, - 1127,5,138,0,0,1127,1129,1,0,0,0,1128,1109,1,0,0,0,1128,1110,1,0,0,0,1128, - 1111,1,0,0,0,1128,1112,1,0,0,0,1128,1113,1,0,0,0,1128,1114,1,0,0,0,1128, - 1115,1,0,0,0,1128,1120,1,0,0,0,1128,1124,1,0,0,0,1129,115,1,0,0,0,1130, - 1132,5,41,0,0,1131,1133,3,112,56,0,1132,1131,1,0,0,0,1132,1133,1,0,0,0, - 1133,1135,1,0,0,0,1134,1136,3,118,59,0,1135,1134,1,0,0,0,1136,1137,1,0, - 0,0,1137,1135,1,0,0,0,1137,1138,1,0,0,0,1138,1141,1,0,0,0,1139,1140,5, - 43,0,0,1140,1142,3,112,56,0,1141,1139,1,0,0,0,1141,1142,1,0,0,0,1142,1143, - 1,0,0,0,1143,1144,5,44,0,0,1144,117,1,0,0,0,1145,1146,5,42,0,0,1146,1147, - 3,112,56,0,1147,1148,5,33,0,0,1148,1149,3,112,56,0,1149,119,1,0,0,0,1150, - 1151,3,122,61,0,1151,1164,5,137,0,0,1152,1165,5,124,0,0,1153,1155,5,35, - 0,0,1154,1153,1,0,0,0,1154,1155,1,0,0,0,1155,1156,1,0,0,0,1156,1161,3, - 112,56,0,1157,1158,5,139,0,0,1158,1160,3,112,56,0,1159,1157,1,0,0,0,1160, - 1163,1,0,0,0,1161,1159,1,0,0,0,1161,1162,1,0,0,0,1162,1165,1,0,0,0,1163, - 1161,1,0,0,0,1164,1152,1,0,0,0,1164,1154,1,0,0,0,1164,1165,1,0,0,0,1165, - 1166,1,0,0,0,1166,1168,5,138,0,0,1167,1169,3,152,76,0,1168,1167,1,0,0, - 0,1168,1169,1,0,0,0,1169,1171,1,0,0,0,1170,1172,3,150,75,0,1171,1170,1, - 0,0,0,1171,1172,1,0,0,0,1172,1181,1,0,0,0,1173,1175,3,154,77,0,1174,1173, - 1,0,0,0,1174,1175,1,0,0,0,1175,1177,1,0,0,0,1176,1178,3,156,78,0,1177, - 1176,1,0,0,0,1177,1178,1,0,0,0,1178,1179,1,0,0,0,1179,1180,5,39,0,0,1180, - 1182,3,136,68,0,1181,1174,1,0,0,0,1181,1182,1,0,0,0,1182,121,1,0,0,0,1183, - 1190,3,128,64,0,1184,1190,5,19,0,0,1185,1190,5,20,0,0,1186,1190,5,103, - 0,0,1187,1190,5,48,0,0,1188,1190,5,40,0,0,1189,1183,1,0,0,0,1189,1184, - 1,0,0,0,1189,1185,1,0,0,0,1189,1186,1,0,0,0,1189,1187,1,0,0,0,1189,1188, - 1,0,0,0,1190,123,1,0,0,0,1191,1196,3,128,64,0,1192,1193,7,15,0,0,1193, - 1195,3,126,63,0,1194,1192,1,0,0,0,1195,1198,1,0,0,0,1196,1194,1,0,0,0, - 1196,1197,1,0,0,0,1197,125,1,0,0,0,1198,1196,1,0,0,0,1199,1202,3,128,64, - 0,1200,1202,3,148,74,0,1201,1199,1,0,0,0,1201,1200,1,0,0,0,1202,127,1, - 0,0,0,1203,1208,5,153,0,0,1204,1208,5,151,0,0,1205,1208,5,152,0,0,1206, - 1208,3,146,73,0,1207,1203,1,0,0,0,1207,1204,1,0,0,0,1207,1205,1,0,0,0, - 1207,1206,1,0,0,0,1208,129,1,0,0,0,1209,1219,5,146,0,0,1210,1219,5,147, - 0,0,1211,1219,5,145,0,0,1212,1219,5,148,0,0,1213,1219,5,149,0,0,1214,1219, - 5,150,0,0,1215,1219,7,16,0,0,1216,1219,7,17,0,0,1217,1219,5,109,0,0,1218, - 1209,1,0,0,0,1218,1210,1,0,0,0,1218,1211,1,0,0,0,1218,1212,1,0,0,0,1218, - 1213,1,0,0,0,1218,1214,1,0,0,0,1218,1215,1,0,0,0,1218,1216,1,0,0,0,1218, - 1217,1,0,0,0,1219,131,1,0,0,0,1220,1222,5,59,0,0,1221,1223,7,18,0,0,1222, - 1221,1,0,0,0,1222,1223,1,0,0,0,1223,1233,1,0,0,0,1224,1226,5,60,0,0,1225, - 1227,7,18,0,0,1226,1225,1,0,0,0,1226,1227,1,0,0,0,1227,1233,1,0,0,0,1228, - 1230,5,61,0,0,1229,1231,7,18,0,0,1230,1229,1,0,0,0,1230,1231,1,0,0,0,1231, - 1233,1,0,0,0,1232,1220,1,0,0,0,1232,1224,1,0,0,0,1232,1228,1,0,0,0,1233, - 133,1,0,0,0,1234,1235,3,112,56,0,1235,1236,5,0,0,1,1236,135,1,0,0,0,1237, - 1248,5,137,0,0,1238,1239,5,40,0,0,1239,1240,5,29,0,0,1240,1245,3,112,56, - 0,1241,1242,5,139,0,0,1242,1244,3,112,56,0,1243,1241,1,0,0,0,1244,1247, - 1,0,0,0,1245,1243,1,0,0,0,1245,1246,1,0,0,0,1246,1249,1,0,0,0,1247,1245, - 1,0,0,0,1248,1238,1,0,0,0,1248,1249,1,0,0,0,1249,1251,1,0,0,0,1250,1252, - 3,108,54,0,1251,1250,1,0,0,0,1251,1252,1,0,0,0,1252,1254,1,0,0,0,1253, - 1255,3,140,70,0,1254,1253,1,0,0,0,1254,1255,1,0,0,0,1255,1256,1,0,0,0, - 1256,1257,5,138,0,0,1257,137,1,0,0,0,1258,1259,7,19,0,0,1259,139,1,0,0, - 0,1260,1267,7,20,0,0,1261,1262,5,52,0,0,1262,1263,3,142,71,0,1263,1264, - 5,6,0,0,1264,1265,3,142,71,0,1265,1268,1,0,0,0,1266,1268,3,142,71,0,1267, - 1261,1,0,0,0,1267,1266,1,0,0,0,1268,1271,1,0,0,0,1269,1270,5,116,0,0,1270, - 1272,3,144,72,0,1271,1269,1,0,0,0,1271,1272,1,0,0,0,1272,141,1,0,0,0,1273, - 1274,5,112,0,0,1274,1281,7,21,0,0,1275,1276,5,115,0,0,1276,1281,5,50,0, - 0,1277,1278,3,112,56,0,1278,1279,7,21,0,0,1279,1281,1,0,0,0,1280,1273, - 1,0,0,0,1280,1275,1,0,0,0,1280,1277,1,0,0,0,1281,143,1,0,0,0,1282,1283, - 5,115,0,0,1283,1289,5,50,0,0,1284,1289,5,27,0,0,1285,1289,5,117,0,0,1286, - 1287,5,85,0,0,1287,1289,5,118,0,0,1288,1282,1,0,0,0,1288,1284,1,0,0,0, - 1288,1285,1,0,0,0,1288,1286,1,0,0,0,1289,145,1,0,0,0,1290,1291,7,22,0, - 0,1291,147,1,0,0,0,1292,1293,7,23,0,0,1293,149,1,0,0,0,1294,1295,5,123, - 0,0,1295,1296,5,137,0,0,1296,1297,5,3,0,0,1297,1298,3,112,56,0,1298,1299, - 5,138,0,0,1299,151,1,0,0,0,1300,1301,5,119,0,0,1301,1302,5,27,0,0,1302, - 1303,5,137,0,0,1303,1304,3,108,54,0,1304,1305,5,138,0,0,1305,153,1,0,0, - 0,1306,1307,5,2,0,0,1307,1308,7,24,0,0,1308,155,1,0,0,0,1309,1310,7,25, - 0,0,1310,1311,5,122,0,0,1311,157,1,0,0,0,166,159,175,178,184,204,213,216, - 224,233,237,249,257,261,269,274,277,285,292,302,309,323,328,337,348,358, - 361,368,377,384,391,396,405,432,439,443,455,461,478,482,489,494,499,509, - 512,515,521,525,528,541,550,555,559,563,567,577,582,587,595,599,601,605, - 610,618,625,629,637,644,650,658,670,675,680,685,692,699,701,710,720,731, - 736,741,749,756,764,768,771,784,787,791,795,799,802,807,810,813,816,819, - 830,842,846,850,858,870,872,881,883,893,896,904,907,909,917,924,929,932, - 938,941,947,964,967,971,975,979,981,993,998,1006,1031,1040,1047,1069,1078, - 1087,1095,1101,1104,1106,1128,1132,1137,1141,1154,1161,1164,1168,1171, - 1174,1177,1181,1189,1196,1201,1207,1218,1222,1226,1230,1232,1245,1248, - 1251,1254,1267,1271,1280,1288 + 2,78,7,78,2,79,7,79,1,0,3,0,162,8,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1, + 0,1,0,1,0,1,0,1,0,1,0,3,0,178,8,0,1,0,3,0,181,8,0,1,0,1,0,1,1,1,1,3,1, + 187,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1, + 2,1,2,1,2,3,2,207,8,2,1,3,1,3,1,3,1,3,1,3,5,3,214,8,3,10,3,12,3,217,9, + 3,3,3,219,8,3,1,4,1,4,1,4,1,4,5,4,225,8,4,10,4,12,4,228,9,4,1,4,1,4,1, + 4,1,4,5,4,234,8,4,10,4,12,4,237,9,4,1,4,3,4,240,8,4,1,5,1,5,1,5,1,5,1, + 6,1,6,1,6,1,6,1,6,1,6,3,6,252,8,6,1,6,1,6,1,6,1,6,5,6,258,8,6,10,6,12, + 6,261,9,6,1,6,3,6,264,8,6,1,7,1,7,1,7,1,7,5,7,270,8,7,10,7,12,7,273,9, + 7,1,8,1,8,3,8,277,8,8,1,8,3,8,280,8,8,1,9,1,9,1,9,1,9,5,9,286,8,9,10,9, + 12,9,289,9,9,1,9,1,9,1,10,1,10,3,10,295,8,10,1,10,1,10,1,10,1,10,1,10, + 1,10,5,10,303,8,10,10,10,12,10,306,9,10,1,10,1,10,5,10,310,8,10,10,10, + 12,10,313,9,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,11,1,11,5,11,324,8, + 11,10,11,12,11,327,9,11,1,11,1,11,3,11,331,8,11,1,11,1,11,1,11,1,12,1, + 12,1,12,1,12,3,12,340,8,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,5,13,349, + 8,13,10,13,12,13,352,9,13,1,13,1,13,1,13,1,13,1,13,5,13,359,8,13,10,13, + 12,13,362,9,13,3,13,364,8,13,1,14,1,14,1,14,1,15,1,15,3,15,371,8,15,1, + 16,1,16,1,16,1,16,1,16,1,17,1,17,3,17,380,8,17,1,17,1,17,1,17,1,17,1,17, + 3,17,387,8,17,1,17,1,17,1,17,5,17,392,8,17,10,17,12,17,395,9,17,1,17,1, + 17,3,17,399,8,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,408,8,17,1,17, + 1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17, + 1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,435,8,17,1,18,1, + 18,1,18,1,18,1,18,3,18,442,8,18,1,19,1,19,3,19,446,8,19,1,19,1,19,1,19, + 1,19,1,19,1,19,1,19,1,19,5,19,456,8,19,10,19,12,19,459,9,19,1,19,1,19, + 1,19,3,19,464,8,19,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1, + 20,1,20,1,20,1,20,1,20,3,20,481,8,20,1,21,1,21,3,21,485,8,21,1,22,1,22, + 1,22,1,22,1,22,3,22,492,8,22,1,23,1,23,1,23,3,23,497,8,23,1,23,5,23,500, + 8,23,10,23,12,23,503,9,23,1,24,1,24,1,24,1,24,1,24,1,25,1,25,3,25,512, + 8,25,1,25,3,25,515,8,25,1,25,3,25,518,8,25,1,25,1,25,1,25,1,25,3,25,524, + 8,25,1,25,1,25,3,25,528,8,25,1,26,3,26,531,8,26,1,26,1,26,1,27,1,27,1, + 27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,544,8,27,1,27,1,27,1,27,1,27,1,27, + 1,27,1,27,3,27,553,8,27,1,27,1,27,1,27,3,27,558,8,27,1,27,1,27,3,27,562, + 8,27,1,27,1,27,3,27,566,8,27,1,27,1,27,3,27,570,8,27,1,27,1,27,1,27,1, + 27,1,27,1,27,5,27,578,8,27,10,27,12,27,581,9,27,1,27,1,27,3,27,585,8,27, + 1,27,5,27,588,8,27,10,27,12,27,591,9,27,1,27,1,27,1,27,1,27,1,27,3,27, + 598,8,27,1,27,1,27,3,27,602,8,27,3,27,604,8,27,1,28,1,28,3,28,608,8,28, + 1,28,1,28,1,28,3,28,613,8,28,1,28,1,28,1,28,1,28,5,28,619,8,28,10,28,12, + 28,622,9,28,1,28,1,28,1,28,1,28,3,28,628,8,28,1,28,1,28,3,28,632,8,28, + 1,28,1,28,1,28,1,28,5,28,638,8,28,10,28,12,28,641,9,28,1,28,1,28,1,28, + 1,28,3,28,647,8,28,1,28,1,28,1,28,1,28,3,28,653,8,28,1,28,1,28,1,28,1, + 28,5,28,659,8,28,10,28,12,28,662,9,28,1,28,1,28,1,28,1,28,1,28,1,28,1, + 28,5,28,671,8,28,10,28,12,28,674,9,28,1,28,1,28,3,28,678,8,28,1,28,5,28, + 681,8,28,10,28,12,28,684,9,28,1,28,1,28,3,28,688,8,28,1,28,1,28,1,28,1, + 28,1,28,3,28,695,8,28,1,29,1,29,1,29,1,29,1,29,5,29,702,8,29,10,29,12, + 29,705,9,29,1,30,1,30,1,30,1,30,1,30,1,30,3,30,713,8,30,1,31,1,31,1,31, + 1,31,1,31,1,31,1,31,1,31,3,31,723,8,31,1,32,1,32,1,32,1,32,1,32,1,32,1, + 32,5,32,732,8,32,10,32,12,32,735,9,32,1,32,1,32,3,32,739,8,32,1,32,1,32, + 1,32,3,32,744,8,32,1,33,1,33,1,33,1,33,5,33,750,8,33,10,33,12,33,753,9, + 33,1,33,1,33,1,34,1,34,3,34,759,8,34,1,35,1,35,1,35,1,35,5,35,765,8,35, + 10,35,12,35,768,9,35,1,35,3,35,771,8,35,1,35,3,35,774,8,35,1,36,1,36,1, + 36,1,36,1,36,1,36,1,36,1,36,1,36,5,36,785,8,36,10,36,12,36,788,9,36,3, + 36,790,8,36,1,37,1,37,3,37,794,8,37,1,37,1,37,3,37,798,8,37,1,38,1,38, + 3,38,802,8,38,1,38,3,38,805,8,38,1,38,1,38,1,38,3,38,810,8,38,1,38,3,38, + 813,8,38,1,38,3,38,816,8,38,1,38,3,38,819,8,38,1,38,3,38,822,8,38,1,39, + 1,39,1,40,1,40,1,40,1,40,1,40,5,40,831,8,40,10,40,12,40,834,9,40,1,41, + 1,41,1,41,1,42,1,42,1,42,1,42,5,42,843,8,42,10,42,12,42,846,9,42,1,42, + 3,42,849,8,42,1,42,1,42,3,42,853,8,42,1,43,1,43,1,43,1,43,1,43,1,43,3, + 43,861,8,43,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,873, + 8,44,3,44,875,8,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,884,8,44,3, + 44,886,8,44,1,45,1,45,1,46,1,46,1,46,1,46,5,46,894,8,46,10,46,12,46,897, + 9,46,3,46,899,8,46,1,47,1,47,1,47,1,47,1,47,1,47,3,47,907,8,47,1,47,3, + 47,910,8,47,3,47,912,8,47,1,48,1,48,1,48,1,48,5,48,918,8,48,10,48,12,48, + 921,9,48,1,49,1,49,5,49,925,8,49,10,49,12,49,928,9,49,1,50,1,50,3,50,932, + 8,50,1,50,3,50,935,8,50,1,50,1,50,1,50,1,50,3,50,941,8,50,1,50,1,50,3, + 50,945,8,50,3,50,947,8,50,1,50,1,50,1,50,1,50,3,50,953,8,50,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,1,51,3,51, + 970,8,51,1,52,3,52,973,8,52,1,52,1,52,3,52,977,8,52,1,52,1,52,3,52,981, + 8,52,1,52,1,52,3,52,985,8,52,3,52,987,8,52,1,53,1,53,1,53,1,54,1,54,1, + 54,1,54,1,54,5,54,997,8,54,10,54,12,54,1000,9,54,1,55,1,55,3,55,1004,8, + 55,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1012,8,56,1,56,1,56,1,56,1,56,1, + 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, + 56,1,56,1,56,1,56,1,56,3,56,1037,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1, + 56,3,56,1046,8,56,1,56,1,56,1,56,1,56,1,56,3,56,1053,8,56,1,56,1,56,1, + 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, + 56,1,56,1,56,1,56,3,56,1075,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3, + 56,1084,8,56,1,56,1,56,1,56,1,56,1,56,5,56,1091,8,56,10,56,12,56,1094, + 9,56,1,56,1,56,1,56,1,56,1,56,3,56,1101,8,56,1,56,1,56,1,56,1,56,3,56, + 1107,8,56,1,56,5,56,1110,8,56,10,56,12,56,1113,9,56,1,57,1,57,1,57,1,57, + 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, + 1,57,3,57,1134,8,57,1,58,1,58,3,58,1138,8,58,1,58,4,58,1141,8,58,11,58, + 12,58,1142,1,58,1,58,3,58,1147,8,58,1,58,1,58,1,59,1,59,1,59,1,59,1,59, + 1,60,1,60,1,60,1,60,3,60,1160,8,60,1,60,1,60,1,60,5,60,1165,8,60,10,60, + 12,60,1168,9,60,3,60,1170,8,60,1,60,1,60,3,60,1174,8,60,1,60,3,60,1177, + 8,60,1,60,3,60,1180,8,60,1,60,3,60,1183,8,60,1,60,1,60,3,60,1187,8,60, + 1,61,1,61,1,61,1,61,1,61,1,61,3,61,1195,8,61,1,62,1,62,1,62,5,62,1200, + 8,62,10,62,12,62,1203,9,62,1,63,1,63,3,63,1207,8,63,1,64,1,64,1,64,1,64, + 3,64,1213,8,64,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,3,65,1224, + 8,65,1,66,1,66,3,66,1228,8,66,1,66,1,66,3,66,1232,8,66,1,66,1,66,3,66, + 1236,8,66,3,66,1238,8,66,1,67,1,67,1,67,1,68,1,68,1,68,1,68,1,68,1,68, + 5,68,1249,8,68,10,68,12,68,1252,9,68,3,68,1254,8,68,1,68,3,68,1257,8,68, + 1,68,3,68,1260,8,68,1,68,1,68,1,69,1,69,1,70,1,70,1,70,1,70,1,70,1,70, + 1,70,3,70,1273,8,70,1,70,1,70,3,70,1277,8,70,1,71,1,71,1,71,1,71,1,71, + 1,71,1,71,3,71,1286,8,71,1,72,1,72,1,72,1,72,1,72,1,72,3,72,1294,8,72, + 1,73,1,73,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,76,1,77,1,77,1,77,1,78,1,78,1,78,1,79,1,79,1,79,1,79,5,79,1322, + 8,79,10,79,12,79,1325,9,79,1,79,1,79,1,79,0,1,112,80,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,120,122,124,126,128,130,132,134,136,138,140,142, + 144,146,148,150,152,154,156,158,0,26,1,0,101,102,1,0,103,104,1,0,93,94, + 1,0,137,138,2,0,34,35,54,54,1,0,128,129,1,0,47,48,1,0,49,50,2,0,8,8,15, + 15,1,0,124,125,1,0,131,136,2,0,6,6,12,12,2,0,7,7,13,13,2,0,9,9,14,14,1, + 0,105,106,1,0,140,141,3,0,25,25,105,105,107,107,3,0,85,85,106,106,108, + 108,1,0,62,63,2,0,87,87,89,89,2,0,49,49,110,111,1,0,113,114,2,0,107,108, + 110,123,2,0,1,106,109,109,2,0,48,48,120,120,2,0,96,96,121,121,1515,0,161, + 1,0,0,0,2,184,1,0,0,0,4,206,1,0,0,0,6,208,1,0,0,0,8,220,1,0,0,0,10,241, + 1,0,0,0,12,245,1,0,0,0,14,265,1,0,0,0,16,274,1,0,0,0,18,281,1,0,0,0,20, + 292,1,0,0,0,22,316,1,0,0,0,24,335,1,0,0,0,26,363,1,0,0,0,28,365,1,0,0, + 0,30,370,1,0,0,0,32,372,1,0,0,0,34,434,1,0,0,0,36,441,1,0,0,0,38,443,1, + 0,0,0,40,480,1,0,0,0,42,482,1,0,0,0,44,491,1,0,0,0,46,493,1,0,0,0,48,504, + 1,0,0,0,50,511,1,0,0,0,52,530,1,0,0,0,54,603,1,0,0,0,56,694,1,0,0,0,58, + 703,1,0,0,0,60,712,1,0,0,0,62,722,1,0,0,0,64,724,1,0,0,0,66,745,1,0,0, + 0,68,758,1,0,0,0,70,760,1,0,0,0,72,789,1,0,0,0,74,797,1,0,0,0,76,799,1, + 0,0,0,78,823,1,0,0,0,80,825,1,0,0,0,82,835,1,0,0,0,84,838,1,0,0,0,86,860, + 1,0,0,0,88,885,1,0,0,0,90,887,1,0,0,0,92,898,1,0,0,0,94,911,1,0,0,0,96, + 913,1,0,0,0,98,922,1,0,0,0,100,952,1,0,0,0,102,969,1,0,0,0,104,986,1,0, + 0,0,106,988,1,0,0,0,108,991,1,0,0,0,110,1001,1,0,0,0,112,1011,1,0,0,0, + 114,1133,1,0,0,0,116,1135,1,0,0,0,118,1150,1,0,0,0,120,1155,1,0,0,0,122, + 1194,1,0,0,0,124,1196,1,0,0,0,126,1206,1,0,0,0,128,1212,1,0,0,0,130,1223, + 1,0,0,0,132,1237,1,0,0,0,134,1239,1,0,0,0,136,1242,1,0,0,0,138,1263,1, + 0,0,0,140,1265,1,0,0,0,142,1285,1,0,0,0,144,1293,1,0,0,0,146,1295,1,0, + 0,0,148,1297,1,0,0,0,150,1299,1,0,0,0,152,1305,1,0,0,0,154,1311,1,0,0, + 0,156,1314,1,0,0,0,158,1317,1,0,0,0,160,162,3,18,9,0,161,160,1,0,0,0,161, + 162,1,0,0,0,162,177,1,0,0,0,163,178,3,2,1,0,164,178,3,20,10,0,165,178, + 3,38,19,0,166,178,3,22,11,0,167,178,3,24,12,0,168,178,3,32,16,0,169,178, + 3,40,20,0,170,178,3,64,32,0,171,178,3,8,4,0,172,178,3,12,6,0,173,178,3, + 132,66,0,174,178,3,6,3,0,175,178,3,14,7,0,176,178,3,70,35,0,177,163,1, + 0,0,0,177,164,1,0,0,0,177,165,1,0,0,0,177,166,1,0,0,0,177,167,1,0,0,0, + 177,168,1,0,0,0,177,169,1,0,0,0,177,170,1,0,0,0,177,171,1,0,0,0,177,172, + 1,0,0,0,177,173,1,0,0,0,177,174,1,0,0,0,177,175,1,0,0,0,177,176,1,0,0, + 0,178,180,1,0,0,0,179,181,5,142,0,0,180,179,1,0,0,0,180,181,1,0,0,0,181, + 182,1,0,0,0,182,183,5,0,0,1,183,1,1,0,0,0,184,186,5,32,0,0,185,187,5,8, + 0,0,186,185,1,0,0,0,186,187,1,0,0,0,187,188,1,0,0,0,188,189,5,31,0,0,189, + 190,5,137,0,0,190,191,3,70,35,0,191,192,5,138,0,0,192,193,5,33,0,0,193, + 194,3,4,2,0,194,3,1,0,0,0,195,207,3,20,10,0,196,207,3,38,19,0,197,207, + 3,22,11,0,198,207,3,24,12,0,199,207,3,32,16,0,200,207,3,40,20,0,201,207, + 3,64,32,0,202,207,3,8,4,0,203,207,3,12,6,0,204,207,3,6,3,0,205,207,3,70, + 35,0,206,195,1,0,0,0,206,196,1,0,0,0,206,197,1,0,0,0,206,198,1,0,0,0,206, + 199,1,0,0,0,206,200,1,0,0,0,206,201,1,0,0,0,206,202,1,0,0,0,206,203,1, + 0,0,0,206,204,1,0,0,0,206,205,1,0,0,0,207,5,1,0,0,0,208,209,7,0,0,0,209, + 218,3,128,64,0,210,215,3,112,56,0,211,212,5,139,0,0,212,214,3,112,56,0, + 213,211,1,0,0,0,214,217,1,0,0,0,215,213,1,0,0,0,215,216,1,0,0,0,216,219, + 1,0,0,0,217,215,1,0,0,0,218,210,1,0,0,0,218,219,1,0,0,0,219,7,1,0,0,0, + 220,221,5,79,0,0,221,226,3,98,49,0,222,223,5,139,0,0,223,225,3,98,49,0, + 224,222,1,0,0,0,225,228,1,0,0,0,226,224,1,0,0,0,226,227,1,0,0,0,227,229, + 1,0,0,0,228,226,1,0,0,0,229,230,5,83,0,0,230,235,3,10,5,0,231,232,5,139, + 0,0,232,234,3,10,5,0,233,231,1,0,0,0,234,237,1,0,0,0,235,233,1,0,0,0,235, + 236,1,0,0,0,236,239,1,0,0,0,237,235,1,0,0,0,238,240,3,106,53,0,239,238, + 1,0,0,0,239,240,1,0,0,0,240,9,1,0,0,0,241,242,3,124,62,0,242,243,5,131, + 0,0,243,244,3,112,56,0,244,11,1,0,0,0,245,251,5,78,0,0,246,247,3,128,64, + 0,247,248,5,140,0,0,248,249,5,124,0,0,249,252,1,0,0,0,250,252,5,124,0, + 0,251,246,1,0,0,0,251,250,1,0,0,0,251,252,1,0,0,0,252,253,1,0,0,0,253, + 254,5,2,0,0,254,259,3,98,49,0,255,256,5,139,0,0,256,258,3,98,49,0,257, + 255,1,0,0,0,258,261,1,0,0,0,259,257,1,0,0,0,259,260,1,0,0,0,260,263,1, + 0,0,0,261,259,1,0,0,0,262,264,3,106,53,0,263,262,1,0,0,0,263,264,1,0,0, + 0,264,13,1,0,0,0,265,266,5,1,0,0,266,271,3,16,8,0,267,268,5,139,0,0,268, + 270,3,16,8,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,15,1,0,0,0,273,271,1,0,0,0,274,279,5,143,0,0,275,277,5,5,0,0, + 276,275,1,0,0,0,276,277,1,0,0,0,277,278,1,0,0,0,278,280,3,128,64,0,279, + 276,1,0,0,0,279,280,1,0,0,0,280,17,1,0,0,0,281,282,5,100,0,0,282,287,3, + 28,14,0,283,284,5,139,0,0,284,286,3,28,14,0,285,283,1,0,0,0,286,289,1, + 0,0,0,287,285,1,0,0,0,287,288,1,0,0,0,288,290,1,0,0,0,289,287,1,0,0,0, + 290,291,5,142,0,0,291,19,1,0,0,0,292,294,5,57,0,0,293,295,5,91,0,0,294, + 293,1,0,0,0,294,295,1,0,0,0,295,296,1,0,0,0,296,297,5,58,0,0,297,298,3, + 128,64,0,298,299,5,137,0,0,299,304,3,46,23,0,300,301,5,139,0,0,301,303, + 3,46,23,0,302,300,1,0,0,0,303,306,1,0,0,0,304,302,1,0,0,0,304,305,1,0, + 0,0,305,311,1,0,0,0,306,304,1,0,0,0,307,308,5,139,0,0,308,310,3,56,28, + 0,309,307,1,0,0,0,310,313,1,0,0,0,311,309,1,0,0,0,311,312,1,0,0,0,312, + 314,1,0,0,0,313,311,1,0,0,0,314,315,5,138,0,0,315,21,1,0,0,0,316,317,5, + 57,0,0,317,318,5,98,0,0,318,330,3,128,64,0,319,320,5,137,0,0,320,325,3, + 128,64,0,321,322,5,139,0,0,322,324,3,128,64,0,323,321,1,0,0,0,324,327, + 1,0,0,0,325,323,1,0,0,0,325,326,1,0,0,0,326,328,1,0,0,0,327,325,1,0,0, + 0,328,329,5,138,0,0,329,331,1,0,0,0,330,319,1,0,0,0,330,331,1,0,0,0,331, + 332,1,0,0,0,332,333,5,5,0,0,333,334,3,70,35,0,334,23,1,0,0,0,335,336,5, + 57,0,0,336,337,5,99,0,0,337,339,3,128,64,0,338,340,3,26,13,0,339,338,1, + 0,0,0,339,340,1,0,0,0,340,341,1,0,0,0,341,342,5,5,0,0,342,343,3,36,18, + 0,343,25,1,0,0,0,344,345,5,137,0,0,345,350,3,28,14,0,346,347,5,139,0,0, + 347,349,3,28,14,0,348,346,1,0,0,0,349,352,1,0,0,0,350,348,1,0,0,0,350, + 351,1,0,0,0,351,353,1,0,0,0,352,350,1,0,0,0,353,354,5,138,0,0,354,364, + 1,0,0,0,355,360,3,28,14,0,356,357,5,139,0,0,357,359,3,28,14,0,358,356, + 1,0,0,0,359,362,1,0,0,0,360,358,1,0,0,0,360,361,1,0,0,0,361,364,1,0,0, + 0,362,360,1,0,0,0,363,344,1,0,0,0,363,355,1,0,0,0,364,27,1,0,0,0,365,366, + 3,30,15,0,366,367,3,50,25,0,367,29,1,0,0,0,368,371,3,124,62,0,369,371, + 5,144,0,0,370,368,1,0,0,0,370,369,1,0,0,0,371,31,1,0,0,0,372,373,5,64, + 0,0,373,374,5,58,0,0,374,375,3,128,64,0,375,376,3,34,17,0,376,33,1,0,0, + 0,377,379,5,67,0,0,378,380,5,69,0,0,379,378,1,0,0,0,379,380,1,0,0,0,380, + 381,1,0,0,0,381,435,3,46,23,0,382,383,5,67,0,0,383,435,3,56,28,0,384,386, + 5,64,0,0,385,387,5,69,0,0,386,385,1,0,0,0,386,387,1,0,0,0,387,388,1,0, + 0,0,388,389,3,128,64,0,389,393,3,50,25,0,390,392,3,54,27,0,391,390,1,0, + 0,0,392,395,1,0,0,0,393,391,1,0,0,0,393,394,1,0,0,0,394,435,1,0,0,0,395, + 393,1,0,0,0,396,398,5,64,0,0,397,399,5,69,0,0,398,397,1,0,0,0,398,399, + 1,0,0,0,399,400,1,0,0,0,400,401,3,128,64,0,401,402,5,83,0,0,402,403,5, + 84,0,0,403,404,3,112,56,0,404,435,1,0,0,0,405,407,5,64,0,0,406,408,5,69, + 0,0,407,406,1,0,0,0,407,408,1,0,0,0,408,409,1,0,0,0,409,410,3,128,64,0, + 410,411,5,68,0,0,411,412,5,84,0,0,412,435,1,0,0,0,413,414,5,68,0,0,414, + 415,5,69,0,0,415,435,3,128,64,0,416,417,5,68,0,0,417,418,5,75,0,0,418, + 435,3,128,64,0,419,420,5,65,0,0,420,421,5,66,0,0,421,435,3,128,64,0,422, + 423,5,65,0,0,423,424,5,69,0,0,424,425,3,128,64,0,425,426,5,66,0,0,426, + 427,3,128,64,0,427,435,1,0,0,0,428,429,5,65,0,0,429,430,5,90,0,0,430,431, + 3,128,64,0,431,432,5,66,0,0,432,433,3,128,64,0,433,435,1,0,0,0,434,377, + 1,0,0,0,434,382,1,0,0,0,434,384,1,0,0,0,434,396,1,0,0,0,434,405,1,0,0, + 0,434,413,1,0,0,0,434,416,1,0,0,0,434,419,1,0,0,0,434,422,1,0,0,0,434, + 428,1,0,0,0,435,35,1,0,0,0,436,442,3,70,35,0,437,442,3,64,32,0,438,442, + 3,8,4,0,439,442,3,12,6,0,440,442,3,20,10,0,441,436,1,0,0,0,441,437,1,0, + 0,0,441,438,1,0,0,0,441,439,1,0,0,0,441,440,1,0,0,0,442,37,1,0,0,0,443, + 445,5,57,0,0,444,446,5,86,0,0,445,444,1,0,0,0,445,446,1,0,0,0,446,447, + 1,0,0,0,447,448,5,90,0,0,448,449,3,128,64,0,449,450,5,25,0,0,450,451,3, + 128,64,0,451,452,5,137,0,0,452,457,3,42,21,0,453,454,5,139,0,0,454,456, + 3,42,21,0,455,453,1,0,0,0,456,459,1,0,0,0,457,455,1,0,0,0,457,458,1,0, + 0,0,458,460,1,0,0,0,459,457,1,0,0,0,460,463,5,138,0,0,461,462,5,92,0,0, + 462,464,3,44,22,0,463,461,1,0,0,0,463,464,1,0,0,0,464,39,1,0,0,0,465,466, + 5,68,0,0,466,467,5,58,0,0,467,481,3,128,64,0,468,469,5,68,0,0,469,470, + 5,90,0,0,470,471,3,128,64,0,471,472,5,25,0,0,472,473,3,128,64,0,473,481, + 1,0,0,0,474,475,5,68,0,0,475,476,5,99,0,0,476,481,3,128,64,0,477,478,5, + 68,0,0,478,479,5,98,0,0,479,481,3,128,64,0,480,465,1,0,0,0,480,468,1,0, + 0,0,480,474,1,0,0,0,480,477,1,0,0,0,481,41,1,0,0,0,482,484,3,128,64,0, + 483,485,7,1,0,0,484,483,1,0,0,0,484,485,1,0,0,0,485,43,1,0,0,0,486,492, + 5,73,0,0,487,488,5,95,0,0,488,492,5,109,0,0,489,490,5,96,0,0,490,492,5, + 109,0,0,491,486,1,0,0,0,491,487,1,0,0,0,491,489,1,0,0,0,492,45,1,0,0,0, + 493,494,3,128,64,0,494,496,3,50,25,0,495,497,3,48,24,0,496,495,1,0,0,0, + 496,497,1,0,0,0,497,501,1,0,0,0,498,500,3,54,27,0,499,498,1,0,0,0,500, + 503,1,0,0,0,501,499,1,0,0,0,501,502,1,0,0,0,502,47,1,0,0,0,503,501,1,0, + 0,0,504,505,5,5,0,0,505,506,5,137,0,0,506,507,3,112,56,0,507,508,5,138, + 0,0,508,49,1,0,0,0,509,512,3,128,64,0,510,512,5,88,0,0,511,509,1,0,0,0, + 511,510,1,0,0,0,512,514,1,0,0,0,513,515,3,128,64,0,514,513,1,0,0,0,514, + 515,1,0,0,0,515,517,1,0,0,0,516,518,3,128,64,0,517,516,1,0,0,0,517,518, + 1,0,0,0,518,527,1,0,0,0,519,520,5,137,0,0,520,523,3,52,26,0,521,522,5, + 139,0,0,522,524,3,52,26,0,523,521,1,0,0,0,523,524,1,0,0,0,524,525,1,0, + 0,0,525,526,5,138,0,0,526,528,1,0,0,0,527,519,1,0,0,0,527,528,1,0,0,0, + 528,51,1,0,0,0,529,531,5,129,0,0,530,529,1,0,0,0,530,531,1,0,0,0,531,532, + 1,0,0,0,532,533,5,146,0,0,533,53,1,0,0,0,534,535,5,8,0,0,535,604,5,109, + 0,0,536,604,5,109,0,0,537,538,5,84,0,0,538,604,3,112,56,0,539,540,5,92, + 0,0,540,604,7,2,0,0,541,542,5,75,0,0,542,544,3,128,64,0,543,541,1,0,0, + 0,543,544,1,0,0,0,544,545,1,0,0,0,545,546,5,97,0,0,546,547,5,137,0,0,547, + 548,3,58,29,0,548,549,5,138,0,0,549,604,1,0,0,0,550,551,5,75,0,0,551,553, + 3,128,64,0,552,550,1,0,0,0,552,553,1,0,0,0,553,554,1,0,0,0,554,555,5,73, + 0,0,555,557,5,74,0,0,556,558,3,138,69,0,557,556,1,0,0,0,557,558,1,0,0, + 0,558,604,1,0,0,0,559,560,5,75,0,0,560,562,3,128,64,0,561,559,1,0,0,0, + 561,562,1,0,0,0,562,563,1,0,0,0,563,565,5,86,0,0,564,566,3,138,69,0,565, + 564,1,0,0,0,565,566,1,0,0,0,566,604,1,0,0,0,567,568,5,75,0,0,568,570,3, + 128,64,0,569,567,1,0,0,0,569,570,1,0,0,0,570,571,1,0,0,0,571,572,5,77, + 0,0,572,584,3,128,64,0,573,574,5,137,0,0,574,579,3,128,64,0,575,576,5, + 139,0,0,576,578,3,128,64,0,577,575,1,0,0,0,578,581,1,0,0,0,579,577,1,0, + 0,0,579,580,1,0,0,0,580,582,1,0,0,0,581,579,1,0,0,0,582,583,5,138,0,0, + 583,585,1,0,0,0,584,573,1,0,0,0,584,585,1,0,0,0,585,589,1,0,0,0,586,588, + 3,60,30,0,587,586,1,0,0,0,588,591,1,0,0,0,589,587,1,0,0,0,589,590,1,0, + 0,0,590,604,1,0,0,0,591,589,1,0,0,0,592,601,5,88,0,0,593,594,5,137,0,0, + 594,597,3,52,26,0,595,596,5,139,0,0,596,598,3,52,26,0,597,595,1,0,0,0, + 597,598,1,0,0,0,598,599,1,0,0,0,599,600,5,138,0,0,600,602,1,0,0,0,601, + 593,1,0,0,0,601,602,1,0,0,0,602,604,1,0,0,0,603,534,1,0,0,0,603,536,1, + 0,0,0,603,537,1,0,0,0,603,539,1,0,0,0,603,543,1,0,0,0,603,552,1,0,0,0, + 603,561,1,0,0,0,603,569,1,0,0,0,603,592,1,0,0,0,604,55,1,0,0,0,605,606, + 5,75,0,0,606,608,3,128,64,0,607,605,1,0,0,0,607,608,1,0,0,0,608,609,1, + 0,0,0,609,610,5,73,0,0,610,612,5,74,0,0,611,613,3,138,69,0,612,611,1,0, + 0,0,612,613,1,0,0,0,613,614,1,0,0,0,614,615,5,137,0,0,615,620,3,128,64, + 0,616,617,5,139,0,0,617,619,3,128,64,0,618,616,1,0,0,0,619,622,1,0,0,0, + 620,618,1,0,0,0,620,621,1,0,0,0,621,623,1,0,0,0,622,620,1,0,0,0,623,624, + 5,138,0,0,624,695,1,0,0,0,625,626,5,75,0,0,626,628,3,128,64,0,627,625, + 1,0,0,0,627,628,1,0,0,0,628,629,1,0,0,0,629,631,5,86,0,0,630,632,3,138, + 69,0,631,630,1,0,0,0,631,632,1,0,0,0,632,633,1,0,0,0,633,634,5,137,0,0, + 634,639,3,128,64,0,635,636,5,139,0,0,636,638,3,128,64,0,637,635,1,0,0, + 0,638,641,1,0,0,0,639,637,1,0,0,0,639,640,1,0,0,0,640,642,1,0,0,0,641, + 639,1,0,0,0,642,643,5,138,0,0,643,695,1,0,0,0,644,645,5,75,0,0,645,647, + 3,128,64,0,646,644,1,0,0,0,646,647,1,0,0,0,647,648,1,0,0,0,648,649,5,76, + 0,0,649,652,5,74,0,0,650,651,5,85,0,0,651,653,5,90,0,0,652,650,1,0,0,0, + 652,653,1,0,0,0,653,654,1,0,0,0,654,655,5,137,0,0,655,660,3,128,64,0,656, + 657,5,139,0,0,657,659,3,128,64,0,658,656,1,0,0,0,659,662,1,0,0,0,660,658, + 1,0,0,0,660,661,1,0,0,0,661,663,1,0,0,0,662,660,1,0,0,0,663,664,5,138, + 0,0,664,665,5,77,0,0,665,677,3,128,64,0,666,667,5,137,0,0,667,672,3,128, + 64,0,668,669,5,139,0,0,669,671,3,128,64,0,670,668,1,0,0,0,671,674,1,0, + 0,0,672,670,1,0,0,0,672,673,1,0,0,0,673,675,1,0,0,0,674,672,1,0,0,0,675, + 676,5,138,0,0,676,678,1,0,0,0,677,666,1,0,0,0,677,678,1,0,0,0,678,682, + 1,0,0,0,679,681,3,60,30,0,680,679,1,0,0,0,681,684,1,0,0,0,682,680,1,0, + 0,0,682,683,1,0,0,0,683,695,1,0,0,0,684,682,1,0,0,0,685,686,5,75,0,0,686, + 688,3,128,64,0,687,685,1,0,0,0,687,688,1,0,0,0,688,689,1,0,0,0,689,690, + 5,97,0,0,690,691,5,137,0,0,691,692,3,58,29,0,692,693,5,138,0,0,693,695, + 1,0,0,0,694,607,1,0,0,0,694,627,1,0,0,0,694,646,1,0,0,0,694,687,1,0,0, + 0,695,57,1,0,0,0,696,702,8,3,0,0,697,698,5,137,0,0,698,699,3,58,29,0,699, + 700,5,138,0,0,700,702,1,0,0,0,701,696,1,0,0,0,701,697,1,0,0,0,702,705, + 1,0,0,0,703,701,1,0,0,0,703,704,1,0,0,0,704,59,1,0,0,0,705,703,1,0,0,0, + 706,707,5,25,0,0,707,708,5,79,0,0,708,713,3,62,31,0,709,710,5,25,0,0,710, + 711,5,78,0,0,711,713,3,62,31,0,712,706,1,0,0,0,712,709,1,0,0,0,713,61, + 1,0,0,0,714,723,5,80,0,0,715,716,5,85,0,0,716,723,5,82,0,0,717,723,5,81, + 0,0,718,719,5,83,0,0,719,723,5,109,0,0,720,721,5,83,0,0,721,723,5,84,0, + 0,722,714,1,0,0,0,722,715,1,0,0,0,722,717,1,0,0,0,722,718,1,0,0,0,722, + 720,1,0,0,0,723,63,1,0,0,0,724,725,5,70,0,0,725,726,5,71,0,0,726,743,3, + 128,64,0,727,728,5,137,0,0,728,733,3,128,64,0,729,730,5,139,0,0,730,732, + 3,128,64,0,731,729,1,0,0,0,732,735,1,0,0,0,733,731,1,0,0,0,733,734,1,0, + 0,0,734,736,1,0,0,0,735,733,1,0,0,0,736,737,5,138,0,0,737,739,1,0,0,0, + 738,727,1,0,0,0,738,739,1,0,0,0,739,740,1,0,0,0,740,744,3,70,35,0,741, + 742,5,84,0,0,742,744,5,72,0,0,743,738,1,0,0,0,743,741,1,0,0,0,744,65,1, + 0,0,0,745,746,5,137,0,0,746,751,3,68,34,0,747,748,5,139,0,0,748,750,3, + 68,34,0,749,747,1,0,0,0,750,753,1,0,0,0,751,749,1,0,0,0,751,752,1,0,0, + 0,752,754,1,0,0,0,753,751,1,0,0,0,754,755,5,138,0,0,755,67,1,0,0,0,756, + 759,5,84,0,0,757,759,3,112,56,0,758,756,1,0,0,0,758,757,1,0,0,0,759,69, + 1,0,0,0,760,766,3,72,36,0,761,762,3,74,37,0,762,763,3,72,36,0,763,765, + 1,0,0,0,764,761,1,0,0,0,765,768,1,0,0,0,766,764,1,0,0,0,766,767,1,0,0, + 0,767,770,1,0,0,0,768,766,1,0,0,0,769,771,3,108,54,0,770,769,1,0,0,0,770, + 771,1,0,0,0,771,773,1,0,0,0,772,774,3,88,44,0,773,772,1,0,0,0,773,774, + 1,0,0,0,774,71,1,0,0,0,775,790,3,76,38,0,776,777,5,137,0,0,777,778,3,70, + 35,0,778,779,5,138,0,0,779,790,1,0,0,0,780,781,5,72,0,0,781,786,3,66,33, + 0,782,783,5,139,0,0,783,785,3,66,33,0,784,782,1,0,0,0,785,788,1,0,0,0, + 786,784,1,0,0,0,786,787,1,0,0,0,787,790,1,0,0,0,788,786,1,0,0,0,789,775, + 1,0,0,0,789,776,1,0,0,0,789,780,1,0,0,0,790,73,1,0,0,0,791,793,5,53,0, + 0,792,794,5,54,0,0,793,792,1,0,0,0,793,794,1,0,0,0,794,798,1,0,0,0,795, + 798,5,55,0,0,796,798,5,56,0,0,797,791,1,0,0,0,797,795,1,0,0,0,797,796, + 1,0,0,0,798,75,1,0,0,0,799,801,5,1,0,0,800,802,3,78,39,0,801,800,1,0,0, + 0,801,802,1,0,0,0,802,804,1,0,0,0,803,805,3,84,42,0,804,803,1,0,0,0,804, + 805,1,0,0,0,805,806,1,0,0,0,806,809,3,92,46,0,807,808,5,71,0,0,808,810, + 3,128,64,0,809,807,1,0,0,0,809,810,1,0,0,0,810,812,1,0,0,0,811,813,3,96, + 48,0,812,811,1,0,0,0,812,813,1,0,0,0,813,815,1,0,0,0,814,816,3,106,53, + 0,815,814,1,0,0,0,815,816,1,0,0,0,816,818,1,0,0,0,817,819,3,80,40,0,818, + 817,1,0,0,0,818,819,1,0,0,0,819,821,1,0,0,0,820,822,3,82,41,0,821,820, + 1,0,0,0,821,822,1,0,0,0,822,77,1,0,0,0,823,824,7,4,0,0,824,79,1,0,0,0, + 825,826,5,27,0,0,826,827,5,29,0,0,827,832,3,112,56,0,828,829,5,139,0,0, + 829,831,3,112,56,0,830,828,1,0,0,0,831,834,1,0,0,0,832,830,1,0,0,0,832, + 833,1,0,0,0,833,81,1,0,0,0,834,832,1,0,0,0,835,836,5,30,0,0,836,837,3, + 112,56,0,837,83,1,0,0,0,838,839,5,4,0,0,839,844,3,86,43,0,840,841,7,5, + 0,0,841,843,3,86,43,0,842,840,1,0,0,0,843,846,1,0,0,0,844,842,1,0,0,0, + 844,845,1,0,0,0,845,848,1,0,0,0,846,844,1,0,0,0,847,849,5,36,0,0,848,847, + 1,0,0,0,848,849,1,0,0,0,849,852,1,0,0,0,850,851,5,92,0,0,851,853,5,117, + 0,0,852,850,1,0,0,0,852,853,1,0,0,0,853,85,1,0,0,0,854,861,5,146,0,0,855, + 861,5,144,0,0,856,857,5,137,0,0,857,858,3,112,56,0,858,859,5,138,0,0,859, + 861,1,0,0,0,860,854,1,0,0,0,860,855,1,0,0,0,860,856,1,0,0,0,861,87,1,0, + 0,0,862,863,5,45,0,0,863,864,3,112,56,0,864,874,3,90,45,0,865,866,5,46, + 0,0,866,867,7,6,0,0,867,868,3,112,56,0,868,872,3,90,45,0,869,873,5,51, + 0,0,870,871,5,92,0,0,871,873,5,117,0,0,872,869,1,0,0,0,872,870,1,0,0,0, + 873,875,1,0,0,0,874,865,1,0,0,0,874,875,1,0,0,0,875,886,1,0,0,0,876,877, + 5,46,0,0,877,878,7,6,0,0,878,879,3,112,56,0,879,883,3,90,45,0,880,884, + 5,51,0,0,881,882,5,92,0,0,882,884,5,117,0,0,883,880,1,0,0,0,883,881,1, + 0,0,0,884,886,1,0,0,0,885,862,1,0,0,0,885,876,1,0,0,0,886,89,1,0,0,0,887, + 888,7,7,0,0,888,91,1,0,0,0,889,899,5,124,0,0,890,895,3,94,47,0,891,892, + 5,139,0,0,892,894,3,94,47,0,893,891,1,0,0,0,894,897,1,0,0,0,895,893,1, + 0,0,0,895,896,1,0,0,0,896,899,1,0,0,0,897,895,1,0,0,0,898,889,1,0,0,0, + 898,890,1,0,0,0,899,93,1,0,0,0,900,901,3,128,64,0,901,902,5,140,0,0,902, + 903,5,124,0,0,903,912,1,0,0,0,904,909,3,112,56,0,905,907,5,5,0,0,906,905, + 1,0,0,0,906,907,1,0,0,0,907,908,1,0,0,0,908,910,3,128,64,0,909,906,1,0, + 0,0,909,910,1,0,0,0,910,912,1,0,0,0,911,900,1,0,0,0,911,904,1,0,0,0,912, + 95,1,0,0,0,913,914,5,2,0,0,914,919,3,98,49,0,915,916,5,139,0,0,916,918, + 3,98,49,0,917,915,1,0,0,0,918,921,1,0,0,0,919,917,1,0,0,0,919,920,1,0, + 0,0,920,97,1,0,0,0,921,919,1,0,0,0,922,926,3,100,50,0,923,925,3,102,51, + 0,924,923,1,0,0,0,925,928,1,0,0,0,926,924,1,0,0,0,926,927,1,0,0,0,927, + 99,1,0,0,0,928,926,1,0,0,0,929,934,3,128,64,0,930,932,5,5,0,0,931,930, + 1,0,0,0,931,932,1,0,0,0,932,933,1,0,0,0,933,935,3,128,64,0,934,931,1,0, + 0,0,934,935,1,0,0,0,935,953,1,0,0,0,936,937,5,137,0,0,937,938,3,70,35, + 0,938,946,5,138,0,0,939,941,5,5,0,0,940,939,1,0,0,0,940,941,1,0,0,0,941, + 942,1,0,0,0,942,944,3,128,64,0,943,945,3,158,79,0,944,943,1,0,0,0,944, + 945,1,0,0,0,945,947,1,0,0,0,946,940,1,0,0,0,946,947,1,0,0,0,947,953,1, + 0,0,0,948,949,5,137,0,0,949,950,3,98,49,0,950,951,5,138,0,0,951,953,1, + 0,0,0,952,929,1,0,0,0,952,936,1,0,0,0,952,948,1,0,0,0,953,101,1,0,0,0, + 954,955,3,104,52,0,955,956,5,23,0,0,956,957,3,100,50,0,957,958,5,25,0, + 0,958,959,3,112,56,0,959,970,1,0,0,0,960,961,5,37,0,0,961,962,5,23,0,0, + 962,970,3,100,50,0,963,964,5,37,0,0,964,965,5,38,0,0,965,970,3,100,50, + 0,966,967,5,22,0,0,967,968,5,38,0,0,968,970,3,100,50,0,969,954,1,0,0,0, + 969,960,1,0,0,0,969,963,1,0,0,0,969,966,1,0,0,0,970,103,1,0,0,0,971,973, + 5,18,0,0,972,971,1,0,0,0,972,973,1,0,0,0,973,987,1,0,0,0,974,976,5,19, + 0,0,975,977,5,22,0,0,976,975,1,0,0,0,976,977,1,0,0,0,977,987,1,0,0,0,978, + 980,5,20,0,0,979,981,5,22,0,0,980,979,1,0,0,0,980,981,1,0,0,0,981,987, + 1,0,0,0,982,984,5,21,0,0,983,985,5,22,0,0,984,983,1,0,0,0,984,985,1,0, + 0,0,985,987,1,0,0,0,986,972,1,0,0,0,986,974,1,0,0,0,986,978,1,0,0,0,986, + 982,1,0,0,0,987,105,1,0,0,0,988,989,5,3,0,0,989,990,3,112,56,0,990,107, + 1,0,0,0,991,992,5,26,0,0,992,993,5,29,0,0,993,998,3,110,55,0,994,995,5, + 139,0,0,995,997,3,110,55,0,996,994,1,0,0,0,997,1000,1,0,0,0,998,996,1, + 0,0,0,998,999,1,0,0,0,999,109,1,0,0,0,1000,998,1,0,0,0,1001,1003,3,112, + 56,0,1002,1004,7,1,0,0,1003,1002,1,0,0,0,1003,1004,1,0,0,0,1004,111,1, + 0,0,0,1005,1006,6,56,-1,0,1006,1007,7,5,0,0,1007,1012,3,112,56,21,1008, + 1009,7,8,0,0,1009,1012,3,112,56,7,1010,1012,3,114,57,0,1011,1005,1,0,0, + 0,1011,1008,1,0,0,0,1011,1010,1,0,0,0,1012,1111,1,0,0,0,1013,1014,10,22, + 0,0,1014,1015,5,127,0,0,1015,1110,3,112,56,23,1016,1017,10,20,0,0,1017, + 1018,7,9,0,0,1018,1110,3,112,56,21,1019,1020,10,19,0,0,1020,1021,5,126, + 0,0,1021,1110,3,112,56,20,1022,1023,10,18,0,0,1023,1024,5,17,0,0,1024, + 1110,3,112,56,19,1025,1026,10,17,0,0,1026,1027,7,5,0,0,1027,1110,3,112, + 56,18,1028,1029,10,16,0,0,1029,1030,5,130,0,0,1030,1110,3,112,56,17,1031, + 1032,10,15,0,0,1032,1033,7,10,0,0,1033,1110,3,112,56,16,1034,1036,10,14, + 0,0,1035,1037,5,8,0,0,1036,1035,1,0,0,0,1036,1037,1,0,0,0,1037,1038,1, + 0,0,0,1038,1039,5,52,0,0,1039,1040,3,112,56,0,1040,1041,5,6,0,0,1041,1042, + 3,112,56,15,1042,1110,1,0,0,0,1043,1045,10,13,0,0,1044,1046,5,8,0,0,1045, + 1044,1,0,0,0,1045,1046,1,0,0,0,1046,1047,1,0,0,0,1047,1048,5,16,0,0,1048, + 1110,3,112,56,14,1049,1050,10,8,0,0,1050,1052,5,28,0,0,1051,1053,5,8,0, + 0,1052,1051,1,0,0,0,1052,1053,1,0,0,0,1053,1054,1,0,0,0,1054,1055,5,35, + 0,0,1055,1056,5,2,0,0,1056,1110,3,112,56,9,1057,1058,10,6,0,0,1058,1059, + 7,11,0,0,1059,1110,3,112,56,7,1060,1061,10,5,0,0,1061,1062,7,12,0,0,1062, + 1110,3,112,56,6,1063,1064,10,4,0,0,1064,1065,7,13,0,0,1065,1110,3,112, + 56,5,1066,1067,10,3,0,0,1067,1068,5,10,0,0,1068,1110,3,112,56,4,1069,1070, + 10,2,0,0,1070,1071,5,11,0,0,1071,1110,3,112,56,3,1072,1074,10,12,0,0,1073, + 1075,5,8,0,0,1074,1073,1,0,0,0,1074,1075,1,0,0,0,1075,1076,1,0,0,0,1076, + 1077,5,24,0,0,1077,1078,5,137,0,0,1078,1079,3,70,35,0,1079,1080,5,138, + 0,0,1080,1110,1,0,0,0,1081,1083,10,11,0,0,1082,1084,5,8,0,0,1083,1082, + 1,0,0,0,1083,1084,1,0,0,0,1084,1085,1,0,0,0,1085,1086,5,24,0,0,1086,1087, + 5,137,0,0,1087,1092,3,112,56,0,1088,1089,5,139,0,0,1089,1091,3,112,56, + 0,1090,1088,1,0,0,0,1091,1094,1,0,0,0,1092,1090,1,0,0,0,1092,1093,1,0, + 0,0,1093,1095,1,0,0,0,1094,1092,1,0,0,0,1095,1096,5,138,0,0,1096,1110, + 1,0,0,0,1097,1098,10,10,0,0,1098,1100,5,28,0,0,1099,1101,5,8,0,0,1100, + 1099,1,0,0,0,1100,1101,1,0,0,0,1101,1102,1,0,0,0,1102,1110,5,109,0,0,1103, + 1104,10,9,0,0,1104,1106,5,28,0,0,1105,1107,5,8,0,0,1106,1105,1,0,0,0,1106, + 1107,1,0,0,0,1107,1108,1,0,0,0,1108,1110,7,14,0,0,1109,1013,1,0,0,0,1109, + 1016,1,0,0,0,1109,1019,1,0,0,0,1109,1022,1,0,0,0,1109,1025,1,0,0,0,1109, + 1028,1,0,0,0,1109,1031,1,0,0,0,1109,1034,1,0,0,0,1109,1043,1,0,0,0,1109, + 1049,1,0,0,0,1109,1057,1,0,0,0,1109,1060,1,0,0,0,1109,1063,1,0,0,0,1109, + 1066,1,0,0,0,1109,1069,1,0,0,0,1109,1072,1,0,0,0,1109,1081,1,0,0,0,1109, + 1097,1,0,0,0,1109,1103,1,0,0,0,1110,1113,1,0,0,0,1111,1109,1,0,0,0,1111, + 1112,1,0,0,0,1112,113,1,0,0,0,1113,1111,1,0,0,0,1114,1134,3,130,65,0,1115, + 1134,3,116,58,0,1116,1134,3,120,60,0,1117,1134,3,124,62,0,1118,1134,5, + 144,0,0,1119,1134,5,143,0,0,1120,1121,5,31,0,0,1121,1122,5,137,0,0,1122, + 1123,3,70,35,0,1123,1124,5,138,0,0,1124,1134,1,0,0,0,1125,1126,5,137,0, + 0,1126,1127,3,70,35,0,1127,1128,5,138,0,0,1128,1134,1,0,0,0,1129,1130, + 5,137,0,0,1130,1131,3,112,56,0,1131,1132,5,138,0,0,1132,1134,1,0,0,0,1133, + 1114,1,0,0,0,1133,1115,1,0,0,0,1133,1116,1,0,0,0,1133,1117,1,0,0,0,1133, + 1118,1,0,0,0,1133,1119,1,0,0,0,1133,1120,1,0,0,0,1133,1125,1,0,0,0,1133, + 1129,1,0,0,0,1134,115,1,0,0,0,1135,1137,5,41,0,0,1136,1138,3,112,56,0, + 1137,1136,1,0,0,0,1137,1138,1,0,0,0,1138,1140,1,0,0,0,1139,1141,3,118, + 59,0,1140,1139,1,0,0,0,1141,1142,1,0,0,0,1142,1140,1,0,0,0,1142,1143,1, + 0,0,0,1143,1146,1,0,0,0,1144,1145,5,43,0,0,1145,1147,3,112,56,0,1146,1144, + 1,0,0,0,1146,1147,1,0,0,0,1147,1148,1,0,0,0,1148,1149,5,44,0,0,1149,117, + 1,0,0,0,1150,1151,5,42,0,0,1151,1152,3,112,56,0,1152,1153,5,33,0,0,1153, + 1154,3,112,56,0,1154,119,1,0,0,0,1155,1156,3,122,61,0,1156,1169,5,137, + 0,0,1157,1170,5,124,0,0,1158,1160,5,35,0,0,1159,1158,1,0,0,0,1159,1160, + 1,0,0,0,1160,1161,1,0,0,0,1161,1166,3,112,56,0,1162,1163,5,139,0,0,1163, + 1165,3,112,56,0,1164,1162,1,0,0,0,1165,1168,1,0,0,0,1166,1164,1,0,0,0, + 1166,1167,1,0,0,0,1167,1170,1,0,0,0,1168,1166,1,0,0,0,1169,1157,1,0,0, + 0,1169,1159,1,0,0,0,1169,1170,1,0,0,0,1170,1171,1,0,0,0,1171,1173,5,138, + 0,0,1172,1174,3,152,76,0,1173,1172,1,0,0,0,1173,1174,1,0,0,0,1174,1176, + 1,0,0,0,1175,1177,3,150,75,0,1176,1175,1,0,0,0,1176,1177,1,0,0,0,1177, + 1186,1,0,0,0,1178,1180,3,154,77,0,1179,1178,1,0,0,0,1179,1180,1,0,0,0, + 1180,1182,1,0,0,0,1181,1183,3,156,78,0,1182,1181,1,0,0,0,1182,1183,1,0, + 0,0,1183,1184,1,0,0,0,1184,1185,5,39,0,0,1185,1187,3,136,68,0,1186,1179, + 1,0,0,0,1186,1187,1,0,0,0,1187,121,1,0,0,0,1188,1195,3,128,64,0,1189,1195, + 5,19,0,0,1190,1195,5,20,0,0,1191,1195,5,103,0,0,1192,1195,5,48,0,0,1193, + 1195,5,40,0,0,1194,1188,1,0,0,0,1194,1189,1,0,0,0,1194,1190,1,0,0,0,1194, + 1191,1,0,0,0,1194,1192,1,0,0,0,1194,1193,1,0,0,0,1195,123,1,0,0,0,1196, + 1201,3,128,64,0,1197,1198,7,15,0,0,1198,1200,3,126,63,0,1199,1197,1,0, + 0,0,1200,1203,1,0,0,0,1201,1199,1,0,0,0,1201,1202,1,0,0,0,1202,125,1,0, + 0,0,1203,1201,1,0,0,0,1204,1207,3,128,64,0,1205,1207,3,148,74,0,1206,1204, + 1,0,0,0,1206,1205,1,0,0,0,1207,127,1,0,0,0,1208,1213,5,153,0,0,1209,1213, + 5,151,0,0,1210,1213,5,152,0,0,1211,1213,3,146,73,0,1212,1208,1,0,0,0,1212, + 1209,1,0,0,0,1212,1210,1,0,0,0,1212,1211,1,0,0,0,1213,129,1,0,0,0,1214, + 1224,5,146,0,0,1215,1224,5,147,0,0,1216,1224,5,145,0,0,1217,1224,5,148, + 0,0,1218,1224,5,149,0,0,1219,1224,5,150,0,0,1220,1224,7,16,0,0,1221,1224, + 7,17,0,0,1222,1224,5,109,0,0,1223,1214,1,0,0,0,1223,1215,1,0,0,0,1223, + 1216,1,0,0,0,1223,1217,1,0,0,0,1223,1218,1,0,0,0,1223,1219,1,0,0,0,1223, + 1220,1,0,0,0,1223,1221,1,0,0,0,1223,1222,1,0,0,0,1224,131,1,0,0,0,1225, + 1227,5,59,0,0,1226,1228,7,18,0,0,1227,1226,1,0,0,0,1227,1228,1,0,0,0,1228, + 1238,1,0,0,0,1229,1231,5,60,0,0,1230,1232,7,18,0,0,1231,1230,1,0,0,0,1231, + 1232,1,0,0,0,1232,1238,1,0,0,0,1233,1235,5,61,0,0,1234,1236,7,18,0,0,1235, + 1234,1,0,0,0,1235,1236,1,0,0,0,1236,1238,1,0,0,0,1237,1225,1,0,0,0,1237, + 1229,1,0,0,0,1237,1233,1,0,0,0,1238,133,1,0,0,0,1239,1240,3,112,56,0,1240, + 1241,5,0,0,1,1241,135,1,0,0,0,1242,1253,5,137,0,0,1243,1244,5,40,0,0,1244, + 1245,5,29,0,0,1245,1250,3,112,56,0,1246,1247,5,139,0,0,1247,1249,3,112, + 56,0,1248,1246,1,0,0,0,1249,1252,1,0,0,0,1250,1248,1,0,0,0,1250,1251,1, + 0,0,0,1251,1254,1,0,0,0,1252,1250,1,0,0,0,1253,1243,1,0,0,0,1253,1254, + 1,0,0,0,1254,1256,1,0,0,0,1255,1257,3,108,54,0,1256,1255,1,0,0,0,1256, + 1257,1,0,0,0,1257,1259,1,0,0,0,1258,1260,3,140,70,0,1259,1258,1,0,0,0, + 1259,1260,1,0,0,0,1260,1261,1,0,0,0,1261,1262,5,138,0,0,1262,137,1,0,0, + 0,1263,1264,7,19,0,0,1264,139,1,0,0,0,1265,1272,7,20,0,0,1266,1267,5,52, + 0,0,1267,1268,3,142,71,0,1268,1269,5,6,0,0,1269,1270,3,142,71,0,1270,1273, + 1,0,0,0,1271,1273,3,142,71,0,1272,1266,1,0,0,0,1272,1271,1,0,0,0,1273, + 1276,1,0,0,0,1274,1275,5,116,0,0,1275,1277,3,144,72,0,1276,1274,1,0,0, + 0,1276,1277,1,0,0,0,1277,141,1,0,0,0,1278,1279,5,112,0,0,1279,1286,7,21, + 0,0,1280,1281,5,115,0,0,1281,1286,5,50,0,0,1282,1283,3,112,56,0,1283,1284, + 7,21,0,0,1284,1286,1,0,0,0,1285,1278,1,0,0,0,1285,1280,1,0,0,0,1285,1282, + 1,0,0,0,1286,143,1,0,0,0,1287,1288,5,115,0,0,1288,1294,5,50,0,0,1289,1294, + 5,27,0,0,1290,1294,5,117,0,0,1291,1292,5,85,0,0,1292,1294,5,118,0,0,1293, + 1287,1,0,0,0,1293,1289,1,0,0,0,1293,1290,1,0,0,0,1293,1291,1,0,0,0,1294, + 145,1,0,0,0,1295,1296,7,22,0,0,1296,147,1,0,0,0,1297,1298,7,23,0,0,1298, + 149,1,0,0,0,1299,1300,5,123,0,0,1300,1301,5,137,0,0,1301,1302,5,3,0,0, + 1302,1303,3,112,56,0,1303,1304,5,138,0,0,1304,151,1,0,0,0,1305,1306,5, + 119,0,0,1306,1307,5,27,0,0,1307,1308,5,137,0,0,1308,1309,3,108,54,0,1309, + 1310,5,138,0,0,1310,153,1,0,0,0,1311,1312,5,2,0,0,1312,1313,7,24,0,0,1313, + 155,1,0,0,0,1314,1315,7,25,0,0,1315,1316,5,122,0,0,1316,157,1,0,0,0,1317, + 1318,5,137,0,0,1318,1323,3,128,64,0,1319,1320,5,139,0,0,1320,1322,3,128, + 64,0,1321,1319,1,0,0,0,1322,1325,1,0,0,0,1323,1321,1,0,0,0,1323,1324,1, + 0,0,0,1324,1326,1,0,0,0,1325,1323,1,0,0,0,1326,1327,5,138,0,0,1327,159, + 1,0,0,0,168,161,177,180,186,206,215,218,226,235,239,251,259,263,271,276, + 279,287,294,304,311,325,330,339,350,360,363,370,379,386,393,398,407,434, + 441,445,457,463,480,484,491,496,501,511,514,517,523,527,530,543,552,557, + 561,565,569,579,584,589,597,601,603,607,612,620,627,631,639,646,652,660, + 672,677,682,687,694,701,703,712,722,733,738,743,751,758,766,770,773,786, + 789,793,797,801,804,809,812,815,818,821,832,844,848,852,860,872,874,883, + 885,895,898,906,909,911,919,926,931,934,940,944,946,952,969,972,976,980, + 984,986,998,1003,1011,1036,1045,1052,1074,1083,1092,1100,1106,1109,1111, + 1133,1137,1142,1146,1159,1166,1169,1173,1176,1179,1182,1186,1194,1201, + 1206,1212,1223,1227,1231,1235,1237,1250,1253,1256,1259,1272,1276,1285, + 1293,1323 }; 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 5d38631c..550b614c 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs @@ -1082,5 +1082,11 @@ public interface IAccessSqlVisitor : IParseTreeVisitor { /// The parse tree. /// The visitor result. Result VisitNullTreatment([NotNull] AccessSqlParser.NullTreatmentContext context); + /// + /// Visit a parse tree produced by . + /// + /// The parse tree. + /// The visitor result. + Result VisitDerivedColumns([NotNull] AccessSqlParser.DerivedColumnsContext context); } } // namespace LibRed.Sql.Grammar diff --git a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs index cd588670..355e93b9 100644 --- a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs +++ b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs @@ -789,6 +789,9 @@ private static (ViewSource Source, string Alias) BuildViewSource(TablePrimaryCon case NamedTablePrimaryContext n: string alias = n.alias is null ? Identifier(n.table) : Identifier(n.alias); return (new ViewSource(Identifier(n.table), n.alias is null ? null : Identifier(n.alias)), alias); + case SubqueryPrimaryContext s when s.derivedColumns() is not null: + // Access stores a derived source as its query text alone, and ACE has no column-list syntax. + throw new NotSupportedException("A view cannot store a derived table's column list."); case SubqueryPrimaryContext s when s.alias is not null: string subAlias = Identifier(s.alias); return (new ViewSource(Table: null, subAlias, OriginalText(s.queryExpression())), subAlias); @@ -1065,11 +1068,24 @@ private static TableReference BuildTableSource(TableSourceContext ctx) private static TableReference BuildTablePrimary(TablePrimaryContext ctx) => ctx switch { NamedTablePrimaryContext n => new NamedTable(Identifier(n.table), OptionalIdentifier(n.alias)), - SubqueryPrimaryContext s => new SubqueryTable(BuildQueryExpression(s.queryExpression()), OptionalIdentifier(s.alias)), + SubqueryPrimaryContext s => new SubqueryTable( + BuildQueryExpression(s.queryExpression()), OptionalIdentifier(s.alias), DerivedColumns(s)), ParenJoinPrimaryContext p => BuildTableSource(p.tableSource()), // a parenthesized join group is just nested _ => throw new SqlParseException($"Unsupported table source: {ctx.GetText()}"), }; + /// A derived table's column list, AS t(a, b), or null without one. A name may appear once. + private static List? DerivedColumns(SubqueryPrimaryContext ctx) + { + if (ctx.derivedColumns() is not { } list) + return null; + List names = [.. list._names.Select(Identifier)]; + if (names.GroupBy(n => n, StringComparer.OrdinalIgnoreCase).FirstOrDefault(g => g.Count() > 1) is { } repeated) + throw new SqlParseException( + $"The column '{repeated.Key}' is named more than once in the column list of '{Identifier(ctx.alias)}'."); + return names; + } + private static JoinKind JoinKindOf(JoinTypeContext ctx) => ctx switch { LeftJoinContext => JoinKind.Left, diff --git a/src/LibRed/README.md b/src/LibRed/README.md index 6b758ab6..c823518c 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -226,6 +226,8 @@ Treat the number as of its date — an EF Core version bump moves it. - **Standard scalar syntax** ACE lacks: `CASE`, `COALESCE`, `NULLIF`, `GREATEST`/`LEAST` (NULL arguments ignored, as SQL Server and PostgreSQL treat them — extended mode translates `Math.Max`/`Math.Min` to them), and the `VALUES` table value constructor standing in for a query. + - **A derived table's column list** — `(VALUES (1, 'a')) AS v(n, s)`, `(SELECT …) AS t(a, b)` — naming its + columns in order, as EF Core writes an inline collection. A view cannot store one. - **Access's `Nz`**, which the Access application has and ACE's OLE DB provider does not, with Access's own results — see [functions.md](docs/functions.md#nz). - **Arguments ACE's own functions do not take**: `LOG(x, base)`, and SQL Server 2022's diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/AdHocMiscellaneousQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/AdHocMiscellaneousQueryLibRedTest.cs index 46758588..e55d7b5b 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/AdHocMiscellaneousQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/AdHocMiscellaneousQueryLibRedTest.cs @@ -2039,7 +2039,7 @@ public override async Task Check_inlined_constants_redacting(bool async, bool en FROM `TestEntities` AS `t` WHERE EXISTS ( SELECT 1 - FROM (SELECT CLNG(?) AS `Value` UNION ALL VALUES (?), (?)) AS `i` + FROM (VALUES (CLNG(?)), (?), (?)) AS `i`(`Value`) WHERE `i`.`Value` = `t`.`Id`) """, // @@ -2063,7 +2063,7 @@ SELECT 1 FROM `TestEntities` AS `t` WHERE EXISTS ( SELECT 1 - FROM (SELECT CLNG(1) AS `Value` UNION ALL VALUES (2), (3)) AS `i` + FROM (VALUES (CLNG(1)), (2), (3)) AS `i`(`Value`) WHERE `i`.`Value` = `t`.`Id`) """, // diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/GearsOfWarQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/GearsOfWarQueryLibRedTest.cs index d415f2b2..78413504 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/GearsOfWarQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/GearsOfWarQueryLibRedTest.cs @@ -9119,7 +9119,7 @@ LEFT JOIN ( ) AS `w0` WHERE `w0`.`row` <= COALESCE(( SELECT `n`.`Value` - FROM (SELECT @numbers1 AS `Value` UNION ALL VALUES (@numbers2), (@numbers3)) AS `n` + FROM (VALUES (@numbers1), (@numbers2), (@numbers3)) AS `n`(`Value`) ORDER BY `n`.`Value` OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY), 0) ) AS `w1` ON `g`.`FullName` = `w1`.`OwnerFullName` diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/Inheritance/TPCGearsOfWarQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/Inheritance/TPCGearsOfWarQueryLibRedTest.cs index b508678e..3683b47d 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/Inheritance/TPCGearsOfWarQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/Inheritance/TPCGearsOfWarQueryLibRedTest.cs @@ -12266,7 +12266,7 @@ LEFT JOIN ( ) AS `w0` WHERE `w0`.`row` <= COALESCE(( SELECT `n`.`Value` - FROM (SELECT @numbers1 AS `Value` UNION ALL VALUES (@numbers2), (@numbers3)) AS `n` + FROM (VALUES (@numbers1), (@numbers2), (@numbers3)) AS `n`(`Value`) ORDER BY `n`.`Value` OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY), 0) ) AS `w1` ON `u`.`FullName` = `w1`.`OwnerFullName` diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/Inheritance/TPTGearsOfWarQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/Inheritance/TPTGearsOfWarQueryLibRedTest.cs index 3fde116c..89215eac 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/Inheritance/TPTGearsOfWarQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/Inheritance/TPTGearsOfWarQueryLibRedTest.cs @@ -10373,7 +10373,7 @@ LEFT JOIN ( ) AS `w0` WHERE `w0`.`row` <= COALESCE(( SELECT `n`.`Value` - FROM (SELECT @numbers1 AS `Value` UNION ALL VALUES (@numbers2), (@numbers3)) AS `n` + FROM (VALUES (@numbers1), (@numbers2), (@numbers3)) AS `n`(`Value`) ORDER BY `n`.`Value` OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY), 0) ) AS `w1` ON `g`.`FullName` = `w1`.`OwnerFullName` diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindCompiledQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindCompiledQueryLibRedTest.cs index 27756b18..3305fd2b 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindCompiledQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindCompiledQueryLibRedTest.cs @@ -402,7 +402,7 @@ public override void Query_with_array_parameter() FROM `Customers` AS `c` WHERE `c`.`CustomerID` = ( SELECT `a`.`Value` - FROM (SELECT 0 AS `_ord`, @args1 AS `Value`) AS `a` + FROM (VALUES (0, @args1)) AS `a`(`_ord`, `Value`) ORDER BY `a`.`_ord` OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) """, @@ -414,7 +414,7 @@ ORDER BY `a`.`_ord` FROM `Customers` AS `c` WHERE `c`.`CustomerID` = ( SELECT `a`.`Value` - FROM (SELECT 0 AS `_ord`, @args1 AS `Value`) AS `a` + FROM (VALUES (0, @args1)) AS `a`(`_ord`, `Value`) ORDER BY `a`.`_ord` OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) """); @@ -432,7 +432,7 @@ public override async Task Query_with_array_parameter_async() FROM `Customers` AS `c` WHERE `c`.`CustomerID` = ( SELECT `a`.`Value` - FROM (SELECT 0 AS `_ord`, @args1 AS `Value`) AS `a` + FROM (VALUES (0, @args1)) AS `a`(`_ord`, `Value`) ORDER BY `a`.`_ord` OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) """, @@ -444,7 +444,7 @@ ORDER BY `a`.`_ord` FROM `Customers` AS `c` WHERE `c`.`CustomerID` = ( SELECT `a`.`Value` - FROM (SELECT 0 AS `_ord`, @args1 AS `Value`) AS `a` + FROM (VALUES (0, @args1)) AS `a`(`_ord`, `Value`) ORDER BY `a`.`_ord` OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) """); diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs index 06652565..e98130d4 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs @@ -1028,7 +1028,7 @@ public override async Task Join_local_collection_int_closure_is_cached_correctly SELECT `e`.`EmployeeID` FROM `Employees` AS `e` -INNER JOIN (SELECT @p1 AS `Value` UNION ALL VALUES (@p2)) AS `p` ON `e`.`EmployeeID` = `p`.`Value` +INNER JOIN (VALUES (@p1), (@p2)) AS `p`(`Value`) ON `e`.`EmployeeID` = `p`.`Value` """, // """ @@ -1036,7 +1036,7 @@ INNER JOIN (SELECT @p1 AS `Value` UNION ALL VALUES (@p2)) AS `p` ON `e`.`Employe SELECT `e`.`EmployeeID` FROM `Employees` AS `e` -INNER JOIN (SELECT @p1 AS `Value`) AS `p` ON `e`.`EmployeeID` = `p`.`Value` +INNER JOIN (VALUES (@p1)) AS `p`(`Value`) ON `e`.`EmployeeID` = `p`.`Value` """); } @@ -1051,7 +1051,7 @@ public override async Task Join_local_string_closure_is_cached_correctly(bool as SELECT `e`.`EmployeeID` FROM `Employees` AS `e` -INNER JOIN (SELECT @p1 AS `Value` UNION ALL VALUES (@p2)) AS `p` ON `e`.`EmployeeID` = ASCW(`p`.`Value`) +INNER JOIN (VALUES (@p1), (@p2)) AS `p`(`Value`) ON `e`.`EmployeeID` = ASCW(`p`.`Value`) """, // """ @@ -1059,7 +1059,7 @@ INNER JOIN (SELECT @p1 AS `Value` UNION ALL VALUES (@p2)) AS `p` ON `e`.`Employe SELECT `e`.`EmployeeID` FROM `Employees` AS `e` -INNER JOIN (SELECT @p1 AS `Value`) AS `p` ON `e`.`EmployeeID` = ASCW(`p`.`Value`) +INNER JOIN (VALUES (@p1)) AS `p`(`Value`) ON `e`.`EmployeeID` = ASCW(`p`.`Value`) """); } @@ -1086,7 +1086,7 @@ join id in ids on e.EmployeeID equals id SELECT `e`.`EmployeeID` FROM `Employees` AS `e` -INNER JOIN (SELECT @p1 AS `Value` UNION ALL VALUES (@p2)) AS `p` ON `e`.`EmployeeID` = `p`.`Value` +INNER JOIN (VALUES (@p1), (@p2)) AS `p`(`Value`) ON `e`.`EmployeeID` = `p`.`Value` """, // """ @@ -1094,7 +1094,7 @@ INNER JOIN (SELECT @p1 AS `Value` UNION ALL VALUES (@p2)) AS `p` ON `e`.`Employe SELECT `e`.`EmployeeID` FROM `Employees` AS `e` -INNER JOIN (SELECT @p1 AS `Value`) AS `p` ON `e`.`EmployeeID` = `p`.`Value` +INNER JOIN (VALUES (@p1)) AS `p`(`Value`) ON `e`.`EmployeeID` = `p`.`Value` """); } diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindMiscellaneousQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindMiscellaneousQueryLibRedTest.cs index 34126b18..202a4107 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindMiscellaneousQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindMiscellaneousQueryLibRedTest.cs @@ -7031,10 +7031,10 @@ public override async Task Subquery_with_navigation_inside_inline_collection(boo FROM `Customers` AS `c` WHERE ( SELECT COALESCE(SUM(`v`.`Value`), 0) - FROM (SELECT CLNG(100) AS `Value` UNION ALL VALUES (( + FROM (VALUES (CLNG(100)), (( SELECT COUNT(*) FROM `Orders` AS `o` - WHERE `c`.`CustomerID` = `o`.`CustomerID`))) AS `v`) > 101 + WHERE `c`.`CustomerID` = `o`.`CustomerID`))) AS `v`(`Value`)) > 101 """); } diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindSelectQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindSelectQueryLibRedTest.cs index b55eeb89..85692f4f 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindSelectQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindSelectQueryLibRedTest.cs @@ -1218,7 +1218,7 @@ public override async Task SelectMany_over_inline_array_projecting_range_variabl """ SELECT `v`.`Value` AS `k`, `c`.`CustomerID`, `c`.`Address`, `c`.`City`, `c`.`CompanyName`, `c`.`ContactName`, `c`.`ContactTitle`, `c`.`Country`, `c`.`Fax`, `c`.`Phone`, `c`.`PostalCode`, `c`.`Region` FROM `Customers` AS `c` -CROSS APPLY (SELECT ('a' & '') AS `Value` UNION ALL VALUES ('b')) AS `v` +CROSS APPLY (VALUES (('a' & '')), ('b')) AS `v`(`Value`) WHERE `c`.`CustomerID` = 'ALFKI' """); } diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/PrecompiledQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/PrecompiledQueryLibRedTest.cs index 0c250fb1..6f77464c 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/PrecompiledQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/PrecompiledQueryLibRedTest.cs @@ -1755,7 +1755,7 @@ public override async Task ValuesExpression() FROM `Blogs` AS `b` WHERE ( SELECT COUNT(*) - FROM (SELECT CLNG(7) AS `Value` UNION ALL VALUES (`b`.`Id`)) AS `v` + FROM (VALUES (CLNG(7)), (`b`.`Id`)) AS `v`(`Value`) WHERE `v`.`Value` > 8) = 2 """); } diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/PrimitiveCollectionsQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/PrimitiveCollectionsQueryLibRedTest.cs index 9c984cf4..fe2020a6 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/PrimitiveCollectionsQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/PrimitiveCollectionsQueryLibRedTest.cs @@ -85,7 +85,7 @@ public override async Task Inline_collection_Count_with_one_value() FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT COUNT(*) - FROM (SELECT CLNG(2) AS `Value`) AS `v` + FROM (VALUES (CLNG(2))) AS `v`(`Value`) WHERE `v`.`Value` > `p`.`Id`) = 1 """); } @@ -100,7 +100,7 @@ public override async Task Inline_collection_Count_with_two_values() FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT COUNT(*) - FROM (SELECT CLNG(2) AS `Value` UNION ALL VALUES (999)) AS `v` + FROM (VALUES (CLNG(2)), (999)) AS `v`(`Value`) WHERE `v`.`Value` > `p`.`Id`) = 1 """); } @@ -115,7 +115,7 @@ public override async Task Inline_collection_Count_with_three_values() FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT COUNT(*) - FROM (SELECT CLNG(2) AS `Value` UNION ALL VALUES (999), (1000)) AS `v` + FROM (VALUES (CLNG(2)), (999), (1000)) AS `v`(`Value`) WHERE `v`.`Value` > `p`.`Id`) = 2 """); } @@ -431,7 +431,7 @@ public override async Task Inline_collection_with_single_parameter_element_Count FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT COUNT(*) - FROM (SELECT @i AS `Value`) AS `v` + FROM (VALUES (@i)) AS `v`(`Value`) WHERE `v`.`Value` > `p`.`Id`) = 1 """); } @@ -497,7 +497,7 @@ public override async Task Inline_collection_in_query_filter() FROM `TestEntity` AS `t` WHERE ( SELECT COUNT(*) - FROM (SELECT CLNG(1) AS `Value` UNION ALL VALUES (2), (3)) AS `v` + FROM (VALUES (CLNG(1)), (2), (3)) AS `v`(`Value`) WHERE `v`.`Value` > `t`.`Id`) = 1 """); } @@ -510,7 +510,7 @@ public override async Task Inline_collection_SelectMany_with_unreferenced_collec """ SELECT `p`.`Id`, `p`.`Bool`, `p`.`Bools`, `p`.`DateTime`, `p`.`DateTimes`, `p`.`Enum`, `p`.`Enums`, `p`.`Int`, `p`.`Ints`, `p`.`NullableInt`, `p`.`NullableInts`, `p`.`NullableString`, `p`.`NullableStrings`, `p`.`NullableWrappedId`, `p`.`NullableWrappedIdWithNullableComparer`, `p`.`String`, `p`.`Strings`, `p`.`WrappedId` FROM `PrimitiveCollectionsEntity` AS `p` -CROSS APPLY (SELECT ('a' & '') AS `Value` UNION ALL VALUES ('b')) AS `v` +CROSS APPLY (VALUES (('a' & '')), ('b')) AS `v`(`Value`) """); } @@ -527,7 +527,7 @@ public override async Task Parameter_collection_Count() FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT COUNT(*) - FROM (SELECT @ids1 AS `Value` UNION ALL VALUES (@ids2)) AS `i` + FROM (VALUES (@ids1), (@ids2)) AS `i`(`Value`) WHERE `i`.`Value` > `p`.`Id`) = 1 """); } @@ -950,7 +950,7 @@ public override async Task Parameter_collection_Where_with_EF_Constant_Where_Any FROM `PrimitiveCollectionsEntity` AS `p` WHERE EXISTS ( SELECT 1 - FROM (SELECT CLNG(2) AS `Value` UNION ALL VALUES (999), (1000)) AS `i` + FROM (VALUES (CLNG(2)), (999), (1000)) AS `i`(`Value`) WHERE `i`.`Value` > 0) """); } @@ -965,7 +965,7 @@ public override async Task Parameter_collection_Count_with_column_predicate_with FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT COUNT(*) - FROM (SELECT CLNG(2) AS `Value` UNION ALL VALUES (999), (1000)) AS `i` + FROM (VALUES (CLNG(2)), (999), (1000)) AS `i`(`Value`) WHERE `i`.`Value` > `p`.`Id`) = 2 """); } @@ -1064,7 +1064,7 @@ public override async Task Parameter_collection_Count_with_column_predicate_with FROM `TestEntity` AS `t` WHERE ( SELECT COUNT(*) - FROM (SELECT CLNG(2) AS `Value` UNION ALL VALUES (999)) AS `i` + FROM (VALUES (CLNG(2)), (999)) AS `i`(`Value`) WHERE `i`.`Value` > `t`.`Id`) = 1 """); break; @@ -1098,7 +1098,7 @@ FROM OPENJSON(@ids) WITH ([value] int '$') AS [i] FROM `TestEntity` AS `t` WHERE ( SELECT COUNT(*) - FROM (SELECT @ids1 AS `Value` UNION ALL VALUES (@ids2)) AS `i` + FROM (VALUES (@ids1), (@ids2)) AS `i`(`Value`) WHERE `i`.`Value` > `t`.`Id`) = 1 """); break; @@ -1170,7 +1170,7 @@ public override async Task Parameter_collection_Count_with_column_predicate_with FROM `TestEntity` AS `t` WHERE ( SELECT COUNT(*) - FROM (SELECT CLNG(2) AS `Value` UNION ALL VALUES (999)) AS `i` + FROM (VALUES (CLNG(2)), (999)) AS `i`(`Value`) WHERE `i`.`Value` > `t`.`Id`) = 1 """); } @@ -1236,7 +1236,7 @@ public override async Task Parameter_collection_Count_with_column_predicate_with FROM `TestEntity` AS `t` WHERE ( SELECT COUNT(*) - FROM (SELECT @ids1 AS `Value` UNION ALL VALUES (@ids2)) AS `i` + FROM (VALUES (@ids1), (@ids2)) AS `i`(`Value`) WHERE `i`.`Value` > `t`.`Id`) = 1 """); } @@ -1303,7 +1303,7 @@ public override async Task Parameter_collection_of_enum_Cast_from_different_enum FROM `TestEntity38008` AS `t` WHERE EXISTS ( SELECT 1 - FROM (SELECT 2 AS `Value`) AS `f` + FROM (VALUES (2)) AS `f`(`Value`) WHERE `f`.`Value` = `t`.`Status`) """); break; @@ -1336,7 +1336,7 @@ FROM OPENJSON(@filter) WITH ([value] int '$') AS [f] FROM `TestEntity38008` AS `t` WHERE EXISTS ( SELECT 1 - FROM (SELECT @filter1 AS `Value`) AS `f` + FROM (VALUES (@filter1)) AS `f`(`Value`) WHERE `f`.`Value` = `t`.`Status`) """); break; @@ -1505,7 +1505,7 @@ public override async Task Constant_with_inferred_value_converter() FROM `TestEntity` AS `t` WHERE ( SELECT COUNT(*) - FROM (SELECT 1 AS `Value` UNION ALL VALUES (8)) AS `v` + FROM (VALUES (1), (8)) AS `v`(`Value`) WHERE `v`.`Value` = `t`.`PropertyWithValueConverter`) = 1 """); } @@ -1613,7 +1613,7 @@ public override async Task Inline_collection_index_Column() FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT `v`.`Value` - FROM (SELECT 0 AS `_ord`, CLNG(1) AS `Value` UNION ALL VALUES (1, 2), (2, 3)) AS `v` + FROM (VALUES (0, CLNG(1)), (1, 2), (2, 3)) AS `v`(`_ord`, `Value`) ORDER BY `v`.`_ord` OFFSET `p`.`Int` ROWS FETCH NEXT 1 ROWS ONLY) = 1 """); @@ -1629,7 +1629,7 @@ public override async Task Inline_collection_index_Column_with_EF_Constant() FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT `i`.`Value` - FROM (SELECT 0 AS `_ord`, CLNG(1) AS `Value` UNION ALL VALUES (1, 2), (2, 3)) AS `i` + FROM (VALUES (0, CLNG(1)), (1, 2), (2, 3)) AS `i`(`_ord`, `Value`) ORDER BY `i`.`_ord` OFFSET `p`.`Int` ROWS FETCH NEXT 1 ROWS ONLY) = 1 """); @@ -1645,7 +1645,7 @@ public override async Task Inline_collection_value_index_Column() FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT `v`.`Value` - FROM (SELECT 0 AS `_ord`, CLNG(1) AS `Value` UNION ALL VALUES (1, `p`.`Int`), (2, 3)) AS `v` + FROM (VALUES (0, CLNG(1)), (1, `p`.`Int`), (2, 3)) AS `v`(`_ord`, `Value`) ORDER BY `v`.`_ord` OFFSET `p`.`Int` ROWS FETCH NEXT 1 ROWS ONLY) = 1 """); @@ -1661,7 +1661,7 @@ public override async Task Inline_collection_List_value_index_Column() FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT `v`.`Value` - FROM (SELECT 0 AS `_ord`, CLNG(1) AS `Value` UNION ALL VALUES (1, `p`.`Int`), (2, 3)) AS `v` + FROM (VALUES (0, CLNG(1)), (1, `p`.`Int`), (2, 3)) AS `v`(`_ord`, `Value`) ORDER BY `v`.`_ord` OFFSET `p`.`Int` ROWS FETCH NEXT 1 ROWS ONLY) = 1 """); @@ -1846,7 +1846,7 @@ SELECT COUNT(*) SELECT COUNT(*) FROM ( SELECT `i`.`Value` AS `Value0` - FROM (SELECT 0 AS `_ord`, @ints1 AS `Value` UNION ALL VALUES (1, @ints2)) AS `i` + FROM (VALUES (0, @ints1), (1, @ints2)) AS `i`(`_ord`, `Value`) ORDER BY `i`.`_ord` OFFSET 1 ROWS ) AS `i0` @@ -2005,7 +2005,7 @@ public override async Task Project_inline_collection_with_Union() FROM `PrimitiveCollectionsEntity` AS `p` OUTER APPLY ( SELECT `v`.`Value` - FROM (SELECT `p`.`String` AS `Value`) AS `v` + FROM (VALUES (`p`.`String`)) AS `v`(`Value`) UNION SELECT `p0`.`String` AS `Value` FROM `PrimitiveCollectionsEntity` AS `p0` @@ -2241,7 +2241,7 @@ public override async Task Values_of_enum_casted_to_underlying_value() FROM `PrimitiveCollectionsEntity` AS `p` WHERE ( SELECT COUNT(*) - FROM (SELECT CLNG(0) AS `Value` UNION ALL VALUES (1), (2), (3)) AS `v` + FROM (VALUES (CLNG(0)), (1), (2), (3)) AS `v`(`Value`) WHERE `v`.`Value` = `p`.`Int`) > 0 """); } diff --git a/test/LibRed.Engine.Tests/DerivedColumnListTests.cs b/test/LibRed.Engine.Tests/DerivedColumnListTests.cs new file mode 100644 index 00000000..c77c12cc --- /dev/null +++ b/test/LibRed.Engine.Tests/DerivedColumnListTests.cs @@ -0,0 +1,71 @@ +using System.Globalization; +using LibRed.Sql.Parsing; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// A derived table's column list, the standard's (query) AS t(a, b): it names the table's columns in order, a +/// table value constructor's included, as SQL Server and PostgreSQL take it and EF Core emits it for an inline +/// collection. ACE has no such syntax, so this is a LibRed extension. +/// +public class DerivedColumnListTests(DerivedColumnListTests.Database database) + : TempDatabaseTest, IClassFixture +{ + private static readonly string[] Setup = + [ + "CREATE TABLE T (Id LONG, K LONG)", + "INSERT INTO T VALUES (1, 10)", + "INSERT INTO T VALUES (2, 20)", + "INSERT INTO T VALUES (7, 70)", + ]; + + public sealed class Database() : SharedDatabase("derived-columns-", Setup); + + private string Run(string sql) + { + var result = database.Engine.ExecuteQuery(sql); + return $"[{string.Join(",", result.ColumnNames)}] " + string.Join(" | ", result.Rows.Select(row => + string.Join(",", row.Select(v => Convert.ToString(v, CultureInfo.InvariantCulture))))); + } + + [Theory] + [InlineData("SELECT * FROM (VALUES (1, 'a'), (2, 'b')) AS v(n, s)", "[n,s] 1,a | 2,b")] + [InlineData("SELECT v.s FROM (VALUES (1, 'a'), (2, 'b')) AS v(n, s) WHERE v.n = 2", "[s] b")] + [InlineData("SELECT x FROM (VALUES (1), (2)) v(x) ORDER BY x DESC", "[x] 2 | 1")] + [InlineData("SELECT t.x FROM (SELECT Id, K FROM T) AS t(x, y) WHERE t.y = 70", "[x] 7")] + [InlineData("SELECT * FROM (SELECT Id AS a, K AS b FROM T WHERE Id = 1) AS t(c, d)", "[c,d] 1,10")] + [InlineData("SELECT * FROM (SELECT Id FROM T WHERE Id = 1 UNION ALL VALUES (5)) AS u(n)", "[n] 1 | 5")] + [InlineData("SELECT a.Id, v.label FROM T AS a INNER JOIN (VALUES (1, 'one'), (7, 'seven')) AS v(id, label) ON a.Id = v.id ORDER BY a.Id", + "[Id,label] 1,one | 7,seven")] + public void The_list_names_the_columns_in_order(string sql, string expected) => + Assert.Equal(expected, Run(sql)); + + [Fact] + public void The_old_names_are_gone() => + Assert.ThrowsAny(() => Run("SELECT t.Id FROM (SELECT Id FROM T) AS t(x)")); + + [Fact] + public void The_list_must_name_every_column() => + Assert.Throws(() => Run("SELECT * FROM (VALUES (1)) AS v(a, b)")); + + [Fact] + public void A_name_may_appear_once() => + Assert.Throws(() => Run("SELECT * FROM (VALUES (1, 2)) AS v(a, A)")); + + [Fact] + public void An_update_can_read_through_one() + { + QueryEngine engine = SharedDatabase.Fresh("derived-columns-update-", Setup); + engine.ExecuteNonQuery("UPDATE T AS a INNER JOIN (VALUES (1, 11), (7, 77)) AS v(id, k) ON a.Id = v.id SET a.K = v.k"); + Assert.Equal([11, 20, 77], engine.ExecuteQuery("SELECT K FROM T ORDER BY Id").Rows.Select(row => (int)row[0]!)); + } + + [Fact] + public void A_view_cannot_store_one() + { + QueryEngine engine = SharedDatabase.Fresh("derived-columns-view-", Setup); + Assert.Throws(() => + engine.ExecuteNonQuery("CREATE VIEW V AS SELECT t.x FROM (SELECT Id FROM T) AS t(x)")); + } +} From b7e32149688130aa9ae568cff6d5b337a90be693 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Tue, 29 Sep 2026 18:41:27 +0800 Subject: [PATCH 71/88] Test the lock manager's case rule per platform The registry test acquired C:\dir\db.accdb and C:\dir\DB.accdb and expected one manager. That held while the file key folded case everywhere, but the key now folds it only where file names are case-insensitive, Windows and macOS: on Linux those are two files, and two managers is right. So the test failed on every Linux leg. The refcount check now uses one path, and the case rule has a test of its own that expects one manager on Windows and macOS and two on Linux. Co-Authored-By: Claude Opus 5.5 --- test/LibRed.Core.Tests/LockManagerTests.cs | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/LibRed.Core.Tests/LockManagerTests.cs b/test/LibRed.Core.Tests/LockManagerTests.cs index dc54f158..9f48fcb4 100644 --- a/test/LibRed.Core.Tests/LockManagerTests.cs +++ b/test/LibRed.Core.Tests/LockManagerTests.cs @@ -66,7 +66,7 @@ public void Locks_on_different_pages_do_not_block_each_other() public void Acquire_returns_one_shared_manager_per_path_and_frees_it_on_last_release() { MonitorLockManager a = MonitorLockManager.Acquire(@"C:\dir\db.accdb"); - MonitorLockManager b = MonitorLockManager.Acquire(@"C:\dir\DB.accdb"); // same file (case-insensitive) + MonitorLockManager b = MonitorLockManager.Acquire(@"C:\dir\db.accdb"); Assert.Same(a, b); // Two acquisitions → two releases; a fresh acquire after that is a new manager (the old was disposed). @@ -76,6 +76,27 @@ public void Acquire_returns_one_shared_manager_per_path_and_frees_it_on_last_rel MonitorLockManager.Release(@"C:\dir\db.accdb"); } + // Paths differing only in case name one file where file names are case-insensitive (Windows, macOS) and two on + // Linux, and the manager follows the file (FileIdentity): sharing one there would pool two files' pages. + [Fact] + public void Paths_differing_in_case_share_a_manager_only_where_file_names_ignore_case() + { + MonitorLockManager a = MonitorLockManager.Acquire(@"C:\dir\case.accdb"); + MonitorLockManager b = MonitorLockManager.Acquire(@"C:\dir\CASE.accdb"); + try + { + if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()) + Assert.Same(a, b); + else + Assert.NotSame(a, b); + } + finally + { + MonitorLockManager.Release(@"C:\dir\case.accdb"); + MonitorLockManager.Release(@"C:\dir\CASE.accdb"); + } + } + [Fact] public void PageChannel_reads_and_writes_correctly_under_a_lock_manager() { From 9e9293c83393110c6a1386e91bd931662d039fcc Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Tue, 29 Sep 2026 23:21:33 +0800 Subject: [PATCH 72/88] Add a probe of how ACE chooses the pages it allocates The whole-file parity diff showed ACE placing the pages of a large INSERT ... SELECT in 8-page groups where LibRed takes the lowest free page. This probe measures why, on a thousand-row insert and variations of it: row by row, and with the file read while ACE's session is still open, where its reservations are visible. ACE reserves extents in the global free map, four aligned 8-page groups at a time, holds them for the session and frees the rest at close; outside them it keeps data and index pages to separate groups, and grows the file at the end or the next 8-page boundary depending on what the session has used. What triggers an extent is not fully pinned. Every test is explicit: the reservation samples take up to twenty minutes. Co-Authored-By: Claude Opus 5.5 --- .../PageGroupProbeTest.cs | 781 ++++++++++++++++++ 1 file changed, 781 insertions(+) create mode 100644 test/LibRed.Engine.AccessTests/PageGroupProbeTest.cs diff --git a/test/LibRed.Engine.AccessTests/PageGroupProbeTest.cs b/test/LibRed.Engine.AccessTests/PageGroupProbeTest.cs new file mode 100644 index 00000000..a8cd4c6a --- /dev/null +++ b/test/LibRed.Engine.AccessTests/PageGroupProbeTest.cs @@ -0,0 +1,781 @@ +using System.Buffers.Binary; +using System.Data.OleDb; +using System.Globalization; +using System.Reflection; +using System.Text; +using LibRed; +using LibRed.Pages; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// Probe: how ACE chooses the page it allocates — the session extents it reserves in the global free map, and where +/// single pages go outside them — measured on a thousand-row INSERT … SELECT and variations of it. Reports; asserts +/// nothing. Every test is explicit: several take minutes, the reservation samples up to twenty. +/// +/// +/// A reservation is visible only while ACE's session is open — at close the unused rest is set free again — so the +/// *_reservations tests copy the file with the session still open, after waiting out ACE's flush timeout. +/// +[Collection(AceCollection.Name)] +public class PageGroupProbeTest(ITestOutputHelper output) +{ + private const int PageSize = 4096; + + [Theory(Explicit = true)] + [InlineData(0)] + [InlineData(40)] + public void Bulk_insert_page_map(int paddingRows) + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + string origin = TemporaryDatabase.CreatePath("pagegroup-origin-"); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + object db = Invoke(workspace, "CreateDatabase", origin, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; + Invoke(db, "Close"); + + var setup = new List + { + "CREATE TABLE Padding (Id LONG, Filler TEXT(255))", + "CREATE TABLE Digits (D BYTE)", + }; + for (int i = 0; i < paddingRows; i++) + setup.Add($"INSERT INTO Padding VALUES ({i}, String(255, 'x'))"); + for (int d = 0; d <= 9; d++) setup.Add($"INSERT INTO Digits (D) VALUES ({d})"); + setup.Add("CREATE TABLE Bulk (Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), Payload TEXT(255), " + + "Code BINARY(8), Amount DECIMAL(10,2))"); + setup.Add("CREATE INDEX ixBulkLabel ON Bulk (Label)"); + const string bulk = + "INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT a.D * 100 + b.D * 10 + c.D, a.D, " + + "'Bulk label number ' & (a.D * 100 + b.D * 10 + c.D) & ' ' & String(30, 'L'), String(150, 'p'), " + + "a.D + b.D * 0.5 FROM Digits AS a, Digits AS b, Digits AS c ORDER BY 1"; + + string ace = TemporaryDatabase.CopyPath(origin, "pagegroup-ace-"); + string libred = TemporaryDatabase.CopyPath(origin, "pagegroup-libred-"); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(origin)) + foreach (string statement in setup) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = statement; + command.ExecuteNonQuery(); + } + File.Copy(origin, ace, overwrite: true); + File.Copy(origin, libred, overwrite: true); + + using (OleDbConnection connection = AceTestDatabase.Open(ace)) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = bulk; + command.ExecuteNonQuery(); + } + using (var database = JetDatabase.Open(libred, readOnly: false)) + new QueryEngine(database).ExecuteNonQuery(bulk); + + var report = new StringBuilder(); + report.AppendLine(CultureInfo.InvariantCulture, $"before: {new FileInfo(origin).Length / PageSize} pages"); + Describe(report, "ACE", ace); + Describe(report, "LibRed", libred); + output.WriteLine(report.ToString()); + } + finally + { + foreach (string path in new[] { origin, ace, libred }) TemporaryDatabase.Delete(path); + } + } + + /// The full table's insert, cut at a few row counts, with the file read while ACE's session is still + /// open (after its flush) and again after close: which pages the free and released maps hold at each point. + [Fact(Explicit = true)] + public void Bulk_insert_maps_while_the_session_is_open() + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + string origin = TemporaryDatabase.CreatePath("pagegroup-origin-"); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + object db = Invoke(workspace, "CreateDatabase", origin, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; + Invoke(db, "Close"); + var setup = new List { "CREATE TABLE Digits (D BYTE)" }; + for (int d = 0; d <= 9; d++) setup.Add($"INSERT INTO Digits (D) VALUES ({d})"); + setup.Add("CREATE TABLE Bulk (Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), Payload TEXT(255), " + + "Code BINARY(8), Amount DECIMAL(10,2))"); + setup.Add("CREATE INDEX ixBulkLabel ON Bulk (Label)"); + var report = new StringBuilder(); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(origin)) + foreach (string statement in setup) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = statement; + command.ExecuteNonQuery(); + } + + foreach (int rows in new[] { 240, 480, 560, 1000 }) + { + string ace = TemporaryDatabase.CopyPath(origin, "pagegroup-open-"); + string open = TemporaryDatabase.CreatePath("pagegroup-opencopy-"); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(ace)) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = + "INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT a.D * 100 + b.D * 10 + c.D, a.D, " + + "'Bulk label number ' & (a.D * 100 + b.D * 10 + c.D) & ' ' & String(30, 'L'), String(150, 'p'), " + + $"a.D + b.D * 0.5 FROM Digits AS a, Digits AS b, Digits AS c WHERE a.D * 100 + b.D * 10 + c.D < {rows} ORDER BY 1"; + command.ExecuteNonQuery(); + Thread.Sleep(3000); // past ACE's flush timeout, so the committed pages are on disk + using var source = new FileStream(ace, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var target = File.Create(open); + source.CopyTo(target); + } + report.AppendLine(CultureInfo.InvariantCulture, $"{rows} rows, session open: {Maps(open)}"); + report.AppendLine(CultureInfo.InvariantCulture, $"{rows} rows, after close: {Maps(ace)}"); + } + finally + { + TemporaryDatabase.Delete(ace); + TemporaryDatabase.Delete(open); + } + } + output.WriteLine(report.ToString()); + } + finally { TemporaryDatabase.Delete(origin); } + } + + /// The full table's insert at sampled row counts — row by row across the known transitions — each read + /// with ACE's session still open: the pages newly in use since the last sample, and the pages ACE holds reserved + /// (neither in use nor free), with the free pages inside the file. + [Fact(Explicit = true)] + public void Bulk_insert_reservations() => + Reservations(FullColumns, LabelIndex, 0, 1000, + [(88, 102), (225, 245), (455, 482), (535, 610), (630, 690), (720, 780), (810, 820), (855, 925), (990, 1000)]); + + /// The same, with the first 200 rows inserted by the earlier session that built the table. + [Fact(Explicit = true)] + public void Bulk_insert_reservations_after_an_earlier_session() => + Reservations(FullColumns, LabelIndex, 200, 1000, [(201, 250), (455, 485), (540, 610), (630, 690), (720, 760)]); + + /// A heap table with no index, 200 rows from the earlier session: whether a data page that must grow + /// the file takes an extent when it is the second session's first growth. + [Fact(Explicit = true)] + public void Heap_insert_reservations_after_an_earlier_session() => + Reservations("Id LONG, Grp LONG, Label TEXT(60), Payload TEXT(255), Code BINARY(8), Amount DECIMAL(10,2)", "", + 200, 300, [(200, 240)]); + + /// An empty heap table, filled from a later session: whether its first data page takes an extent. + [Fact(Explicit = true)] + public void Heap_first_page_reservations() => + Reservations("Id LONG, Grp LONG, Label TEXT(60), Payload TEXT(255), Code BINARY(8), Amount DECIMAL(10,2)", "", + 0, 50, [(1, 20)]); + + /// A primary-keyed table, 600 rows from the earlier session, so the second session's first allocation + /// is the key's root split (at the 603rd row) while the last data page still has room. + [Fact(Explicit = true)] + public void Root_split_first_reservations() => + Reservations("Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), Payload TEXT(255), Code BINARY(8), Amount DECIMAL(10,2)", "", + 600, 625, [(600, 612)]); + + /// Whether growth lands at the end of the file or the next 8-page boundary depends on this session having + /// used the file's last group. Setup leaves Bulk (primary key only) at 600 rows, ending mid-group, and a heap + /// table Side with full pages; the second session first grows Side, then brings Bulk to its root split. + [Fact(Explicit = true)] + public void Growth_alignment_follows_the_session() + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + string origin = TemporaryDatabase.CreatePath("pagegroup-origin-"); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + object db = Invoke(workspace, "CreateDatabase", origin, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; + Invoke(db, "Close"); + + const string select = + "INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT a.D * 100 + b.D * 10 + c.D, a.D, " + + "'Bulk label number ' & (a.D * 100 + b.D * 10 + c.D) & ' ' & String(30, 'L'), String(150, 'p'), " + + "a.D + b.D * 0.5 FROM Digits AS a, Digits AS b, Digits AS c WHERE a.D * 100 + b.D * 10 + c.D "; + var setup = new List { "CREATE TABLE Digits (D BYTE)" }; + for (int d = 0; d <= 9; d++) setup.Add($"INSERT INTO Digits (D) VALUES ({d})"); + setup.Add("CREATE TABLE Side (Id LONG, Filler TEXT(255))"); + for (int i = 0; i < 28; i++) setup.Add($"INSERT INTO Side VALUES ({i}, String(255, 's'))"); + setup.Add("CREATE TABLE Bulk (Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), Payload TEXT(255), " + + "Code BINARY(8), Amount DECIMAL(10,2))"); + setup.Add($"{select}< 600 ORDER BY 1"); + + string[] side = [.. Enumerable.Range(100, 20).Select(i => $"INSERT INTO Side VALUES ({i}, String(255, 't'))")]; + var runs = new List<(string Label, string[] Sql)> + { + ("nothing", []), + ("Side grown", side), + ("then Bulk split", [.. side, $"{select}>= 600 AND a.D * 100 + b.D * 10 + c.D < 603 ORDER BY 1"]), + }; + + var report = new StringBuilder(); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(origin)) + foreach (string statement in setup) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = statement; + command.ExecuteNonQuery(); + } + report.AppendLine(CultureInfo.InvariantCulture, $"after setup: {new FileInfo(origin).Length / PageSize} pages"); + + Dictionary previous = Kinds(origin); + foreach ((string label, string[] sql) in runs) + { + string ace = TemporaryDatabase.CopyPath(origin, "pagegroup-grow-"); + string open = TemporaryDatabase.CreatePath("pagegroup-growcopy-"); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(ace)) + { + foreach (string statement in sql) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = statement; + command.ExecuteNonQuery(); + } + Thread.Sleep(3000); // past ACE's flush timeout, so the committed pages are on disk + using var source = new FileStream(ace, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var target = File.Create(open); + source.CopyTo(target); + } + + Dictionary kinds = Kinds(open); + int pages = (int)(new FileInfo(open).Length / PageSize); + HashSet free; + string owners; + using (var database = JetDatabase.Open(open, readOnly: true)) + { + free = GlobalFree(database.OpenTable("MSysObjects"), 512); + owners = $"Side tdef {database.OpenTable("Side").Definition.DefinitionPage}, " + + $"Bulk tdef {database.OpenTable("Bulk").Definition.DefinitionPage}"; + } + string reserved = Ranges(Enumerable.Range(1, 511) + .Where(p => !free.Contains(p) && (p >= pages || !kinds.ContainsKey(p)))); + var changes = kinds.OrderBy(p => p.Key) + .Where(p => !previous.TryGetValue(p.Key, out string? before) || before != p.Value) + .Select(p => $"+{p.Key}{p.Value}"); + report.AppendLine(CultureInfo.InvariantCulture, + $"{label,-16} ({pages}; {owners}): {string.Join(" ", changes)}\n{"",18}reserved [{reserved}] " + + $"free in file [{Ranges(free.Where(p => p < pages))}]"); + } + finally + { + TemporaryDatabase.Delete(ace); + TemporaryDatabase.Delete(open); + } + } + output.WriteLine(report.ToString()); + } + finally { TemporaryDatabase.Delete(origin); } + } + + private const string FullColumns = + "Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), Payload TEXT(255), Code BINARY(8), Amount DECIMAL(10,2)"; + + private const string LabelIndex = "CREATE INDEX ixBulkLabel ON Bulk (Label)"; + + private void Reservations(string columns, string index, int preloaded, int last, (int From, int To)[] windows) + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + string origin = TemporaryDatabase.CreatePath("pagegroup-origin-"); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + object db = Invoke(workspace, "CreateDatabase", origin, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; + Invoke(db, "Close"); + var setup = new List { "CREATE TABLE Digits (D BYTE)" }; + for (int d = 0; d <= 9; d++) setup.Add($"INSERT INTO Digits (D) VALUES ({d})"); + setup.Add($"CREATE TABLE Bulk ({columns})"); + if (index.Length > 0) setup.Add(index); + const string select = + "INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT a.D * 100 + b.D * 10 + c.D, a.D, " + + "'Bulk label number ' & (a.D * 100 + b.D * 10 + c.D) & ' ' & String(30, 'L'), String(150, 'p'), " + + "a.D + b.D * 0.5 FROM Digits AS a, Digits AS b, Digits AS c WHERE a.D * 100 + b.D * 10 + c.D "; + if (preloaded > 0) setup.Add($"{select}< {preloaded} ORDER BY 1"); + + var samples = new SortedSet(); + samples.Add(preloaded); // the session open, nothing inserted: the state it starts from + for (int rows = preloaded + 25; rows <= last; rows += 25) samples.Add(rows); + foreach ((int from, int to) in windows) + for (int rows = from; rows <= to; rows++) samples.Add(rows); + + var report = new StringBuilder(); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(origin)) + foreach (string statement in setup) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = statement; + command.ExecuteNonQuery(); + } + + Dictionary previous = []; + string previousReserved = ""; + foreach (int rows in samples) + { + string ace = TemporaryDatabase.CopyPath(origin, "pagegroup-res-"); + string open = TemporaryDatabase.CreatePath("pagegroup-rescopy-"); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(ace)) + { + using OleDbCommand command = connection.CreateCommand(); + // Not BETWEEN: Jet swaps reversed bounds, so the empty range of the first sample would not be empty. + command.CommandText = $"{select}>= {preloaded} AND a.D * 100 + b.D * 10 + c.D < {rows} ORDER BY 1"; + command.ExecuteNonQuery(); + Thread.Sleep(3000); // past ACE's flush timeout, so the committed pages are on disk + using var source = new FileStream(ace, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var target = File.Create(open); + source.CopyTo(target); + } + + Dictionary used = Used(open); + byte[] file = File.ReadAllBytes(open); + int pages = file.Length / PageSize; + HashSet free; + using (var database = JetDatabase.Open(open, readOnly: true)) + free = GlobalFree(database.OpenTable("Bulk"), 512); + int first = used.Keys.Min(); + string reserved = Ranges(Enumerable.Range(first, 512 - first) + .Where(p => !free.Contains(p) && (p >= pages || file[p * PageSize] == 0 && file[p * PageSize + 1] == 0))); + string freeInFile = Ranges(free.Where(p => p < pages)); + + var changes = new List(); + foreach (var (page, kind) in used.OrderBy(p => p.Key)) + if (!previous.TryGetValue(page, out string? before) || before != kind) changes.Add($"+{page}{kind}"); + if (changes.Count > 0 || reserved != previousReserved) + report.AppendLine(CultureInfo.InvariantCulture, + $"{rows,5} ({pages}): {string.Join(" ", changes),-28} reserved [{reserved}] free in file [{freeInFile}]"); + previous = used; + previousReserved = reserved; + } + finally + { + TemporaryDatabase.Delete(ace); + TemporaryDatabase.Delete(open); + } + } + output.WriteLine(report.ToString()); + } + finally { TemporaryDatabase.Delete(origin); } + } + + /// The full table's 1,000-row insert under different Max Locks Per File limits: the page map each + /// leaves, to see whether intermediate commits are what start a new extent. + [Fact(Explicit = true)] + public void Bulk_insert_under_lock_limits() + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + string origin = TemporaryDatabase.CreatePath("pagegroup-origin-"); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + object db = Invoke(workspace, "CreateDatabase", origin, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; + Invoke(db, "Close"); + var setup = new List { "CREATE TABLE Digits (D BYTE)" }; + for (int d = 0; d <= 9; d++) setup.Add($"INSERT INTO Digits (D) VALUES ({d})"); + setup.Add("CREATE TABLE Bulk (Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), Payload TEXT(255), " + + "Code BINARY(8), Amount DECIMAL(10,2))"); + setup.Add("CREATE INDEX ixBulkLabel ON Bulk (Label)"); + var report = new StringBuilder(); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(origin)) + foreach (string statement in setup) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = statement; + command.ExecuteNonQuery(); + } + + report.AppendLine("(OLE DB default, for comparison, is the 1000-row line of Bulk_insert_snapshots)"); + foreach (int? limit in new int?[] { null, 1_000_000, 4000, 1000 }) + { + string ace = TemporaryDatabase.CopyPath(origin, "pagegroup-locks-"); + try + { + object dao = AceTestDatabase.CreateDaoEngine()!; + if (limit is not null) Invoke(dao, "SetOption", 62, limit.Value); // dbMaxLocksPerFile + object session = Invoke(dao, "CreateWorkspace", "", "admin", "", 2)!; + object database = Invoke(session, "OpenDatabase", ace)!; + Invoke(database, "Execute", + "INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT a.D * 100 + b.D * 10 + c.D, a.D, " + + "'Bulk label number ' & (a.D * 100 + b.D * 10 + c.D) & ' ' & String(30, 'L'), String(150, 'p'), " + + "a.D + b.D * 0.5 FROM Digits AS a, Digits AS b, Digits AS c ORDER BY 1"); + Invoke(database, "Close"); + Invoke(session, "Close"); + report.AppendLine(CultureInfo.InvariantCulture, $"limit {limit?.ToString(CultureInfo.InvariantCulture) ?? "default"}: {Runs(ace)}"); + } + finally { TemporaryDatabase.Delete(ace); } + } + output.WriteLine(report.ToString()); + } + finally { TemporaryDatabase.Delete(origin); } + } + + /// The setup session itself — DDL, the Digits inserts, then the 200-row preload — each prefix run in one + /// session on a fresh DAO-made file and read with the session still open: the pages each step brings into use, + /// the pages reserved, and the free pages in the file. + [Fact(Explicit = true)] + public void Setup_session_reservations() + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + string origin = TemporaryDatabase.CreatePath("pagegroup-origin-"); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + object db = Invoke(workspace, "CreateDatabase", origin, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; + Invoke(db, "Close"); + + const string select = + "INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT a.D * 100 + b.D * 10 + c.D, a.D, " + + "'Bulk label number ' & (a.D * 100 + b.D * 10 + c.D) & ' ' & String(30, 'L'), String(150, 'p'), " + + "a.D + b.D * 0.5 FROM Digits AS a, Digits AS b, Digits AS c WHERE a.D * 100 + b.D * 10 + c.D < "; + var steps = new List<(string Label, string Sql)> { ("CREATE Digits", "CREATE TABLE Digits (D BYTE)") }; + for (int d = 0; d <= 9; d++) steps.Add(($"Digits {d}", $"INSERT INTO Digits (D) VALUES ({d})")); + steps.Add(("CREATE Bulk", "CREATE TABLE Bulk (Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), " + + "Payload TEXT(255), Code BINARY(8), Amount DECIMAL(10,2))")); + steps.Add(("CREATE INDEX", "CREATE INDEX ixBulkLabel ON Bulk (Label)")); + int ddl = steps.Count; + + // Prefixes: nothing, each DDL/insert step, then the preload at a few row counts after the whole DDL. + var runs = new List<(string Label, string[] Sql)> { ("session only", []) }; + for (int k = 1; k <= ddl; k++) runs.Add((steps[k - 1].Label, [.. steps.Take(k).Select(s => s.Sql)])); + foreach (int rows in new[] { 9, 10, 25, 100, 180, 185, 190, 191, 192, 195, 199, 200 }) + runs.Add(($"preload {rows}", [.. steps.Select(s => s.Sql), $"{select}{rows} ORDER BY 1"])); + + var report = new StringBuilder(); + report.AppendLine(CultureInfo.InvariantCulture, $"DAO-made origin: {new FileInfo(origin).Length / PageSize} pages"); + try + { + Dictionary previous = Kinds(origin); + foreach ((string label, string[] sql) in runs) + { + string ace = TemporaryDatabase.CopyPath(origin, "pagegroup-setup-"); + string open = TemporaryDatabase.CreatePath("pagegroup-setupcopy-"); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(ace)) + { + foreach (string statement in sql) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = statement; + command.ExecuteNonQuery(); + } + Thread.Sleep(3000); // past ACE's flush timeout, so the committed pages are on disk + using var source = new FileStream(ace, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var target = File.Create(open); + source.CopyTo(target); + } + + Dictionary kinds = Kinds(open); + byte[] file = File.ReadAllBytes(open); + int pages = file.Length / PageSize; + HashSet free; + using (var database = JetDatabase.Open(open, readOnly: true)) + free = GlobalFree(database.OpenTable("MSysObjects"), 512); + string reserved = Ranges(Enumerable.Range(1, 511) + .Where(p => !free.Contains(p) && (p >= pages || !kinds.ContainsKey(p)))); + var changes = kinds.OrderBy(p => p.Key) + .Where(p => !previous.TryGetValue(p.Key, out string? before) || before != p.Value) + .Select(p => $"+{p.Key}{p.Value}"); + report.AppendLine(CultureInfo.InvariantCulture, + $"{label,-14} ({pages}): {string.Join(" ", changes)}\n{"",16}reserved [{reserved}] free in file [{Ranges(free.Where(p => p < pages))}]"); + previous = kinds; + } + finally + { + TemporaryDatabase.Delete(ace); + TemporaryDatabase.Delete(open); + } + } + output.WriteLine(report.ToString()); + } + finally { TemporaryDatabase.Delete(origin); } + } + + /// Every page that holds something, as its kind: D/L/I with the owning TDEF page for data and index + /// pages, T for a table definition, otherwise the type byte. + private static Dictionary Kinds(string path) + { + byte[] file = File.ReadAllBytes(path); + var kinds = new Dictionary(); + for (int page = 1; page < file.Length / PageSize; page++) + { + ReadOnlySpan p = file.AsSpan(page * PageSize, PageSize); + if (p[0] == 0 && p[1] == 0) continue; + int owner = BinaryPrimitives.ReadInt32LittleEndian(p[4..]); + kinds[page] = PageHeader.ReadType(p) switch + { + PageType.DataPage => $"D@{owner}", + PageType.LeafIndexPage => $"L@{owner}", + PageType.IntermediateIndexPage => $"I@{owner}", + PageType.TableDefinition => "T", + _ => $"x{p[0]:X2}", + }; + } + return kinds; + } + + /// File length, pages whose type byte is 0 (never written), and the pages set in the global free + /// (page 1 row 0) and released (page 1 row 1) maps, whole map range. + private static string Maps(string path) + { + byte[] file = File.ReadAllBytes(path); + int pages = file.Length / PageSize; + using var database = JetDatabase.Open(path, readOnly: true); + Table any = database.OpenTable("Bulk"); + var holder = new DataPage(); + holder.Read(any.Channel.ReadPageShared(1), any.Channel.Format); + var zero = Enumerable.Range(1, pages - 1).Where(p => file[p * PageSize] == 0 && file[p * PageSize + 1] == 0); + return $"{pages} pages; zero [{Ranges(zero)}]; free [{Ranges(Bits(holder.GetRow(0)))}]; " + + $"released [{Ranges(Bits(holder.GetRow(1)))}]"; + + static List Bits(ReadOnlySpan map) + { + var set = new List(); + if (map[0] != 0) { set.Add(-1); return set; } + int start = BinaryPrimitives.ReadInt32LittleEndian(map[1..]); + for (int i = 5; i < map.Length; i++) + for (int bit = 0; bit < 8; bit++) + if ((map[i] & (1 << bit)) != 0) set.Add(start + (i - 5) * 8 + bit); + return set; + } + } + + /// ACE only, the same insert cut at Id < rows for every row count, reporting the pages each + /// extra row brought into use: the order ACE allocates in. + [Theory(Explicit = true)] + [InlineData("Id LONG, Grp LONG, Label TEXT(60), Payload TEXT(255), Code BINARY(8), Amount DECIMAL(10,2)", "")] + [InlineData("Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), Payload TEXT(255), Code BINARY(8), Amount DECIMAL(10,2)", "")] + [InlineData("Id LONG, Grp LONG, Label TEXT(60), Payload TEXT(255), Code BINARY(8), Amount DECIMAL(10,2)", "CREATE INDEX ixBulkLabel ON Bulk (Label)")] + [InlineData("Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), Payload TEXT(255), Code BINARY(8), Amount DECIMAL(10,2)", "CREATE INDEX ixBulkLabel ON Bulk (Label)")] + public void Bulk_insert_snapshots(string columns, string index) => Snapshots(columns, index, 0); + + /// The full table, with the first 200 rows inserted by an earlier session. + [Fact(Explicit = true)] + public void Bulk_insert_snapshots_after_an_earlier_session() => + Snapshots("Id LONG CONSTRAINT pkBulk PRIMARY KEY, Grp LONG, Label TEXT(60), Payload TEXT(255), Code BINARY(8), Amount DECIMAL(10,2)", + "CREATE INDEX ixBulkLabel ON Bulk (Label)", 200); + + private void Snapshots(string columns, string index, int preloaded) + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + string origin = TemporaryDatabase.CreatePath("pagegroup-origin-"); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + object db = Invoke(workspace, "CreateDatabase", origin, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; + Invoke(db, "Close"); + var setup = new List { "CREATE TABLE Digits (D BYTE)" }; + for (int d = 0; d <= 9; d++) setup.Add($"INSERT INTO Digits (D) VALUES ({d})"); + setup.Add($"CREATE TABLE Bulk ({columns})"); + if (index.Length > 0) setup.Add(index); + const string select = + "INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT a.D * 100 + b.D * 10 + c.D, a.D, " + + "'Bulk label number ' & (a.D * 100 + b.D * 10 + c.D) & ' ' & String(30, 'L'), String(150, 'p'), " + + "a.D + b.D * 0.5 FROM Digits AS a, Digits AS b, Digits AS c WHERE a.D * 100 + b.D * 10 + c.D "; + if (preloaded > 0) setup.Add($"{select}< {preloaded} ORDER BY 1"); + var report = new StringBuilder(); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(origin)) + foreach (string statement in setup) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = statement; + command.ExecuteNonQuery(); + } + report.AppendLine(CultureInfo.InvariantCulture, $"before: {new FileInfo(origin).Length / PageSize} pages"); + + Dictionary previous = []; + for (int rows = preloaded + 1; rows <= 1000; rows++) + { + string ace = TemporaryDatabase.CopyPath(origin, "pagegroup-snap-"); + try + { + using (OleDbConnection connection = AceTestDatabase.Open(ace)) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = $"{select}BETWEEN {preloaded} AND {rows - 1} ORDER BY 1"; + command.ExecuteNonQuery(); + } + Dictionary used = Used(ace); + var changes = new List(); + foreach (var (page, kind) in used.OrderBy(p => p.Key)) + if (!previous.TryGetValue(page, out string? before) || before != kind) changes.Add($"+{page}{kind}"); + foreach (int page in previous.Keys.Where(p => !used.ContainsKey(p)).Order()) changes.Add($"-{page}"); + if (changes.Count > 0) + report.AppendLine(CultureInfo.InvariantCulture, + $"{rows,5} ({new FileInfo(ace).Length / PageSize}): {string.Join(" ", changes)}"); + previous = used; + if (rows % 100 == 0) report.AppendLine(CultureInfo.InvariantCulture, $" {Runs(ace)}"); + } + finally { TemporaryDatabase.Delete(ace); } + } + output.WriteLine(report.ToString()); + } + finally { TemporaryDatabase.Delete(origin); } + } + + /// The file from Bulk's first page on as runs: 51-61 D0 is data pages whose rows continue one + /// another from id 0, L leaf pages, . free, with the file length and the global free pages. + private static string Runs(string path) + { + byte[] file = File.ReadAllBytes(path); + using var database = JetDatabase.Open(path, readOnly: true); + Table bulk = database.OpenTable("Bulk"); + int tdef = bulk.Definition.DefinitionPage; + HashSet free = GlobalFree(bulk, file.Length / PageSize); + var parts = new List(); + int start = -1, startId = -1, nextId = -1; string kind = ""; + for (int page = tdef + 1; page <= file.Length / PageSize; page++) + { + string k; int first = -1, last = -1; + if (page == file.Length / PageSize) k = "end"; + else + { + ReadOnlySpan p = file.AsSpan(page * PageSize, PageSize); + k = PageHeader.ReadType(p) switch + { + PageType.DataPage when BinaryPrimitives.ReadInt32LittleEndian(p[4..]) == tdef => "D", + PageType.LeafIndexPage => "L", + PageType.IntermediateIndexPage => "I", + _ when free.Contains(page) => ".", + _ => $"x{p[0]:X2}", + }; + if (k == "D") (first, last) = IdRange(bulk, page); + } + bool continues = k == kind && (k != "D" || first == nextId); + if (!continues) + { + if (start >= 0) parts.Add(Run(start, page - 1, kind)); + start = page; kind = k; startId = first; + } + if (k == "D") nextId = last + 1; + } + return $"{file.Length / PageSize} pages | " + string.Join(" ", parts); + + string Run(int from, int to, string what) => + (from == to ? $"{from}" : $"{from}-{to}") + (what == "D" ? $"D{startId}" : what); + } + + /// Every page past Bulk's definition that holds something: D for a Bulk data page, L leaf, I + /// intermediate, or the type byte. + private static Dictionary Used(string path) + { + byte[] file = File.ReadAllBytes(path); + using var database = JetDatabase.Open(path, readOnly: true); + int tdef = database.OpenTable("Bulk").Definition.DefinitionPage; + var used = new Dictionary(); + for (int page = tdef + 1; page < file.Length / PageSize; page++) + { + ReadOnlySpan p = file.AsSpan(page * PageSize, PageSize); + string? kind = PageHeader.ReadType(p) switch + { + PageType.DataPage when BinaryPrimitives.ReadInt32LittleEndian(p[4..]) == tdef => "D", + PageType.LeafIndexPage => "L", + PageType.IntermediateIndexPage => "I", + _ when p[0] == 0 => null, + _ => $"x{p[0]:X2}", + }; + if (kind is not null) used[page] = kind; + } + return used; + } + + private static (int First, int Last) IdRange(Table bulk, int page) + { + var data = new DataPage(); + data.Read(bulk.Channel.ReadPageShared(page), bulk.Channel.Format); + var ids = new List(); + for (int row = 0; row < data.RowCount; row++) + if (data.Rows[row] is { IsDeleted: false, HasOverflow: false }) + ids.Add(BinaryPrimitives.ReadInt32LittleEndian(data.GetRow(row)[2..])); + return ids.Count == 0 ? (-2, -2) : (ids.Min(), ids.Max()); + } + + private static void Describe(StringBuilder report, string name, string path) + { + byte[] file = File.ReadAllBytes(path); + using var database = JetDatabase.Open(path, readOnly: true); + Table bulk = database.OpenTable("Bulk"); + int bulkTdef = bulk.Definition.DefinitionPage; + var owned = new HashSet(bulk.UsageMap.DataPages()); + var freeSpace = new HashSet(bulk.UsageMap.FreeDataPages()); + HashSet globalFree = GlobalFree(bulk, file.Length / PageSize); + + report.AppendLine(CultureInfo.InvariantCulture, + $"==== {name}: {file.Length / PageSize} pages; Bulk tdef {bulkTdef}; owned [{Ranges(owned)}]; " + + $"free-space [{Ranges(freeSpace)}]; global free (in file) [{Ranges(globalFree)}]"); + for (int page = 1; page < file.Length / PageSize; page++) + { + ReadOnlySpan p = file.AsSpan(page * PageSize, PageSize); + PageType type = PageHeader.ReadType(p); + int tdef = BinaryPrimitives.ReadInt32LittleEndian(p[4..]); + string what = type switch + { + PageType.DataPage when tdef == bulkTdef => $"D {BulkIds(bulk, page)}", + PageType.DataPage => $"d tdef {tdef}", + PageType.LeafIndexPage => $"{(tdef == bulkTdef ? "L" : "l")} tdef {tdef} entries-end 0x{BinaryPrimitives.ReadUInt16LittleEndian(p[2..]):X}", + PageType.IntermediateIndexPage => $"{(tdef == bulkTdef ? "I" : "i")} tdef {tdef}", + _ => $"type 0x{p[0]:X2}", + }; + string flags = (owned.Contains(page) ? " owned" : "") + (freeSpace.Contains(page) ? " has-space" : "") + + (globalFree.Contains(page) ? " FREE" : ""); + if (page < 8 && type != PageType.DataPage) continue; + report.AppendLine(CultureInfo.InvariantCulture, $" {page,4} {what}{flags}"); + } + } + + private static string BulkIds(Table bulk, int page) + { + var data = new DataPage(); + data.Read(bulk.Channel.ReadPageShared(page), bulk.Channel.Format); + var ids = new List(); + for (int row = 0; row < data.RowCount; row++) + { + if (data.Rows[row] is not { IsDeleted: false, HasOverflow: false }) continue; + ids.Add(BinaryPrimitives.ReadInt32LittleEndian(data.GetRow(row)[2..])); + } + return ids.Count == 0 ? "empty" : $"{ids.Count} rows ids {ids.Min()}-{ids.Max()}"; + } + + /// The global free-pages map: page 1 row 0 in every file ACE writes, an inline map with a set bit + /// for a free page. + private static HashSet GlobalFree(Table any, int pages) + { + var holder = new DataPage(); + holder.Read(any.Channel.ReadPageShared(1), any.Channel.Format); + ReadOnlySpan map = holder.GetRow(0); + var free = new HashSet(); + if (map[0] != 0) return free; + int start = BinaryPrimitives.ReadInt32LittleEndian(map[1..]); + for (int i = 5; i < map.Length; i++) + for (int bit = 0; bit < 8; bit++) + if ((map[i] & (1 << bit)) != 0 && start + (i - 5) * 8 + bit < pages) free.Add(start + (i - 5) * 8 + bit); + return free; + } + + private static string Ranges(IEnumerable pages) + { + var sorted = pages.Order().ToList(); + var parts = new List(); + for (int i = 0; i < sorted.Count;) + { + int j = i; + while (j + 1 < sorted.Count && sorted[j + 1] == sorted[j] + 1) j++; + parts.Add(i == j ? $"{sorted[i]}" : $"{sorted[i]}-{sorted[j]}"); + i = j + 1; + } + return string.Join(",", parts); + } + + private static object? Invoke(object target, string member, params object?[] args) => + target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); +} From 8c4b482a6621189a5bd89229c9cbbabb3fde304f Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Tue, 29 Sep 2026 23:21:43 +0800 Subject: [PATCH 73/88] Assert the step-by-step whole-file parity with ACE The step-by-step comparison only reported. 136 of its 139 statements leave a file byte-identical to ACE's, so it now asserts that, with the three that differ named and the reason for each: the bulk INSERT ... SELECT (ACE's extent allocation), a row's long values written in a different order, and an UPDATE that reuses the pages of the value it replaces. A named statement must still differ, and differ only in pages, so the list cannot outlive a fix. The whole-run comparison stays a report: one early placement difference misaligns every page after it. Co-Authored-By: Claude Opus 5.5 --- .../WholeFileParityProbeTest.cs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs index d8eb06fc..c4118d0c 100644 --- a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs @@ -13,13 +13,14 @@ namespace LibRed.Engine.Tests; /// /// About 140 statements run through ACE over OLE DB and through LibRed on copies of one ACE-created empty database, -/// and the files compared byte for byte. It reports how far apart they are rather than asserting they match. +/// and the files compared byte for byte. /// /// /// Two measurements. The whole run applies every statement in one session per engine and compares the two files at -/// the end — how close LibRed comes over a realistic workload. Step by step applies one statement at a time, both -/// engines starting from the same file — ACE's result of the step before — so each statement is compared on its own -/// and one early divergence does not leave every later page misaligned. +/// the end — how close LibRed comes over a realistic workload; it reports, since one early placement difference +/// leaves every later page misaligned. Step by step applies one statement at a time, both engines starting from the +/// same file — ACE's result of the step before — so each statement is compared on its own, and it asserts: every +/// statement leaves identical files except the , each of which must still differ. /// Only what differs for reasons other than the format is masked: page 0's user commit-byte table, which /// counts a lock-file user's writes and LibRed does not keep; the wall-clock DateCreate and DateUpdate of each /// MSysObjects row; and a data page's write stamp at 0x08, with the copy of it a chained long value's descriptor @@ -71,6 +72,7 @@ public void Each_statement_leaves_libred_and_ace_with_the_same_file() string current = origin; var summary = new StringBuilder(); var details = new StringBuilder(); + var outcomes = new string[statements.Length]; int same = 0; try { @@ -88,6 +90,7 @@ public void Each_statement_leaves_libred_and_ace_with_the_same_file() ? $"ACE: {aceError ?? "ok"} | LibRed: {libredError ?? "ok"}" : pages == 0 ? "identical" : $"{pages} page(s) differ"; if (outcome == "identical") same++; + outcomes[i] = outcome; summary.AppendLine(CultureInfo.InvariantCulture, $"#{i + 1,-3} {outcome,-22} {Abbreviate(statements[i])}"); if (pages > 0) details.AppendLine(CultureInfo.InvariantCulture, $"==== #{i + 1} {statements[i]}").Append(report); @@ -100,6 +103,17 @@ public void Each_statement_leaves_libred_and_ace_with_the_same_file() } output.WriteLine($"{same} of {statements.Length} statements left identical files\n{summary}\n{details}"); + + var unexpected = new List(); + for (int i = 0; i < statements.Length; i++) + { + string? known = KnownDifferences.FirstOrDefault(k => statements[i].StartsWith(k.Statement, StringComparison.Ordinal)).Reason; + if (outcomes[i] == "identical" && known is not null) + unexpected.Add($"#{i + 1} is now identical; remove it from the known differences: {Abbreviate(statements[i])}"); + else if (outcomes[i] != "identical" && (known is null || !outcomes[i].EndsWith("differ", StringComparison.Ordinal))) + unexpected.Add($"#{i + 1} {outcomes[i]}: {Abbreviate(statements[i])}"); + } + Assert.True(unexpected.Count == 0, string.Join("\n", unexpected)); } finally { @@ -108,6 +122,17 @@ public void Each_statement_leaves_libred_and_ace_with_the_same_file() } } + /// The statements, by how each begins, that ACE and LibRed are known to leave differently — each a + /// placement difference with every pointer correct — and why. + private static readonly (string Statement, string Reason)[] KnownDifferences = + [ + ("INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT", + "ACE allocates from session extents and 8-page groups (PageGroupProbeTest); LibRed takes the lowest free page"), + ("INSERT INTO Doc VALUES (1,", "ACE writes the row's long values in a different order"), + ("UPDATE Doc SET Body = String(8000, 'e')", + "ACE does not reuse the replaced long value's pages within the UPDATE that frees them"), + ]; + /// A hundred statements over six tables: creates with every ordinary column type and each kind of /// constraint, inserts, updates and deletes, more inserts into the space the deletes freed, a column added, /// retyped and dropped, indexes made and dropped, and a table dropped and another made after it. Then what From 74f63faa55bec3bdea69c326ed34d991f052a33a Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Tue, 29 Sep 2026 23:25:39 +0800 Subject: [PATCH 74/88] Free an UPDATE's replaced long value after writing the new one An UPDATE that replaced a chained Memo/OLE value freed the old pages before writing the new value, so the new value landed on them. ACE frees them in the same statement but only after the new value is written: the UPDATE that frees them never reuses them, the statements after it do. Freeing afterwards makes LibRed's file byte-identical to ACE's for such an UPDATE, so the whole-file parity comparison no longer lists it as a known difference. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Storage/RowInserter.cs | 8 ++++++-- src/LibRed/README.md | 3 ++- src/LibRed/docs/format/page-05-usage-maps.md | 10 +++++----- test/LibRed.Core.Tests/ReleaseAtCloseTests.cs | 10 ++++++++-- .../WholeFileParityProbeTest.cs | 2 -- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/LibRed/LibRed.Core/Storage/RowInserter.cs b/src/LibRed/LibRed.Core/Storage/RowInserter.cs index ccfb1d1c..a0f80e66 100644 --- a/src/LibRed/LibRed.Core/Storage/RowInserter.cs +++ b/src/LibRed/LibRed.Core/Storage/RowInserter.cs @@ -133,17 +133,19 @@ private void UpdateCore(RowId id, object?[] values, IReadOnlySet changedCol object?[] storage = (object?[])values.Clone(); // Long-value (memo/OLE) columns: keep an unchanged column's on-disk descriptor verbatim (so it is not - // needlessly re-materialised onto fresh LVAL pages), and free a changed column's old chained pages. + // needlessly re-materialised onto fresh LVAL pages), and free a changed column's old chained pages — + // after the new values are written, as ACE does: the UPDATE that frees them never reuses them. byte[] oldRow = ReadRowBytes(id); // One layout for both: the trailer arithmetic is the same for either selection off this row. RowLayout oldLayout = RowDecoder.ParseLayout(_table.Columns, format, oldRow); var oldDescriptors = RowDecoder.LongValueDescriptors(_table.Columns, oldLayout, oldRow); var oldCalculated = RowDecoder.CalculatedSlots(_table.Columns, oldLayout, oldRow); + var replaced = new List<(ColumnDef Column, byte[] Descriptor)>(); foreach (ColumnDef column in _table.Columns) { if (column.Type is not (JetDataType.Memo or JetDataType.Ole)) continue; if (!oldDescriptors.TryGetValue(column.Index, out byte[]? oldDescriptor)) continue; // old value was null - if (changedColumns.Contains(column.Index)) FreeLongValue(column, oldDescriptor, releaseAtClose: false); + if (changedColumns.Contains(column.Index)) replaced.Add((column, oldDescriptor)); else storage[column.Index] = new LongValueDescriptor(oldDescriptor); } @@ -175,6 +177,8 @@ private void UpdateCore(RowId id, object?[] values, IReadOnlySet changedCol } MaterializeLongValues(storage); + foreach ((ColumnDef column, byte[] descriptor) in replaced) + FreeLongValue(column, descriptor, releaseAtClose: false); byte[] srcPage = _channel.ReadPageShared(id.Page).Span.ToArray(); // Use the same guarded inference as Insert (Math.Max with the column-derived length): the raw per-row diff --git a/src/LibRed/README.md b/src/LibRed/README.md index c823518c..9576b66a 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -99,7 +99,8 @@ Treat the number as of its date — an EF Core version bump moves it. `int32`, which ACE does not treat as an error either); unique-index statistics; allocation through the global free-pages map; `MSysObjects` / `MSysACEs` catalog rows. `UPDATE`/`DELETE` write in place, relocate rows that no longer fit, maintain every index, and reclaim LVAL pages. Freed pages go back to that map - when the database closes, as ACE holds them — only an `UPDATE`'s replaced long value is reusable at once. + when the database closes, as ACE holds them — only an `UPDATE`'s replaced long value is reusable at once, + by the statements after it. `DROP TABLE` frees every page the table owns: data and long-value pages (each Memo/OLE column's hang off a per-column usage map, and are most of a memo-heavy table), every page of every index, the TDEF and its continuation pages, and a reference-form map's bitmap pages. It retires each map's records from their diff --git a/src/LibRed/docs/format/page-05-usage-maps.md b/src/LibRed/docs/format/page-05-usage-maps.md index 1a1f46d7..1c329ba1 100644 --- a/src/LibRed/docs/format/page-05-usage-maps.md +++ b/src/LibRed/docs/format/page-05-usage-maps.md @@ -276,12 +276,12 @@ usage map, etc.), and the only change to page 1 is one cleared bit per page take > taken before it. Closing a writable `JetDatabase` returns those pages and any already set in the > released-pages map to the free map, clears the released map and first sizes it as above, all in one > transaction. A close that released nothing and wrote nothing writes -> nothing. Only an `UPDATE`'s replaced long value goes through `Free` at once. +> nothing. Only an `UPDATE`'s replaced long value goes through `Free` at once — after the new value is written, +> so, as in ACE, the `UPDATE` that frees those pages does not reuse them (the whole file then matches ACE's). > -> **Where LibRed differs from ACE.** An `UPDATE` frees the old long value before writing the new one, so the -> new value reuses those pages in the same statement. Held pages live in the handle, not in the released-pages -> map — which is ACE's own behaviour (above), the difference being only that each LibRed handle releases its -> own at its own close, while other handles stay open. +> **Where LibRed differs from ACE.** Held pages live in the handle, not in the released-pages map — which is +> ACE's own behaviour (above), the difference being only that each LibRed handle releases its own at its own +> close, while other handles stay open. > > **Global-map growth.** The inline growth rule in §9 applies, but ACE leaves **4 bytes free in the > holder page** before promoting the global map to reference form. With a 69-byte companion row, diff --git a/test/LibRed.Core.Tests/ReleaseAtCloseTests.cs b/test/LibRed.Core.Tests/ReleaseAtCloseTests.cs index 9377d1e9..32f9a908 100644 --- a/test/LibRed.Core.Tests/ReleaseAtCloseTests.cs +++ b/test/LibRed.Core.Tests/ReleaseAtCloseTests.cs @@ -162,11 +162,17 @@ public void Dropping_an_index_holds_its_root_but_a_memo_update_frees_the_old_cha table = db.OpenTable("Paths"); (RowId id, object?[] values) = table.Rows().WithIds().Single(); - // The old chain is free at once, so the new value lands on it and the file does not grow. + // The old chain is freed after the new value is written, as ACE does, so this update grows the file... int pagesBefore = channel.PageCount; values[1] = new string('b', 20000); table.Update(id, values, new HashSet { 1 }); - Assert.Equal(pagesBefore, channel.PageCount); + Assert.True(channel.PageCount > pagesBefore); + + // ...but it is free at once, not held to close: the next update lands on it and the file does not grow. + int pagesAfterFirst = channel.PageCount; + values[1] = new string('c', 20000); + table.Update(id, values, new HashSet { 1 }); + Assert.Equal(pagesAfterFirst, channel.PageCount); } finally { TemporaryDatabase.Delete(path); } } diff --git a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs index c4118d0c..02738050 100644 --- a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs @@ -129,8 +129,6 @@ private static readonly (string Statement, string Reason)[] KnownDifferences = ("INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT", "ACE allocates from session extents and 8-page groups (PageGroupProbeTest); LibRed takes the lowest free page"), ("INSERT INTO Doc VALUES (1,", "ACE writes the row's long values in a different order"), - ("UPDATE Doc SET Body = String(8000, 'e')", - "ACE does not reuse the replaced long value's pages within the UPDATE that frees them"), ]; /// A hundred statements over six tables: creates with every ordinary column type and each kind of From 06482c146ed835450b88d8f3a95b87f168bb40ab Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Tue, 29 Sep 2026 23:34:08 +0800 Subject: [PATCH 75/88] Write a row's long values where ACE writes them Two differences in how a row's Memo/OLE values reach their pages, both measured against ACE with a new probe: - ACE writes all of a row's chained values first and its single-page values after, each in column order. LibRed wrote them in plain column order, so a single-page value in an earlier column took the page before the chains. - ACE places a compressed single-page value by its uncompressed size: it writes the uncompressed bytes where they would go, then the compressed row over their upper end, leaving the rest in the page's free space. A page with room for the compressed row but not the uncompressed bytes is passed over for a new one. LibRed placed and wrote only the compressed row. With both, the whole-file parity comparison's long-value INSERT is byte-identical to ACE's and leaves the known differences. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Storage/LongValueWriter.cs | 26 ++- src/LibRed/LibRed.Core/Storage/RowInserter.cs | 30 ++- src/LibRed/docs/format/data-types.md | 4 +- src/LibRed/docs/format/long-values.md | 18 ++ .../LongValueOrderProbeTest.cs | 195 ++++++++++++++++++ .../WholeFileParityProbeTest.cs | 1 - 6 files changed, 257 insertions(+), 17 deletions(-) create mode 100644 test/LibRed.Engine.AccessTests/LongValueOrderProbeTest.cs diff --git a/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs b/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs index 940d805e..8ae0903c 100644 --- a/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs @@ -92,18 +92,26 @@ public LongValueResult Write(byte[] payload) } /// Allocates a fresh LVAL page, writes as its row 0, and returns the - /// page number — the caller records it in the column's usage maps. - public int WriteNewPage(byte[] row) + /// page number — the caller records it in the column's usage maps. is as + /// for . + public int WriteNewPage(byte[] row, byte[]? uncompressed = null) { int page = _allocator.Allocate(); - WriteChunkPage(page, row); + WriteChunkPage(page, row, uncompressed: uncompressed); return page; } /// Appends to an existing LVAL page if it has room, returning the new /// row index and the page's remaining free space (null if it does not fit). Lets several small long /// values share one page, the way Access packs them. - public (int Row, int RemainingFree)? TryAppend(int pageNumber, byte[] row) + /// + /// is the value before compression, when is its + /// compressed form. ACE places such a value as though it were uncompressed — the page must have room for the + /// uncompressed bytes — writes those bytes where they would go, then the compressed row over their upper end, + /// so the rest of the uncompressed image stays behind in the page's free space. Both are measured + /// (long-values.md). + /// + public (int Row, int RemainingFree)? TryAppend(int pageNumber, byte[] row, byte[]? uncompressed = null) { JetFormatBase format = _channel.Format; if (pageNumber <= 0 || pageNumber >= _channel.PageCount) @@ -137,10 +145,12 @@ public int WriteNewPage(byte[] row) if (parsed.FreeSpace != physicalFree) throw new InvalidDataException( $"LVAL page {pageNumber} declares {parsed.FreeSpace} free bytes but its row geometry has {physicalFree}."); - if (physicalFree < row.Length + 2) return null; // row data + its 2-byte directory entry + // Row data + its 2-byte directory entry; a compressed value needs room for its uncompressed bytes. + if (physicalFree < Math.Max(row.Length, uncompressed?.Length ?? 0) + 2) return null; byte[] page = buffer.Span.ToArray(); + uncompressed?.CopyTo(page.AsSpan(lowest - uncompressed.Length)); int offset = lowest - row.Length; row.CopyTo(page.AsSpan(offset)); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + rowCount * 2, 2), (ushort)offset); @@ -158,8 +168,9 @@ public static byte[] SinglePageDescriptor(int length, int page, int row) => /// Writes one row () to a fresh LVAL data page, packed from the page end. /// is the chain stamp for the first page of a chain, and zero everywhere else — - /// which is what ACE writes on a single-page value and on every chunk after the first. - private void WriteChunkPage(int pageNumber, byte[] row, uint stamp = 0) + /// which is what ACE writes on a single-page value and on every chunk after the first. + /// is as for . + private void WriteChunkPage(int pageNumber, byte[] row, uint stamp = 0, byte[]? uncompressed = null) { JetFormatBase format = _channel.Format; var page = new byte[format.PageSize]; @@ -167,6 +178,7 @@ private void WriteChunkPage(int pageNumber, byte[] row, uint stamp = 0) BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.DataOwnerOffset, 4), LongValueFormat.LvalMarker); BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.DataChainStampOffset, 4), stamp); + uncompressed?.CopyTo(page.AsSpan(format.PageSize - uncompressed.Length)); int offset = format.PageSize - row.Length; row.CopyTo(page.AsSpan(offset)); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset, 2), (ushort)offset); diff --git a/src/LibRed/LibRed.Core/Storage/RowInserter.cs b/src/LibRed/LibRed.Core/Storage/RowInserter.cs index a0f80e66..117c81e3 100644 --- a/src/LibRed/LibRed.Core/Storage/RowInserter.cs +++ b/src/LibRed/LibRed.Core/Storage/RowInserter.cs @@ -1060,10 +1060,21 @@ private static int LowestRowOffset(byte[] page, JetFormatBase format, int rowCou /// private void MaterializeLongValues(object?[] values) { + // ACE writes a row's chained values first and its single-page values after, each in column order + // (measured), so a single-page value in an earlier column takes the page after the chains, not before. LongValueWriter? writer = null; - foreach (ColumnDef column in _table.Columns) - if (column.Type is JetDataType.Memo or JetDataType.Ole) - values[column.Index] = MaterializeLongValue(column, values[column.Index], ref writer); + foreach (bool chained in new[] { true, false }) + foreach (ColumnDef column in _table.Columns) + if (column.Type is JetDataType.Memo or JetDataType.Ole && Chains(values[column.Index]) == chained) + values[column.Index] = MaterializeLongValue(column, values[column.Index], ref writer); + + // The storage form follows the uncompressed length, as MaterializeLongValue decides it. + static bool Chains(object? value) => value switch + { + string s => Encoding.Unicode.GetByteCount(s) > LongValueFormat.MaxSinglePageValue, + byte[] b => b.Length > LongValueFormat.MaxSinglePageValue, + _ => false, + }; } /// The storage form of one Memo/OLE value: a value over 64 bytes is written to an LVAL page and @@ -1088,16 +1099,18 @@ private void MaterializeLongValues(object?[] values) // Compress before storing, but AFTER the inline test above and using the uncompressed length for // the single-page test below: ACE decides the storage form on the uncompressed size and applies // compression to whatever form results, never to a chained value (LongTextStorageAccessTests). + // ACE places a compressed value by its uncompressed bytes and leaves them behind (LongValueWriter.TryAppend). + byte[]? uncompressed = null; if (payload.Length <= LongValueFormat.MaxSinglePageValue && value is string text && Types.JetTypeCodec.TryCompressText(column, text) is { } compressed) - payload = compressed; + (uncompressed, payload) = (payload, compressed); writer ??= new LongValueWriter(_channel); TableDefinitionPage definition = LongValueMaps; definition.LongValueOwnedMaps.TryGetValue(column.ColumnId, out (int Row, int Page) owned); definition.LongValueFreeMaps.TryGetValue(column.ColumnId, out (int Row, int Page) free); - return new LongValueDescriptor(StoreLongValue(writer, payload, owned, free)); + return new LongValueDescriptor(StoreLongValue(writer, payload, owned, free, uncompressed)); } // A page is dropped from the free-pages map once it cannot hold a 256-byte value and its 2-byte row-directory @@ -1160,7 +1173,8 @@ public byte[] StorePackedLongValue(int columnId, byte[] payload) /// and MSysAccessXML.LValue that way, and packs nothing into them — measured across every such file, /// one LVAL page per stored value. /// - private byte[] StoreLongValue(LongValueWriter writer, byte[] payload, (int Row, int Page) owned, (int Row, int Page) free) + private byte[] StoreLongValue(LongValueWriter writer, byte[] payload, (int Row, int Page) owned, (int Row, int Page) free, + byte[]? uncompressed = null) { if (owned.Page == 0) throw new InvalidDataException("Long-value column has no owned-pages usage-map pointer."); @@ -1179,14 +1193,14 @@ private byte[] StoreLongValue(LongValueWriter writer, byte[] payload, (int Row, // Pack onto the first free page that has room for the value plus its directory entry. if (free.Page != 0) foreach (int page in freePages) - if (writer.TryAppend(page, payload) is (int row, int remaining)) + if (writer.TryAppend(page, payload, uncompressed) is (int row, int remaining)) { if (remaining < MinLvalRow) _usageMaps.SetBit(free.Row, free.Page, page, set: false); // now full return LongValueWriter.SinglePageDescriptor(payload.Length, page, row); } // No free page had room: a fresh page (owned, and free — it still has spare room). - int newPage = writer.WriteNewPage(payload); + int newPage = writer.WriteNewPage(payload, uncompressed); _usageMaps.SetBit(owned.Row, owned.Page, newPage, set: true); if (free.Page != 0) _usageMaps.SetBit(free.Row, free.Page, newPage, set: true, movableWindow: true); diff --git a/src/LibRed/docs/format/data-types.md b/src/LibRed/docs/format/data-types.md index 29ebe934..b3707315 100644 --- a/src/LibRed/docs/format/data-types.md +++ b/src/LibRed/docs/format/data-types.md @@ -220,7 +220,9 @@ byte-for-byte): compresses (whatever the capable flag says), a single-page one compresses only on a `WITH COMPRESSION` column, and a chained one never does; the compressed size never approaches any limit. Microsoft's "only instances that, when compressed, will fit within 4096 bytes" describes the wrong - quantity; see [long-values.md](long-values.md) for the measured boundary. + quantity; see [long-values.md](long-values.md) for the measured boundary. The uncompressed length also picks + the *page* for a compressed single-page value, and its uncompressed bytes are left in that page's free space + ([long-values.md](long-values.md#writing-long-values)). > **The mixed form is real, and ACE writes it readily.** A value can toggle between 1-byte and 2-byte runs > mid-string: after the `FF FE` marker the value starts in 1-byte mode and every `0x00` byte at a character diff --git a/src/LibRed/docs/format/long-values.md b/src/LibRed/docs/format/long-values.md index 081dd089..88ff6b30 100644 --- a/src/LibRed/docs/format/long-values.md +++ b/src/LibRed/docs/format/long-values.md @@ -185,6 +185,24 @@ the long values: > each on its own page with a 4-byte next-pointer, matching ACE byte-for-byte (verified: LibRed and > Access both read back memo values from 65 bytes to 100 KB — single-page and multi-page). > +> **A row's values are written chains first.** ACE writes every chained value of the row, in column order, and +> only then its single-page values, in column order — so a single-page value in an earlier column takes a +> page after the chains, not before them. Measured on one inserted row with each pairing: two chains, two +> single-page values (either the larger first), and a single-page value before a chain, which alone differs +> from plain column order. LibRed writes in the same order, and the pages match ACE's. +> +> **A compressed single-page value is placed by its uncompressed size, and leaves its uncompressed bytes +> behind.** ACE writes the value's uncompressed UTF-16 bytes where they would sit — ending at the page end on a +> fresh page, or at the lowest existing row on a shared one — then the compressed row, ending at the same place, +> over their upper end; the rest of the uncompressed image stays in the page's free space. So a page takes the +> value only if it has room for the *uncompressed* bytes and a directory entry: `String(1800,'e')` then +> `String(1200,'f')` in a `MEMO WITH COMPRESSION` column put the second value on a new page, though its 1,202 +> compressed bytes would have fit beside the first; `String(1000,'f')` shared the page. The free-space field and +> the free-pages map count the compressed row. Measured on a fresh page (300 and 1,800 characters), on a shared +> page, and at that boundary; the exact room test (whether the directory entry counts on top of the +> uncompressed bytes) is not measured. A value that does not compress, or a column without compression, leaves +> nothing behind. LibRed does the same, byte for byte. +> > **LibRed writes the §3.3.2 entry + empty usage maps for every memo/OLE column** — byte-faithful with > ACE, whose usage-map page lays the records out as: row 0 table-owned, row 1 table-free, then one row > **per index**, then two rows (owned/free) **per long-value column** (verified against ACE-created diff --git a/test/LibRed.Engine.AccessTests/LongValueOrderProbeTest.cs b/test/LibRed.Engine.AccessTests/LongValueOrderProbeTest.cs new file mode 100644 index 00000000..1fbb3440 --- /dev/null +++ b/test/LibRed.Engine.AccessTests/LongValueOrderProbeTest.cs @@ -0,0 +1,195 @@ +using System.Buffers.Binary; +using System.Data.OleDb; +using System.Globalization; +using LibRed; +using LibRed.Catalog; +using LibRed.Pages; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// Probe: which pages ACE and LibRed give each long value of one inserted row, to see the order ACE writes a row's +/// long values in. Reports; asserts nothing. +/// +[Collection(AceCollection.Name)] +public class LongValueOrderProbeTest(ITestOutputHelper output) +{ + [Theory(Explicit = true)] + [InlineData("INSERT INTO Doc VALUES (1, String(5000, 'b'), String(300, 'c'), 'short and compressed', 0x{6000}, 0x{400})")] + [InlineData("INSERT INTO Doc (Id, Body, Blob) VALUES (1, String(5000, 'b'), 0x{6000})")] + [InlineData("INSERT INTO Doc (Id, Body, Blob) VALUES (1, String(300, 'b'), 0x{6000})")] + [InlineData("INSERT INTO Doc (Id, Body, Blob) VALUES (1, String(5000, 'b'), 0x{300})")] + [InlineData("INSERT INTO Doc (Id, Body, Packed) VALUES (1, String(5000, 'b'), String(300, 'c'))")] + [InlineData("INSERT INTO Doc (Id, Body, Blob) VALUES (1, String(5000, 'b'), 0x{12000})")] + [InlineData("INSERT INTO Doc (Id, Body, Blob) VALUES (1, String(200, 'b'), 0x{600})")] + [InlineData("INSERT INTO Doc (Id, Body, Blob) VALUES (1, String(1000, 'b'), 0x{600})")] + public void Long_value_pages_of_one_row(string insert) + { + string sql = System.Text.RegularExpressions.Regex.Replace(insert, @"0x\{(\d+)\}", + m => "0x" + Hex(int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture))); + string origin = TemporaryDatabase.CreatePath("lvorder-origin-"); + string ace = TemporaryDatabase.CreatePath("lvorder-ace-"); + string libred = TemporaryDatabase.CreatePath("lvorder-libred-"); + try + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + object db = Invoke(workspace, "CreateDatabase", origin, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; + Invoke(db, "Close"); + using (OleDbConnection connection = AceTestDatabase.Open(origin)) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = "CREATE TABLE Doc (Id LONG CONSTRAINT pkDoc PRIMARY KEY, Body MEMO, " + + "Packed MEMO WITH COMPRESSION, Brief TEXT(100) WITH COMPRESSION, Blob LONGBINARY, Big BIGBINARY(500))"; + command.ExecuteNonQuery(); + } + File.Copy(origin, ace, overwrite: true); + File.Copy(origin, libred, overwrite: true); + + using (OleDbConnection connection = AceTestDatabase.Open(ace)) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = sql; + command.ExecuteNonQuery(); + } + using (var database = JetDatabase.Open(libred, readOnly: false)) + new QueryEngine(database).ExecuteNonQuery(sql); + + output.WriteLine($"{insert}\nbefore {new FileInfo(origin).Length / 4096} pages\n" + + $"ACE: {Describe(ace)}\nLibRed: {Describe(libred)}"); + } + finally + { + foreach (string path in new[] { origin, ace, libred }) TemporaryDatabase.Delete(path); + } + } + + /// Compressed single-page memos: the bytes of every long-value page that differ between ACE and + /// LibRed, as ranges with ACE's bytes at the start of each — to see what ACE leaves in a page's free space. + [Theory(Explicit = true)] + [InlineData("INSERT INTO Doc (Id, Packed) VALUES (1, String(300, 'c'))")] + [InlineData("INSERT INTO Doc (Id, Packed) VALUES (1, String(300, 'c'))|INSERT INTO Doc (Id, Packed) VALUES (2, String(200, 'd'))")] + [InlineData("INSERT INTO Doc (Id, Packed) VALUES (1, String(300, ChrW(20013)))")] + [InlineData("INSERT INTO Doc (Id, Packed) VALUES (1, String(1800, 'e'))")] + [InlineData("INSERT INTO Doc (Id, Body) VALUES (1, String(300, 'c'))")] + [InlineData("INSERT INTO Doc (Id, Packed) VALUES (1, String(1800, 'e'))|INSERT INTO Doc (Id, Packed) VALUES (2, String(1000, 'f'))")] + [InlineData("INSERT INTO Doc (Id, Packed) VALUES (1, String(1800, 'e'))|INSERT INTO Doc (Id, Packed) VALUES (2, String(1200, 'f'))")] + public void Compressed_value_page_bytes(string statements) + { + string origin = TemporaryDatabase.CreatePath("lvbytes-origin-"); + string ace = TemporaryDatabase.CreatePath("lvbytes-ace-"); + string libred = TemporaryDatabase.CreatePath("lvbytes-libred-"); + try + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; + object db = Invoke(workspace, "CreateDatabase", origin, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; + Invoke(db, "Close"); + using (OleDbConnection connection = AceTestDatabase.Open(origin)) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = "CREATE TABLE Doc (Id LONG CONSTRAINT pkDoc PRIMARY KEY, Body MEMO, " + + "Packed MEMO WITH COMPRESSION, Brief TEXT(100) WITH COMPRESSION, Blob LONGBINARY, Big BIGBINARY(500))"; + command.ExecuteNonQuery(); + } + File.Copy(origin, ace, overwrite: true); + File.Copy(origin, libred, overwrite: true); + + string[] sql = statements.Split('|'); + using (OleDbConnection connection = AceTestDatabase.Open(ace)) + foreach (string statement in sql) + { + using OleDbCommand command = connection.CreateCommand(); + command.CommandText = statement; + command.ExecuteNonQuery(); + } + using (var database = JetDatabase.Open(libred, readOnly: false)) + { + var queries = new QueryEngine(database); + foreach (string statement in sql) queries.ExecuteNonQuery(statement); + } + + byte[] a = File.ReadAllBytes(ace), l = File.ReadAllBytes(libred); + var report = new System.Text.StringBuilder($"{statements}\nACE {a.Length / 4096} pages, LibRed {l.Length / 4096} pages\n"); + for (int page = 1; page < Math.Min(a.Length, l.Length) / 4096; page++) + { + int at = page * 4096; + if (!a.AsSpan(at + 4, 4).SequenceEqual("LVAL"u8)) continue; + var ranges = new List(); + for (int i = 0; i < 4096; i++) + { + if (a[at + i] == l[at + i]) continue; + int start = i; + while (i + 1 < 4096 && (a[at + i + 1] != l[at + i + 1] || (i + 2 < 4096 && a[at + i + 2] != l[at + i + 2]))) i++; + ranges.Add($"0x{start:X3}-0x{i:X3} ACE {Convert.ToHexString(a, at + start, Math.Min(8, i - start + 1))}"); + } + int rows = BinaryPrimitives.ReadUInt16LittleEndian(a.AsSpan(at + 12)); + string slots = string.Join(",", Enumerable.Range(0, rows) + .Select(r => $"0x{BinaryPrimitives.ReadUInt16LittleEndian(a.AsSpan(at + 14 + r * 2)) & 0x1FFF:X3}")); + report.AppendLine(CultureInfo.InvariantCulture, + $" LVAL page {page}: ACE rows at [{slots}]; {(ranges.Count == 0 ? "identical" : string.Join("; ", ranges))}"); + } + output.WriteLine(report.ToString()); + } + finally + { + foreach (string path in new[] { origin, ace, libred }) TemporaryDatabase.Delete(path); + } + } + + /// Each long-value column of Doc's one row: its form (inline, single page, chained) and the pages + /// its value occupies, first to last, with the file length. + private static string Describe(string path) + { + using var database = JetDatabase.Open(path, readOnly: true); + Table doc = database.OpenTable("Doc"); + (RowId id, _) = doc.Rows().WithIds().Single(); + var page = new DataPage(); + page.Read(doc.Channel.ReadPageShared(id.Page), doc.Channel.Format); + byte[] row = page.GetRow(id.Row).ToArray(); + var parts = new List(); + foreach ((int index, byte[] descriptor) in RowDecoder.LongValueDescriptors(doc.Definition.Columns, doc.Channel.Format, row) + .OrderBy(d => d.Key)) + { + ColumnDef column = doc.Definition.Columns[index]; + byte flags = (byte)(descriptor[3] & 0xC0); + int length = BinaryPrimitives.ReadInt32LittleEndian(descriptor) & 0x3FFFFFFF; + string where = flags switch + { + 0x80 => "inline", + 0x40 => $"single {Pointer(descriptor.AsSpan(4))}", + _ => $"chained {string.Join(">", Chain(doc, descriptor))}", + }; + parts.Add($"{column.Name}({length}) {where}"); + } + return $"{new FileInfo(path).Length / 4096} pages, row on {id.Page}; " + string.Join("; ", parts); + } + + private static List Chain(Table table, byte[] descriptor) + { + var pages = new List(); + int row = descriptor[4], pageNumber = descriptor[5] | (descriptor[6] << 8) | (descriptor[7] << 16); + while (pageNumber != 0 && pages.Count < 64) + { + pages.Add($"{pageNumber}:{row}"); + var page = new DataPage(); + page.Read(table.Channel.ReadPageShared(pageNumber), table.Channel.Format); + ReadOnlySpan record = page.GetRow(row); + row = record[0]; + pageNumber = record[1] | (record[2] << 8) | (record[3] << 16); + } + return pages; + } + + private static string Pointer(ReadOnlySpan at) => $"{at[1] | (at[2] << 8) | (at[3] << 16)}:{at[0]}"; + + private static string Hex(int bytes) => + Convert.ToHexString([.. Enumerable.Range(0, bytes).Select(i => (byte)((i * 7 + 1) % 251))]); + + private static object? Invoke(object target, string member, params object?[] args) => + target.GetType().InvokeMember(member, System.Reflection.BindingFlags.InvokeMethod, null, target, args); +} diff --git a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs index 02738050..a08fe974 100644 --- a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs @@ -128,7 +128,6 @@ private static readonly (string Statement, string Reason)[] KnownDifferences = [ ("INSERT INTO Bulk (Id, Grp, Label, Payload, Amount) SELECT", "ACE allocates from session extents and 8-page groups (PageGroupProbeTest); LibRed takes the lowest free page"), - ("INSERT INTO Doc VALUES (1,", "ACE writes the row's long values in a different order"), ]; /// A hundred statements over six tables: creates with every ordinary column type and each kind of From 74ce28f0004677e68fdc68deeb21d110d25c6401 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 30 Sep 2026 00:27:09 +0800 Subject: [PATCH 76/88] Test a choice inside arithmetic that takes its literal arm linq2db reported 1000 - CASE WHEN s IS NULL THEN 0 ELSE s END over an empty group coming back as the 0 literal's Int32 under a column declared Decimal. Converting a value that holds a choice anywhere to its declared type already fixed it; nothing tested that shape, where the choice is nested in arithmetic and takes the literal arm. Co-Authored-By: Claude Opus 5.5 --- test/LibRed.Engine.Tests/ResultColumnTypeTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/LibRed.Engine.Tests/ResultColumnTypeTests.cs b/test/LibRed.Engine.Tests/ResultColumnTypeTests.cs index 28a629ac..70269091 100644 --- a/test/LibRed.Engine.Tests/ResultColumnTypeTests.cs +++ b/test/LibRed.Engine.Tests/ResultColumnTypeTests.cs @@ -148,6 +148,8 @@ public void A_scalar_subquery_declares_its_type_through_a_choice() [Theory] // Rows taking different arms still come back in the one declared type. [InlineData("IIF(Id = 1, 9, (SELECT SUM(M) FROM T))", 9, 12.5)] + // ... and a choice inside arithmetic too: linq2db's coalesced sum, taking the 0 arm, came back an Integer. + [InlineData("1000 - IIF(Id = 1, 0, (SELECT SUM(M) FROM T))", 1000, 987.5)] // A correlated subquery is typed without the outer row. [InlineData("(SELECT SUM(i.M) FROM T i WHERE i.Id = o.Id)", 10.5, 2)] public void A_scalar_subquery_column_is_its_type_on_every_row(string expression, double first, double second) From d5b871a77b4967abbab7a55cc988702e4d300cbf Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 30 Sep 2026 01:09:34 +0800 Subject: [PATCH 77/88] Store a query's DISTINCT, TOP and PERCENT on one option row, as ACE does A stored query's DISTINCT, TOP and PERCENT are bits of the one MSysQueries option row (Attribute 3), the TOP count in its Name1: ACE writes DISTINCT TOP 2 as Flag 18 and DISTINCT TOP 25 PERCENT as 50. LibRed had three things wrong there: - PERCENT was never written, so a stored TOP 50 PERCENT came back as TOP 50 and returned every row instead of half. - DISTINCT and TOP went on two rows where ACE writes one. - A make-table or append query's own SELECT lost its DISTINCT and TOP altogether, written and read, so a stored SELECT DISTINCT ... INTO copied every row. Every kind of stored query now writes ACE's single row, and a make-table or append query reads its DISTINCT, DISTINCTROW and TOP back into the SELECT. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/JetCatalog.cs | 15 ++++++- src/LibRed/LibRed.Core/Catalog/ViewSpec.cs | 4 +- src/LibRed/LibRed.Core/Storage/ViewCreator.cs | 26 +++++++---- .../Execution/StatementExecutor.cs | 3 +- src/LibRed/LibRed.Sql/Ast/Statements.cs | 6 ++- src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs | 3 +- src/LibRed/docs/format/system-catalog.md | 6 +++ .../StoredActionQueryWriteAccessTests.cs | 45 +++++++++++++++++++ .../CreateProcedureTests.cs | 9 ++++ 9 files changed, 101 insertions(+), 16 deletions(-) diff --git a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs index 7c343b45..909c9dea 100644 --- a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs +++ b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs @@ -411,6 +411,17 @@ private static StoredActionQuery ReconstructAction( string Where() => where is null ? "" : $" WHERE {where}"; var columns = OfAttr(StoredQueryFormat.AttrColumn); string declared = ParametersClause(rows, attr, flag, n1, order, lvExtra); + // The SELECT of a make-table or an append query keeps its DISTINCT, DISTINCTROW and TOP on the same option row a + // view does, and reads back the same way. + short options = 0; + foreach (object?[] r in OfAttr(StoredQueryFormat.AttrOption)) + if (r[flag] is short o) options |= o; + string? topCount = OfAttr(StoredQueryFormat.AttrOption) + .Where(r => r[flag] is short tf && (tf & StoredQueryFormat.FlagTop) != 0) + .Select(r => r[n1] as string).FirstOrDefault(); + string selecting = ((options & StoredQueryFormat.FlagDistinctRow) != 0 ? "DISTINCTROW " + : (options & StoredQueryFormat.FlagDistinct) != 0 ? "DISTINCT " : "") + + (topCount is null ? "" : $"TOP {topCount} {((options & StoredQueryFormat.FlagPercent) != 0 ? "PERCENT " : "")}"); switch (kind) { @@ -433,7 +444,7 @@ private static StoredActionQuery ReconstructAction( return source is { } appendSource ? new StoredActionQuery( - $"{declared}INSERT INTO {target} ({targetColumns}) SELECT {values} FROM {appendSource.From}{Where()}", null) + $"{declared}INSERT INTO {target} ({targetColumns}) SELECT {selecting}{values} FROM {appendSource.From}{Where()}", null) : new StoredActionQuery(null, "An append query with no values and no source is not executed by LibRed."); } @@ -473,7 +484,7 @@ private static StoredActionQuery ReconstructAction( .Select(r => r[expr] as string).FirstOrDefault(s => !string.IsNullOrEmpty(s)) is { } h ? $" HAVING {h}" : ""; return new StoredActionQuery( - $"{declared}SELECT {selected} INTO {Quote(action[n1] as string ?? "")} FROM {intoSource.From}{Where()}{grouping}{having}", null); + $"{declared}SELECT {selecting}{selected} INTO {Quote(action[n1] as string ?? "")} FROM {intoSource.From}{Where()}{grouping}{having}", null); } } diff --git a/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs b/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs index 7ee68375..65bc33d2 100644 --- a/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs +++ b/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs @@ -89,6 +89,7 @@ public sealed record StoredQueryParameter( /// ORDER BY (all verbatim text) — that Access stores as MSysQueries rows. and /// together are what Access calls a "totals" query: the aggregate output columns /// are ordinary column rows, and only the grouping keys and the group filter get rows of their own. +/// makes a percentage. /// public sealed record ViewSpec( bool Distinct, @@ -100,4 +101,5 @@ public sealed record ViewSpec( string? Having = null, IReadOnlyList? Parameters = null, IReadOnlyList? OrderBy = null, - int? Top = null); \ No newline at end of file + int? Top = null, + bool TopPercent = false); \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs index 5b1f6cb2..976be9fd 100644 --- a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs @@ -167,6 +167,22 @@ private void AddActionRows(int objectId, ActionQuerySpec spec) } AddJoinAndWhereRows(mq, objectId, spec.Body); + // The option row a view carries: a make-table or append query's DISTINCT and TOP. + AddOptionRow(mq, objectId, spec.Body); + } + + /// + /// The query's one 0x03 option row, when it has an option: DISTINCT, TOP and PERCENT are bits of the same + /// row, the TOP count in its Name1 (verified vs ACE: DISTINCT TOP is 18, and with PERCENT 50; a make-table + /// query's DISTINCT is the same row as a view's). + /// + private void AddOptionRow(TableDef mq, int objectId, ViewSpec? body) + { + short options = (short)((body?.Distinct == true ? StoredQueryFormat.FlagDistinct : 0) + | (body?.Top is null ? 0 : StoredQueryFormat.FlagTop | (body.TopPercent ? StoredQueryFormat.FlagPercent : 0))); + if (options != 0) + Row(mq, objectId, StoredQueryFormat.AttrOption, order: 1, flag: options, + name1: body?.Top?.ToString(System.Globalization.CultureInfo.InvariantCulture)); } /// The 0x02 parameter rows, in declaration order — written identically for a view and for @@ -225,15 +241,7 @@ private void AddQueryRows(int objectId, ViewSpec spec) Row(mq, objectId, StoredQueryFormat.AttrEnd, order: 1); // Declared parameters (CREATE PROCEDURE) come right after the End row, before the tables. AddParameterRows(mq, objectId, spec.Parameters); - // DISTINCT and TOP are both StoredQueryFormat.AttrOption (0x03) rows, distinguished by their Flag bits; a TOP row also - // carries the count in Name1. The bits are cumulative, so Access can put both on one row -- writing - // them separately is equally valid and keeps the two spec fields independent here. - // Give them distinct Order values so the composite PK stays unique. - int flagOrder = 1; - if (spec.Distinct) - Row(mq, objectId, StoredQueryFormat.AttrOption, order: flagOrder++, flag: StoredQueryFormat.FlagDistinct); - if (spec.Top is { } top) - Row(mq, objectId, StoredQueryFormat.AttrOption, order: flagOrder++, flag: StoredQueryFormat.FlagTop, name1: top.ToString(System.Globalization.CultureInfo.InvariantCulture)); + AddOptionRow(mq, objectId, spec); AddSourceRows(mq, objectId, spec); for (int i = 0; i < spec.Columns.Count; i++) Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, flag: 0, diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 610c92eb..1db86031 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -803,7 +803,8 @@ private int ExecuteCreateActionProcedure(CreateActionProcedureStatement statemen d.Having, Parameters: null, OrderBy: d.OrderBy.Select(o => new ViewOrderBySpec(o.Expression, o.Descending)).ToList(), - Top: d.Top); + Top: d.Top, + TopPercent: d.TopPercent); /// /// A make-table query: SELECT … INTO newtable FROM source. diff --git a/src/LibRed/LibRed.Sql/Ast/Statements.cs b/src/LibRed/LibRed.Sql/Ast/Statements.cs index f9f7e4ba..4a395e0a 100644 --- a/src/LibRed/LibRed.Sql/Ast/Statements.cs +++ b/src/LibRed/LibRed.Sql/Ast/Statements.cs @@ -230,7 +230,8 @@ public sealed record ViewOrderBy(string Expression, bool Descending); /// A view's decomposed SELECT (columns/tables/joins/where/group-by/having, all as verbatim text), /// which Access stores as MSysQueries rows. A GROUP BY makes it a "totals" query (aggregate columns are /// ordinary column rows; the group-by columns are separate rows), and is that -/// query's own filter over the groups, stored as its own Attribute=0x0A row. +/// query's own filter over the groups, stored as its own Attribute=0x0A row. +/// makes a percentage. public sealed record ViewDefinition( bool Distinct, IReadOnlyList Columns, @@ -240,7 +241,8 @@ public sealed record ViewDefinition( IReadOnlyList GroupBy, string? Having, IReadOnlyList OrderBy, - int? Top); + int? Top, + bool TopPercent = false); /// CREATE VIEW view [(fields)] AS select — a stored query, decomposed for byte-faithful storage. public sealed record CreateViewStatement( diff --git a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs index 355e93b9..2e02c9b0 100644 --- a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs +++ b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs @@ -715,7 +715,8 @@ private static ViewDefinition BuildViewDefinition(QueryExpressionContext ctx) CollectSources(ts, tables, joins); string? where = select.whereClause() is { } w ? OriginalText(w.expression()) : null; - return new ViewDefinition(select.predicate?.DISTINCT() is not null, columns, tables, joins, where, groupBy, having, orderBy, top); + return new ViewDefinition(select.predicate?.DISTINCT() is not null, columns, tables, joins, where, groupBy, having, orderBy, top, + TopPercent: select.topClause()?.percent is not null); } private static void CollectSources(TableSourceContext ts, List tables, List joins) diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index ec479caf..6a6f81fd 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -408,6 +408,12 @@ > `DISTINCT` dedupes output rows, `DISTINCTROW` dedupes by contributing base rows. **`Flag 9` > (`0x08|0x01`) is what Access writes for its auto-generated form/report record-source queries**, the > `~sq_f…` / `~sq_r…` / `~sq_c…` objects, which it renders as `SELECT DISTINCTROW * FROM
`. + > + > **What ACE's own `CREATE VIEW` / `CREATE PROCEDURE` writes is one `0x03` row, `Order 1`, holding every bit the + > query has**, the `TOP` count in its `Name1`: `DISTINCT TOP 2` is `Flag 18`, and `DISTINCT TOP 25 PERCENT` `50`. + > A query with none of them has no `0x03` row. A make-table or append query's own SELECT keeps its + > `DISTINCT` and `TOP` on the same row (measured: `SELECT DISTINCT … INTO`, `SELECT TOP 5 … INTO`, + > `INSERT INTO … SELECT DISTINCT`). LibRed writes the same single row, and reads it back into the statement. > **A query with no `0x05` rows at all has no FROM clause.** `SELECT 1 AS n` is a query Access stores (as > a view or a procedure) and stores exactly as any other, minus the table rows: the type row, one `0x06` diff --git a/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs b/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs index ce27cca5..04a53e77 100644 --- a/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs @@ -25,6 +25,9 @@ private static string Copy() => TemporaryDatabase.CopyPath( [InlineData("DELETE FROM Shippers WHERE ShipperID > 900")] [InlineData("DELETE Shippers.* FROM Shippers WHERE ShipperID > 900")] [InlineData("SELECT ShipperID, CompanyName INTO ShipperCopy FROM Shippers WHERE ShipperID > 1")] + [InlineData("SELECT DISTINCT Country INTO CountryCopy FROM Customers")] + [InlineData("SELECT TOP 5 CompanyName INTO CustomerCopy FROM Customers")] + [InlineData("INSERT INTO Shippers (CompanyName) SELECT DISTINCT Country FROM Customers")] [InlineData("INSERT INTO Shippers (CompanyName, Phone) SELECT CompanyName, Phone FROM Customers WHERE Country = 'UK'")] public void A_libred_written_action_query_stores_the_rows_ace_stores(string body) { @@ -175,6 +178,48 @@ public void A_from_less_body_is_stored_as_ace_stores_it(string sql) } } + /// A stored SELECT's DISTINCT, TOP and PERCENT are bits of one option row, as ACE writes them — and PERCENT + /// survives the round trip: ACE and LibRed return the same rows from the query LibRed stored. + [Theory] + [InlineData("CREATE PROCEDURE [Q] AS SELECT DISTINCT TOP 2 Country FROM Customers ORDER BY Country")] + [InlineData("CREATE PROCEDURE [Q] AS SELECT TOP 10 PERCENT CompanyName FROM Customers ORDER BY CompanyName")] + [InlineData("CREATE PROCEDURE [Q] AS SELECT DISTINCT TOP 25 PERCENT Country FROM Customers ORDER BY Country")] + public void A_queries_options_share_one_row_as_ace_writes_them(string sql) + { + string ourPath = Copy(), acePath = Copy(); + try + { + using (var db = TemporaryDatabase.OpenTracked(ourPath, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery(sql); + + using (var connection = AceTestDatabase.Open(acePath)) + { + using var create = connection.CreateCommand(); + create.CommandText = sql; + create.ExecuteNonQuery(); + } + + Assert.Equal(QueryRows(acePath, "Q"), QueryRows(ourPath, "Q")); + + int aceRows = 0; + using (var connection = AceTestDatabase.Open(ourPath)) + { + using var run = connection.CreateCommand(); + run.CommandText = "Q"; + run.CommandType = CommandType.StoredProcedure; + using var reader = run.ExecuteReader(); + while (reader.Read()) aceRows++; + } + using var ours = TemporaryDatabase.OpenTracked(ourPath, readOnly: true); + Assert.Equal(aceRows, new QueryEngine(ours).ExecuteQuery("SELECT * FROM [Q]").Rows.Count()); + } + finally + { + TemporaryDatabase.Delete(ourPath); + TemporaryDatabase.Delete(acePath); + } + } + [Fact] public void A_written_action_query_reads_back_as_the_statement_it_was_written_from() { diff --git a/test/LibRed.Engine.Tests/CreateProcedureTests.cs b/test/LibRed.Engine.Tests/CreateProcedureTests.cs index cf85949c..9a5cfa85 100644 --- a/test/LibRed.Engine.Tests/CreateProcedureTests.cs +++ b/test/LibRed.Engine.Tests/CreateProcedureTests.cs @@ -231,6 +231,15 @@ public void Unsupported_procedure_body_is_rejected() "SELECT ShipperID, CompanyName INTO [ShipperCopy] FROM [Shippers] WHERE ShipperID > 1", 2)] [InlineData("INSERT INTO Shippers (CompanyName) SELECT ContactName FROM Customers WHERE Country = 'UK'", "INSERT INTO [Shippers] ([CompanyName]) SELECT ContactName FROM [Customers] WHERE Country = 'UK'", 7)] + // A make-table or append query's DISTINCT and TOP [PERCENT] are kept on the option row, and read back. + [InlineData("SELECT DISTINCT Country INTO CountryCopy FROM Customers", + "SELECT DISTINCT Country INTO [CountryCopy] FROM [Customers]", 21)] + [InlineData("SELECT TOP 5 CompanyName INTO CustomerCopy FROM Customers", + "SELECT TOP 5 CompanyName INTO [CustomerCopy] FROM [Customers]", 5)] + [InlineData("SELECT TOP 10 PERCENT CompanyName INTO CustomerCopy FROM Customers", + "SELECT TOP 10 PERCENT CompanyName INTO [CustomerCopy] FROM [Customers]", 10)] + [InlineData("INSERT INTO Shippers (CompanyName) SELECT DISTINCT Country FROM Customers WHERE Country = 'UK'", + "INSERT INTO [Shippers] ([CompanyName]) SELECT DISTINCT Country FROM [Customers] WHERE Country = 'UK'", 1)] public void Action_query_body_round_trips_through_the_file(string body, string expected, int affected) { string path = Fresh(); From 15d3391af685b2a461dce031c84cf83c6462b85b Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 30 Sep 2026 01:11:49 +0800 Subject: [PATCH 78/88] Accept WITH OWNERACCESS OPTION, and keep it in a stored query linq2db asked for Access SQL's WITH OWNERACCESS OPTION, which runs a query with its owner's permissions. LibRed has no users to act for, so a statement with it does exactly what it does without it. It is taken where ACE takes it: at the end of every SELECT, a UNION's arms and subqueries included, after a query's ORDER BY, and at the end of an INSERT, UPDATE or DELETE. It is refused where ACE refuses it: before the query's ORDER BY, twice, incomplete, or on DDL. Neither word is reserved. A stored query keeps it, as ACE does: CREATE VIEW or CREATE PROCEDURE with it sets bit 0x04 of the query's option row, on the DISTINCT or TOP row when there is one, and otherwise on a row of its own. LibRed reads it back as the clause at the end of the rebuilt statement. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/JetCatalog.cs | 13 +- src/LibRed/LibRed.Core/Catalog/ViewSpec.cs | 12 +- src/LibRed/LibRed.Core/Storage/ViewCreator.cs | 16 +- .../Execution/StatementExecutor.cs | 6 +- src/LibRed/LibRed.Sql/Ast/Statements.cs | 10 +- src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 | 17 +- .../Grammar/Generated/AccessSqlBaseVisitor.cs | 10 + .../Grammar/Generated/AccessSqlLexer.cs | 856 ++--- .../Grammar/Generated/AccessSqlParser.cs | 3078 +++++++++-------- .../Grammar/Generated/AccessSqlVisitor.cs | 6 + src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs | 29 +- src/LibRed/README.md | 5 +- src/LibRed/docs/format/system-catalog.md | 8 + .../StoredActionQueryWriteAccessTests.cs | 64 + .../CreateProcedureTests.cs | 2 + test/LibRed.Engine.Tests/OwnerAccessTests.cs | 105 + 16 files changed, 2311 insertions(+), 1926 deletions(-) create mode 100644 test/LibRed.Engine.Tests/OwnerAccessTests.cs diff --git a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs index 909c9dea..cd987f7f 100644 --- a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs +++ b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs @@ -416,6 +416,8 @@ private static StoredActionQuery ReconstructAction( short options = 0; foreach (object?[] r in OfAttr(StoredQueryFormat.AttrOption)) if (r[flag] is short o) options |= o; + // WITH OWNERACCESS OPTION ends every kind of statement, as it ends a SELECT. + string owner = (options & StoredQueryFormat.FlagOwnerAccess) != 0 ? " WITH OWNERACCESS OPTION" : ""; string? topCount = OfAttr(StoredQueryFormat.AttrOption) .Where(r => r[flag] is short tf && (tf & StoredQueryFormat.FlagTop) != 0) .Select(r => r[n1] as string).FirstOrDefault(); @@ -439,12 +441,12 @@ private static StoredActionQuery ReconstructAction( if (columns.All(r => r[flag] is short cf && cf == StoredQueryFormat.AppendValueFlag)) return source is null - ? new StoredActionQuery($"{declared}INSERT INTO {target} ({targetColumns}) VALUES ({values})", null) + ? new StoredActionQuery($"{declared}INSERT INTO {target} ({targetColumns}) VALUES ({values}){owner}", null) : new StoredActionQuery(null, "An append query cannot take both literal values and a source."); return source is { } appendSource ? new StoredActionQuery( - $"{declared}INSERT INTO {target} ({targetColumns}) SELECT {selecting}{values} FROM {appendSource.From}{Where()}", null) + $"{declared}INSERT INTO {target} ({targetColumns}) SELECT {selecting}{values} FROM {appendSource.From}{Where()}{owner}", null) : new StoredActionQuery(null, "An append query with no values and no source is not executed by LibRed."); } @@ -456,7 +458,7 @@ private static StoredActionQuery ReconstructAction( return new StoredActionQuery(null, "An update query with no assignments is not executed by LibRed."); string assignments = string.Join(", ", columns.Select(r => $"{Qualified(r[n2] as string ?? "")} = {r[expr] as string ?? "NULL"}")); - return new StoredActionQuery($"{declared}UPDATE {updateSource.From} SET {assignments}{Where()}", null); + return new StoredActionQuery($"{declared}UPDATE {updateSource.From} SET {assignments}{Where()}{owner}", null); } case StoredQueryFormat.ActionDelete when source is { } deleteSource: @@ -464,7 +466,7 @@ private static StoredActionQuery ReconstructAction( // Access writes `DELETE
.* FROM …` when the query names the table's columns and // `DELETE * FROM …` when it doesn't; the column row holds that `
.*` verbatim. string what = columns.Count > 0 ? columns[0][expr] as string ?? "*" : "*"; - return new StoredActionQuery($"{declared}DELETE {what} FROM {deleteSource.From}{Where()}", null); + return new StoredActionQuery($"{declared}DELETE {what} FROM {deleteSource.From}{Where()}{owner}", null); } case StoredQueryFormat.ActionMakeTable when source is { } intoSource: @@ -484,7 +486,7 @@ private static StoredActionQuery ReconstructAction( .Select(r => r[expr] as string).FirstOrDefault(s => !string.IsNullOrEmpty(s)) is { } h ? $" HAVING {h}" : ""; return new StoredActionQuery( - $"{declared}SELECT {selecting}{selected} INTO {Quote(action[n1] as string ?? "")} FROM {intoSource.From}{Where()}{grouping}{having}", null); + $"{declared}SELECT {selecting}{selected} INTO {Quote(action[n1] as string ?? "")} FROM {intoSource.From}{Where()}{grouping}{having}{owner}", null); } } @@ -709,6 +711,7 @@ private static string Facets(byte code, int? lvExtra) if (groupBy.Count > 0) sql.Append(" GROUP BY ").Append(string.Join(", ", groupBy)); if (having is not null) sql.Append(" HAVING ").Append(having); if (orderBy.Count > 0) sql.Append(" ORDER BY ").Append(string.Join(", ", orderBy)); + if ((options & StoredQueryFormat.FlagOwnerAccess) != 0) sql.Append(" WITH OWNERACCESS OPTION"); return sql.ToString(); } diff --git a/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs b/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs index 65bc33d2..d02eedf3 100644 --- a/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs +++ b/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs @@ -60,7 +60,8 @@ public sealed record AppendColumnSpec(string Column, string ValueExpression); /// does, in , and differs only in the action row and in what its column rows mean: /// holds an append's columns or an update's assignments, /// the table an append or make-table writes into, and the body's own columns are a make-table's output list. -/// are declared exactly as a parameterized SELECT declares them. +/// are declared exactly as a parameterized SELECT declares them, and +/// is WITH OWNERACCESS OPTION, stored as the view stores it. /// public sealed record ActionQuerySpec( ActionQueryKind Kind, @@ -69,7 +70,8 @@ public sealed record ActionQuerySpec( IReadOnlyList? Values = null, ViewSpec? Body = null, IReadOnlyList? Parameters = null, - string? DeleteTarget = null); + string? DeleteTarget = null, + bool OwnerAccess = false); /// A stored action query read back from the catalog. is the reconstructed, /// executable statement when LibRed supports the kind; otherwise it is null and @@ -89,7 +91,8 @@ public sealed record StoredQueryParameter( /// ORDER BY (all verbatim text) — that Access stores as MSysQueries rows. and /// together are what Access calls a "totals" query: the aggregate output columns /// are ordinary column rows, and only the grouping keys and the group filter get rows of their own. -/// makes a percentage. +/// makes a percentage. is WITH +/// OWNERACCESS OPTION: an option-row bit, and nothing LibRed acts on. /// public sealed record ViewSpec( bool Distinct, @@ -102,4 +105,5 @@ public sealed record ViewSpec( IReadOnlyList? Parameters = null, IReadOnlyList? OrderBy = null, int? Top = null, - bool TopPercent = false); \ No newline at end of file + bool TopPercent = false, + bool OwnerAccess = false); \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs index 976be9fd..2a8f3b13 100644 --- a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs @@ -167,18 +167,20 @@ private void AddActionRows(int objectId, ActionQuerySpec spec) } AddJoinAndWhereRows(mq, objectId, spec.Body); - // The option row a view carries: a make-table or append query's DISTINCT and TOP. - AddOptionRow(mq, objectId, spec.Body); + // The option row a view carries: a make-table or append query's DISTINCT and TOP, and any kind's WITH + // OWNERACCESS OPTION. + AddOptionRow(mq, objectId, spec.Body, spec.OwnerAccess); } /// - /// The query's one 0x03 option row, when it has an option: DISTINCT, TOP and PERCENT are bits of the same - /// row, the TOP count in its Name1 (verified vs ACE: DISTINCT TOP is 18, and with PERCENT 50; a make-table - /// query's DISTINCT is the same row as a view's). + /// The query's one 0x03 option row, when it has an option: DISTINCT, WITH OWNERACCESS OPTION, TOP and PERCENT + /// are bits of the same row, the TOP count in its Name1 (verified vs ACE: DISTINCT TOP is 18, with the option 22, + /// and with PERCENT too 54; the option alone is 4; a make-table query's DISTINCT is the same row as a view's). /// - private void AddOptionRow(TableDef mq, int objectId, ViewSpec? body) + private void AddOptionRow(TableDef mq, int objectId, ViewSpec? body, bool ownerAccess) { short options = (short)((body?.Distinct == true ? StoredQueryFormat.FlagDistinct : 0) + | (ownerAccess ? StoredQueryFormat.FlagOwnerAccess : 0) | (body?.Top is null ? 0 : StoredQueryFormat.FlagTop | (body.TopPercent ? StoredQueryFormat.FlagPercent : 0))); if (options != 0) Row(mq, objectId, StoredQueryFormat.AttrOption, order: 1, flag: options, @@ -241,7 +243,7 @@ private void AddQueryRows(int objectId, ViewSpec spec) Row(mq, objectId, StoredQueryFormat.AttrEnd, order: 1); // Declared parameters (CREATE PROCEDURE) come right after the End row, before the tables. AddParameterRows(mq, objectId, spec.Parameters); - AddOptionRow(mq, objectId, spec); + AddOptionRow(mq, objectId, spec, spec.OwnerAccess); AddSourceRows(mq, objectId, spec); for (int i = 0; i < spec.Columns.Count; i++) Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, flag: 0, diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 1db86031..3587a1ba 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -785,7 +785,8 @@ private int ExecuteCreateActionProcedure(CreateActionProcedureStatement statemen Values: statement.AppendColumns?.Select(c => new AppendColumnSpec(c.Column, c.ValueExpression)).ToList(), Body: statement.Body is { } body ? BuildViewSpec(body) : null, Parameters: BuildParameterSpecs(statement.Parameters), - DeleteTarget: statement.DeleteTarget); + DeleteTarget: statement.DeleteTarget, + OwnerAccess: statement.OwnerAccess); _database.CreateActionQuery(statement.Name, spec); return 0; @@ -804,7 +805,8 @@ private int ExecuteCreateActionProcedure(CreateActionProcedureStatement statemen Parameters: null, OrderBy: d.OrderBy.Select(o => new ViewOrderBySpec(o.Expression, o.Descending)).ToList(), Top: d.Top, - TopPercent: d.TopPercent); + TopPercent: d.TopPercent, + OwnerAccess: d.OwnerAccess); /// /// A make-table query: SELECT … INTO newtable FROM source. diff --git a/src/LibRed/LibRed.Sql/Ast/Statements.cs b/src/LibRed/LibRed.Sql/Ast/Statements.cs index 4a395e0a..cae5f67d 100644 --- a/src/LibRed/LibRed.Sql/Ast/Statements.cs +++ b/src/LibRed/LibRed.Sql/Ast/Statements.cs @@ -231,7 +231,8 @@ public sealed record ViewOrderBy(string Expression, bool Descending); /// which Access stores as MSysQueries rows. A GROUP BY makes it a "totals" query (aggregate columns are /// ordinary column rows; the group-by columns are separate rows), and is that /// query's own filter over the groups, stored as its own Attribute=0x0A row. -/// makes a percentage. +/// makes a percentage, and is +/// WITH OWNERACCESS OPTION, which a stored query keeps. public sealed record ViewDefinition( bool Distinct, IReadOnlyList Columns, @@ -242,7 +243,8 @@ public sealed record ViewDefinition( string? Having, IReadOnlyList OrderBy, int? Top, - bool TopPercent = false); + bool TopPercent = false, + bool OwnerAccess = false); /// CREATE VIEW view [(fields)] AS select — a stored query, decomposed for byte-faithful storage. public sealed record CreateViewStatement( @@ -282,6 +284,7 @@ public sealed record AppendColumn(string Column, string ValueExpression); /// are an INSERT's columns or an UPDATE's assignments, /// is the table an INSERT or a make-table writes into, and /// is the table.* a DELETE names when it names one. +/// is WITH OWNERACCESS OPTION, which the stored query keeps. /// public sealed record CreateActionProcedureStatement( string Name, @@ -291,7 +294,8 @@ public sealed record CreateActionProcedureStatement( IReadOnlyList? AppendColumns, ViewDefinition? Body = null, string? DeleteTarget = null, - IReadOnlyList? Parameters = null) : SqlStatement; + IReadOnlyList? Parameters = null, + bool OwnerAccess = false) : SqlStatement; /// One action of an ALTER TABLE statement (Access allows exactly one per statement). public abstract record AlterTableAction; diff --git a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 index 7408b705..e9d8cd16 100644 --- a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 +++ b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 @@ -52,14 +52,14 @@ executeStatement : (EXECUTE | EXEC) name=identifier (expression (COMMA expressio // UPDATE tableexpression SET col=expr, … [WHERE …]. The tableexpression is a table SOURCE (Access allows a // join here) or, as in a FROM clause, a comma list of them (verified vs ACE: UPDATE a, b SET … is accepted), and a // SET target may be table-qualified (col or alias.col) to touch a specific joined table. -updateStatement : UPDATE tableSource (COMMA tableSource)* SET assignment (COMMA assignment)* whereClause? ; +updateStatement : UPDATE tableSource (COMMA tableSource)* SET assignment (COMMA assignment)* whereClause? ownerAccessOption? ; assignment : target=columnRef EQ expression ; // DELETE [table.* | *] FROM tableexpression [WHERE …]. For a join, the `table.*` target selects which // table's rows to delete; a bare `*` (or no target) is only valid for a single table — a join without a // `table.*` target is ambiguous and rejected at execution (matching Access, which asks you to specify it). // As in a FROM clause, the tableexpression may be a comma list of sources (verified vs ACE). -deleteStatement : DELETE (target=identifier DOT STAR | STAR)? FROM tableSource (COMMA tableSource)* whereClause? ; +deleteStatement : DELETE (target=identifier DOT STAR | STAR)? FROM tableSource (COMMA tableSource)* whereClause? ownerAccessOption? ; // A FROM-less SELECT of system variables only — ACE allows `SELECT @@IDENTITY` / `SELECT @@ROWCOUNT` // (and a comma list of them) with no FROM clause. Listed before queryExpression so it is preferred; a @@ -278,7 +278,13 @@ rowValue : DEFAULT | expression ; // is what gives `(SELECT TOP 5 … ORDER BY x) UNION …` its meaning — the ordering is what makes that TOP // deterministic. This is the standard's own structure: carries the ordering, // does not. -queryExpression : queryTerm (setOperator queryTerm)* orderByClause? offsetFetchClause? ; +queryExpression : queryTerm (setOperator queryTerm)* orderByClause? offsetFetchClause? ownerAccessOption? ; + +// WITH OWNERACCESS OPTION: run the query with its owner's permissions. LibRed has no users to act for, so it +// changes nothing when a query runs; a stored query keeps it (StoredQueryFormat.FlagOwnerAccess). ACE takes it +// at the end of every SELECT — the first of a UNION's and a subquery's too — after a query's ORDER BY but not +// before it, and at the end of an INSERT, UPDATE or DELETE (verified). +ownerAccessOption : WITH OWNERACCESS OPTION ; // A set-operation operand is an order-less SELECT or a parenthesised query expression (so `A UNION ALL (B UNION // C)` groups the right side as one term — EF emits this from Concat/Union nesting). queryTerm @@ -307,6 +313,7 @@ setOperator : UNION ALL? | INTERSECT | EXCEPT ; // `SELECT TOP 5 … UNION SELECT TOP 5 …` takes five rows from each side. querySpecification : SELECT predicate=selectPredicate? topClause? selectList (INTO into=identifier)? fromClause? whereClause? groupByClause? havingClause? + ownerAccessOption? ; // The optional row predicate. ALL is the default (return every row); DISTINCT dedupes on the output @@ -581,6 +588,7 @@ nonReservedKeyword : RANGE | GROUPS | UNBOUNDED | PRECEDING | FOLLOWING | CURRENT | EXCLUDE | TIES | OTHERS | WITHIN | LAST | RESPECT | NULLS | FILTER | YES | OFF + | OWNERACCESS | OPTION ; // Every other keyword, which a name may be only after a separator — see columnRef. @@ -742,6 +750,9 @@ LAST : [Ll][Aa][Ss][Tt] ; RESPECT : [Rr][Ee][Ss][Pp][Ee][Cc][Tt] ; NULLS : [Nn][Uu][Ll][Ll][Ss] ; FILTER : [Ff][Ii][Ll][Tt][Ee][Rr] ; +// WITH OWNERACCESS OPTION's words — not reserved; see nonReservedKeyword. +OWNERACCESS : [Oo][Ww][Nn][Ee][Rr][Aa][Cc][Cc][Ee][Ss][Ss] ; +OPTION : [Oo][Pp][Tt][Ii][Oo][Nn] ; STAR : '*' ; SLASH : '/' ; diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs index c2e6fb71..3713ff6f 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs @@ -733,6 +733,16 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result. public virtual Result VisitQueryExpression([NotNull] AccessSqlParser.QueryExpressionContext context) { return VisitChildren(context); } /// + /// Visit a parse tree produced by . + /// + /// The default implementation returns the result of calling + /// on . + /// + /// + /// The parse tree. + /// The visitor result. + public virtual Result VisitOwnerAccessOption([NotNull] AccessSqlParser.OwnerAccessOptionContext context) { return VisitChildren(context); } + /// /// Visit a parse tree produced by the SelectTerm /// labeled alternative in . /// diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs index 3dd4e265..f33e1ef3 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs @@ -51,12 +51,13 @@ public const int ASC=103, DESC=104, TRUE=105, FALSE=106, YES=107, OFF=108, NULL=109, RANGE=110, GROUPS=111, UNBOUNDED=112, PRECEDING=113, FOLLOWING=114, CURRENT=115, EXCLUDE=116, TIES=117, OTHERS=118, WITHIN=119, LAST=120, RESPECT=121, - NULLS=122, FILTER=123, STAR=124, SLASH=125, BACKSLASH=126, CARET=127, - PLUS=128, MINUS=129, AMP=130, EQ=131, NEQ=132, LTE=133, GTE=134, LT=135, - GT=136, LPAREN=137, RPAREN=138, COMMA=139, DOT=140, BANG=141, SEMI=142, - SYSVAR=143, PARAM=144, HEX_LITERAL=145, INTEGER_LITERAL=146, NUMBER_LITERAL=147, - STRING_LITERAL=148, DATE_LITERAL=149, GUID_LITERAL=150, BRACKET_ID=151, - BACKTICK_ID=152, IDENTIFIER=153, WS=154, LINE_COMMENT=155, BLOCK_COMMENT=156; + NULLS=122, FILTER=123, OWNERACCESS=124, OPTION=125, STAR=126, SLASH=127, + BACKSLASH=128, CARET=129, PLUS=130, MINUS=131, AMP=132, EQ=133, NEQ=134, + LTE=135, GTE=136, LT=137, GT=138, LPAREN=139, RPAREN=140, COMMA=141, DOT=142, + BANG=143, SEMI=144, SYSVAR=145, PARAM=146, HEX_LITERAL=147, INTEGER_LITERAL=148, + NUMBER_LITERAL=149, STRING_LITERAL=150, DATE_LITERAL=151, GUID_LITERAL=152, + BRACKET_ID=153, BACKTICK_ID=154, IDENTIFIER=155, WS=156, LINE_COMMENT=157, + BLOCK_COMMENT=158; public static string[] channelNames = { "DEFAULT_TOKEN_CHANNEL", "HIDDEN" }; @@ -81,12 +82,13 @@ public const int "CHECK", "VIEW", "PROCEDURE", "PARAMETERS", "EXECUTE", "EXEC", "ASC", "DESC", "TRUE", "FALSE", "YES", "OFF", "NULL", "RANGE", "GROUPS", "UNBOUNDED", "PRECEDING", "FOLLOWING", "CURRENT", "EXCLUDE", "TIES", "OTHERS", "WITHIN", - "LAST", "RESPECT", "NULLS", "FILTER", "STAR", "SLASH", "BACKSLASH", "CARET", - "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", "LT", "GT", "LPAREN", - "RPAREN", "COMMA", "DOT", "BANG", "SEMI", "SYSVAR", "PARAM", "HEX_LITERAL", - "INTEGER_LITERAL", "NUMBER_LITERAL", "EXPONENT", "STRING_LITERAL", "DATE_LITERAL", - "GUID_LITERAL", "GUID_BODY", "HEXDIGIT", "BRACKET_ID", "BACKTICK_ID", - "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT" + "LAST", "RESPECT", "NULLS", "FILTER", "OWNERACCESS", "OPTION", "STAR", + "SLASH", "BACKSLASH", "CARET", "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", + "GTE", "LT", "GT", "LPAREN", "RPAREN", "COMMA", "DOT", "BANG", "SEMI", + "SYSVAR", "PARAM", "HEX_LITERAL", "INTEGER_LITERAL", "NUMBER_LITERAL", + "EXPONENT", "STRING_LITERAL", "DATE_LITERAL", "GUID_LITERAL", "GUID_BODY", + "HEXDIGIT", "BRACKET_ID", "BACKTICK_ID", "IDENTIFIER", "WS", "LINE_COMMENT", + "BLOCK_COMMENT" }; @@ -121,9 +123,9 @@ 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, null, null, null, null, "'*'", "'/'", "'\\'", "'^'", "'+'", + "'-'", "'&'", "'='", null, "'<='", "'>='", "'<'", "'>'", "'('", "')'", + "','", "'.'", "'!'", "';'" }; private static readonly string[] _SymbolicNames = { null, "SELECT", "FROM", "WHERE", "TOP", "AS", "AND", "OR", "NOT", "XOR", @@ -141,12 +143,12 @@ public AccessSqlLexer(ICharStream input, TextWriter output, TextWriter errorOutp "CHECK", "VIEW", "PROCEDURE", "PARAMETERS", "EXECUTE", "EXEC", "ASC", "DESC", "TRUE", "FALSE", "YES", "OFF", "NULL", "RANGE", "GROUPS", "UNBOUNDED", "PRECEDING", "FOLLOWING", "CURRENT", "EXCLUDE", "TIES", "OTHERS", "WITHIN", - "LAST", "RESPECT", "NULLS", "FILTER", "STAR", "SLASH", "BACKSLASH", "CARET", - "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", "LT", "GT", "LPAREN", - "RPAREN", "COMMA", "DOT", "BANG", "SEMI", "SYSVAR", "PARAM", "HEX_LITERAL", - "INTEGER_LITERAL", "NUMBER_LITERAL", "STRING_LITERAL", "DATE_LITERAL", - "GUID_LITERAL", "BRACKET_ID", "BACKTICK_ID", "IDENTIFIER", "WS", "LINE_COMMENT", - "BLOCK_COMMENT" + "LAST", "RESPECT", "NULLS", "FILTER", "OWNERACCESS", "OPTION", "STAR", + "SLASH", "BACKSLASH", "CARET", "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", + "GTE", "LT", "GT", "LPAREN", "RPAREN", "COMMA", "DOT", "BANG", "SEMI", + "SYSVAR", "PARAM", "HEX_LITERAL", "INTEGER_LITERAL", "NUMBER_LITERAL", + "STRING_LITERAL", "DATE_LITERAL", "GUID_LITERAL", "BRACKET_ID", "BACKTICK_ID", + "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT" }; public static readonly IVocabulary DefaultVocabulary = new Vocabulary(_LiteralNames, _SymbolicNames); @@ -177,7 +179,7 @@ static AccessSqlLexer() { } public override bool Sempred(RuleContext _localctx, int ruleIndex, int predIndex) { switch (ruleIndex) { - case 157 : return LINE_COMMENT_sempred(_localctx, predIndex); + case 159 : return LINE_COMMENT_sempred(_localctx, predIndex); } return true; } @@ -189,7 +191,7 @@ private bool LINE_COMMENT_sempred(RuleContext _localctx, int predIndex) { } private static int[] _serializedATN = { - 4,0,156,1362,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,158,1385,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, @@ -213,106 +215,108 @@ private bool LINE_COMMENT_sempred(RuleContext _localctx, int predIndex) { 7,140,2,141,7,141,2,142,7,142,2,143,7,143,2,144,7,144,2,145,7,145,2,146, 7,146,2,147,7,147,2,148,7,148,2,149,7,149,2,150,7,150,2,151,7,151,2,152, 7,152,2,153,7,153,2,154,7,154,2,155,7,155,2,156,7,156,2,157,7,157,2,158, - 7,158,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,9,1,9,1,9,1,9,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,13,1,13,1,13,1,13,1,13,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,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,19,1,19, - 1,19,1,20,1,20,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,23,1,23,1,23,1,24,1,24,1,24,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,27,1,27,1,27,1,28,1,28,1,28,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,31, - 1,31,1,31,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,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34,1,34,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,36,1,36,1,36,1,36,1,36,1,36, - 1,37,1,37,1,37,1,37,1,37,1,37,1,38,1,38,1,38,1,38,1,38,1,39,1,39,1,39, - 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40,1,41,1,41, - 1,41,1,41,1,41,1,42,1,42,1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,44,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,46,1,46,1,46, - 1,46,1,46,1,47,1,47,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,50,1,50,1,50,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1,51, - 1,51,1,51,1,52,1,52,1,52,1,52,1,52,1,52,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,54,1,54,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,57,1,57,1,57,1,57,1,57,1,57, - 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,60,1,61,1,61,1,61,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,63,1,63,1,63, - 1,63,1,63,1,63,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,67,1,67,1,67,1,67,1,67,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,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,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72, - 1,73,1,73,1,73,1,73,1,74,1,74,1,74,1,74,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,75,1,75,1,76,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,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,80,1,80,1,80,1,80,1,81,1,81,1,81,1,81,1,81, - 1,81,1,81,1,82,1,82,1,82,1,82,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83, - 1,84,1,84,1,84,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,86,1,86,1,86,1,86, - 1,86,1,86,1,86,1,86,1,86,1,86,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87, - 1,87,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88, - 1,89,1,89,1,89,1,89,1,89,1,89,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90, - 1,90,1,90,1,91,1,91,1,91,1,91,1,91,1,92,1,92,1,92,1,92,1,92,1,92,1,92, - 1,92,1,92,1,92,1,92,1,92,1,93,1,93,1,93,1,93,1,93,1,94,1,94,1,94,1,94, - 1,94,1,94,1,94,1,94,1,94,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,96,1,96, - 1,96,1,96,1,96,1,96,1,97,1,97,1,97,1,97,1,97,1,98,1,98,1,98,1,98,1,98, - 1,98,1,98,1,98,1,98,1,98,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99, - 1,99,1,99,1,100,1,100,1,100,1,100,1,100,1,100,1,100,1,100,1,101,1,101, - 1,101,1,101,1,101,1,102,1,102,1,102,1,102,1,103,1,103,1,103,1,103,1,103, - 1,104,1,104,1,104,1,104,1,104,1,105,1,105,1,105,1,105,1,105,1,105,1,106, - 1,106,1,106,1,106,1,107,1,107,1,107,1,107,1,108,1,108,1,108,1,108,1,108, - 1,109,1,109,1,109,1,109,1,109,1,109,1,110,1,110,1,110,1,110,1,110,1,110, - 1,110,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,112, - 1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,113,1,113,1,113, - 1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,114,1,114,1,114,1,114,1,114, - 1,114,1,114,1,114,1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,116, - 1,116,1,116,1,116,1,116,1,117,1,117,1,117,1,117,1,117,1,117,1,117,1,118, - 1,118,1,118,1,118,1,118,1,118,1,118,1,119,1,119,1,119,1,119,1,119,1,120, - 1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,121,1,121,1,121,1,121,1,121, - 1,121,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,123,1,123,1,124,1,124, - 1,125,1,125,1,126,1,126,1,127,1,127,1,128,1,128,1,129,1,129,1,130,1,130, - 1,131,1,131,1,131,1,131,3,131,1116,8,131,1,132,1,132,1,132,1,133,1,133, - 1,133,1,134,1,134,1,135,1,135,1,136,1,136,1,137,1,137,1,138,1,138,1,139, - 1,139,1,140,1,140,1,141,1,141,1,142,1,142,1,142,1,142,1,142,5,142,1145, - 8,142,10,142,12,142,1148,9,142,1,143,1,143,1,143,1,143,5,143,1154,8,143, - 10,143,12,143,1157,9,143,3,143,1159,8,143,1,144,1,144,1,144,4,144,1164, - 8,144,11,144,12,144,1165,1,145,4,145,1169,8,145,11,145,12,145,1170,1,146, - 4,146,1174,8,146,11,146,12,146,1175,1,146,1,146,5,146,1180,8,146,10,146, - 12,146,1183,9,146,1,146,3,146,1186,8,146,1,146,1,146,4,146,1190,8,146, - 11,146,12,146,1191,1,146,3,146,1195,8,146,1,146,4,146,1198,8,146,11,146, - 12,146,1199,1,146,3,146,1203,8,146,1,147,1,147,3,147,1207,8,147,1,147, - 4,147,1210,8,147,11,147,12,147,1211,1,148,1,148,1,148,1,148,5,148,1218, - 8,148,10,148,12,148,1221,9,148,1,148,1,148,1,148,1,148,1,148,5,148,1228, - 8,148,10,148,12,148,1231,9,148,1,148,3,148,1234,8,148,1,149,1,149,5,149, - 1238,8,149,10,149,12,149,1241,9,149,1,149,1,149,1,150,1,150,1,150,1,150, - 1,150,1,150,1,150,1,150,1,150,1,150,5,150,1255,8,150,10,150,12,150,1258, - 9,150,1,150,1,150,1,150,1,150,5,150,1264,8,150,10,150,12,150,1267,9,150, - 1,150,1,150,3,150,1271,8,150,1,151,4,151,1274,8,151,11,151,12,151,1275, - 1,151,1,151,4,151,1280,8,151,11,151,12,151,1281,1,151,1,151,4,151,1286, - 8,151,11,151,12,151,1287,1,151,1,151,4,151,1292,8,151,11,151,12,151,1293, - 1,151,1,151,4,151,1298,8,151,11,151,12,151,1299,1,152,1,152,1,153,1,153, - 4,153,1306,8,153,11,153,12,153,1307,1,153,1,153,1,154,1,154,4,154,1314, - 8,154,11,154,12,154,1315,1,154,1,154,1,155,1,155,5,155,1322,8,155,10,155, - 12,155,1325,9,155,1,155,3,155,1328,8,155,1,156,4,156,1331,8,156,11,156, - 12,156,1332,1,156,1,156,1,157,1,157,1,157,1,157,1,157,5,157,1342,8,157, - 10,157,12,157,1345,9,157,1,157,1,157,1,158,1,158,1,158,1,158,5,158,1353, - 8,158,10,158,12,158,1356,9,158,1,158,1,158,1,158,1,158,1,158,1,1354,0, - 159,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,110,221,111,223,112,225,113,227,114,229,115,231,116,233,117,235, - 118,237,119,239,120,241,121,243,122,245,123,247,124,249,125,251,126,253, - 127,255,128,257,129,259,130,261,131,263,132,265,133,267,134,269,135,271, - 136,273,137,275,138,277,139,279,140,281,141,283,142,285,143,287,144,289, - 145,291,146,293,147,295,0,297,148,299,149,301,150,303,0,305,0,307,151, - 309,152,311,153,313,154,315,155,317,156,1,0,40,2,0,83,83,115,115,2,0,69, + 7,158,2,159,7,159,2,160,7,160,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,9,1,9,1,9,1,9,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,13,1,13, + 1,13,1,13,1,13,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,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,19,1,19,1,19,1,20,1,20,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,23,1,23,1,23,1,24,1,24,1,24, + 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,27,1,27, + 1,27,1,28,1,28,1,28,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,31,1,31,1,31,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,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34, + 1,34,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,36, + 1,36,1,36,1,36,1,36,1,36,1,37,1,37,1,37,1,37,1,37,1,37,1,38,1,38,1,38, + 1,38,1,38,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,40,1,40, + 1,40,1,40,1,40,1,41,1,41,1,41,1,41,1,41,1,42,1,42,1,42,1,42,1,42,1,43, + 1,43,1,43,1,43,1,44,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,46,1,46,1,46,1,46,1,46,1,47,1,47,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,50,1,50,1,50,1,50,1,50,1,51, + 1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,52,1,52,1,52,1,52,1,52,1,52,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,54,1,54,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,57, + 1,57,1,57,1,57,1,57,1,57,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,60,1,61, + 1,61,1,61,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,63,1,63,1,63,1,63,1,63,1,63,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,67,1,67,1,67,1,67,1,67,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,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,72,1,72,1,72, + 1,72,1,72,1,72,1,72,1,72,1,73,1,73,1,73,1,73,1,74,1,74,1,74,1,74,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,75,1,75, + 1,76,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,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,80,1,80,1,80,1,80, + 1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,82,1,82,1,82,1,82,1,83,1,83,1,83, + 1,83,1,83,1,83,1,83,1,83,1,84,1,84,1,84,1,85,1,85,1,85,1,85,1,85,1,85, + 1,85,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,87,1,87,1,87, + 1,87,1,87,1,87,1,87,1,87,1,87,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88, + 1,88,1,88,1,88,1,88,1,88,1,89,1,89,1,89,1,89,1,89,1,89,1,90,1,90,1,90, + 1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,91,1,91,1,91,1,91,1,91,1,92,1,92, + 1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,93,1,93,1,93,1,93, + 1,93,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,95,1,95,1,95,1,95, + 1,95,1,95,1,95,1,96,1,96,1,96,1,96,1,96,1,96,1,97,1,97,1,97,1,97,1,97, + 1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,99,1,99,1,99,1,99, + 1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,100,1,100,1,100,1,100,1,100,1,100, + 1,100,1,100,1,101,1,101,1,101,1,101,1,101,1,102,1,102,1,102,1,102,1,103, + 1,103,1,103,1,103,1,103,1,104,1,104,1,104,1,104,1,104,1,105,1,105,1,105, + 1,105,1,105,1,105,1,106,1,106,1,106,1,106,1,107,1,107,1,107,1,107,1,108, + 1,108,1,108,1,108,1,108,1,109,1,109,1,109,1,109,1,109,1,109,1,110,1,110, + 1,110,1,110,1,110,1,110,1,110,1,111,1,111,1,111,1,111,1,111,1,111,1,111, + 1,111,1,111,1,111,1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,112, + 1,112,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,114, + 1,114,1,114,1,114,1,114,1,114,1,114,1,114,1,115,1,115,1,115,1,115,1,115, + 1,115,1,115,1,115,1,116,1,116,1,116,1,116,1,116,1,117,1,117,1,117,1,117, + 1,117,1,117,1,117,1,118,1,118,1,118,1,118,1,118,1,118,1,118,1,119,1,119, + 1,119,1,119,1,119,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,121, + 1,121,1,121,1,121,1,121,1,121,1,122,1,122,1,122,1,122,1,122,1,122,1,122, + 1,123,1,123,1,123,1,123,1,123,1,123,1,123,1,123,1,123,1,123,1,123,1,123, + 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,125,1,125,1,126,1,126,1,127, + 1,127,1,128,1,128,1,129,1,129,1,130,1,130,1,131,1,131,1,132,1,132,1,133, + 1,133,1,133,1,133,3,133,1139,8,133,1,134,1,134,1,134,1,135,1,135,1,135, + 1,136,1,136,1,137,1,137,1,138,1,138,1,139,1,139,1,140,1,140,1,141,1,141, + 1,142,1,142,1,143,1,143,1,144,1,144,1,144,1,144,1,144,5,144,1168,8,144, + 10,144,12,144,1171,9,144,1,145,1,145,1,145,1,145,5,145,1177,8,145,10,145, + 12,145,1180,9,145,3,145,1182,8,145,1,146,1,146,1,146,4,146,1187,8,146, + 11,146,12,146,1188,1,147,4,147,1192,8,147,11,147,12,147,1193,1,148,4,148, + 1197,8,148,11,148,12,148,1198,1,148,1,148,5,148,1203,8,148,10,148,12,148, + 1206,9,148,1,148,3,148,1209,8,148,1,148,1,148,4,148,1213,8,148,11,148, + 12,148,1214,1,148,3,148,1218,8,148,1,148,4,148,1221,8,148,11,148,12,148, + 1222,1,148,3,148,1226,8,148,1,149,1,149,3,149,1230,8,149,1,149,4,149,1233, + 8,149,11,149,12,149,1234,1,150,1,150,1,150,1,150,5,150,1241,8,150,10,150, + 12,150,1244,9,150,1,150,1,150,1,150,1,150,1,150,5,150,1251,8,150,10,150, + 12,150,1254,9,150,1,150,3,150,1257,8,150,1,151,1,151,5,151,1261,8,151, + 10,151,12,151,1264,9,151,1,151,1,151,1,152,1,152,1,152,1,152,1,152,1,152, + 1,152,1,152,1,152,1,152,5,152,1278,8,152,10,152,12,152,1281,9,152,1,152, + 1,152,1,152,1,152,5,152,1287,8,152,10,152,12,152,1290,9,152,1,152,1,152, + 3,152,1294,8,152,1,153,4,153,1297,8,153,11,153,12,153,1298,1,153,1,153, + 4,153,1303,8,153,11,153,12,153,1304,1,153,1,153,4,153,1309,8,153,11,153, + 12,153,1310,1,153,1,153,4,153,1315,8,153,11,153,12,153,1316,1,153,1,153, + 4,153,1321,8,153,11,153,12,153,1322,1,154,1,154,1,155,1,155,4,155,1329, + 8,155,11,155,12,155,1330,1,155,1,155,1,156,1,156,4,156,1337,8,156,11,156, + 12,156,1338,1,156,1,156,1,157,1,157,5,157,1345,8,157,10,157,12,157,1348, + 9,157,1,157,3,157,1351,8,157,1,158,4,158,1354,8,158,11,158,12,158,1355, + 1,158,1,158,1,159,1,159,1,159,1,159,1,159,5,159,1365,8,159,10,159,12,159, + 1368,9,159,1,159,1,159,1,160,1,160,1,160,1,160,5,160,1376,8,160,10,160, + 12,160,1379,9,160,1,160,1,160,1,160,1,160,1,160,1,1377,0,161,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,110,221, + 111,223,112,225,113,227,114,229,115,231,116,233,117,235,118,237,119,239, + 120,241,121,243,122,245,123,247,124,249,125,251,126,253,127,255,128,257, + 129,259,130,261,131,263,132,265,133,267,134,269,135,271,136,273,137,275, + 138,277,139,279,140,281,141,283,142,285,143,287,144,289,145,291,146,293, + 147,295,148,297,149,299,0,301,150,303,151,305,152,307,0,309,0,311,153, + 313,154,315,155,317,156,319,157,321,158,1,0,40,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, @@ -550,7 +554,7 @@ private bool LINE_COMMENT_sempred(RuleContext _localctx, int predIndex) { 126590,126590,126592,126601,126603,126619,126625,126627,126629,126633, 126635,126651,130032,130041,131072,173791,173824,177977,177984,178205, 178208,183969,183984,191456,194560,195101,196608,201546,201552,205743, - 917760,917999,3,0,9,10,13,13,32,32,2,0,10,10,13,13,1395,0,1,1,0,0,0,0, + 917760,917999,3,0,9,10,13,13,32,32,2,0,10,10,13,13,1418,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, @@ -579,305 +583,311 @@ private bool LINE_COMMENT_sempred(RuleContext _localctx, int predIndex) { 0,0,0,263,1,0,0,0,0,265,1,0,0,0,0,267,1,0,0,0,0,269,1,0,0,0,0,271,1,0, 0,0,0,273,1,0,0,0,0,275,1,0,0,0,0,277,1,0,0,0,0,279,1,0,0,0,0,281,1,0, 0,0,0,283,1,0,0,0,0,285,1,0,0,0,0,287,1,0,0,0,0,289,1,0,0,0,0,291,1,0, - 0,0,0,293,1,0,0,0,0,297,1,0,0,0,0,299,1,0,0,0,0,301,1,0,0,0,0,307,1,0, - 0,0,0,309,1,0,0,0,0,311,1,0,0,0,0,313,1,0,0,0,0,315,1,0,0,0,0,317,1,0, - 0,0,1,319,1,0,0,0,3,326,1,0,0,0,5,331,1,0,0,0,7,337,1,0,0,0,9,341,1,0, - 0,0,11,344,1,0,0,0,13,348,1,0,0,0,15,351,1,0,0,0,17,355,1,0,0,0,19,359, - 1,0,0,0,21,363,1,0,0,0,23,367,1,0,0,0,25,372,1,0,0,0,27,376,1,0,0,0,29, - 381,1,0,0,0,31,386,1,0,0,0,33,391,1,0,0,0,35,395,1,0,0,0,37,401,1,0,0, - 0,39,406,1,0,0,0,41,412,1,0,0,0,43,417,1,0,0,0,45,423,1,0,0,0,47,428,1, - 0,0,0,49,431,1,0,0,0,51,434,1,0,0,0,53,440,1,0,0,0,55,446,1,0,0,0,57,449, - 1,0,0,0,59,452,1,0,0,0,61,459,1,0,0,0,63,466,1,0,0,0,65,469,1,0,0,0,67, - 474,1,0,0,0,69,486,1,0,0,0,71,495,1,0,0,0,73,503,1,0,0,0,75,509,1,0,0, - 0,77,515,1,0,0,0,79,520,1,0,0,0,81,530,1,0,0,0,83,535,1,0,0,0,85,540,1, - 0,0,0,87,545,1,0,0,0,89,549,1,0,0,0,91,556,1,0,0,0,93,562,1,0,0,0,95,567, - 1,0,0,0,97,573,1,0,0,0,99,578,1,0,0,0,101,582,1,0,0,0,103,587,1,0,0,0, - 105,595,1,0,0,0,107,601,1,0,0,0,109,605,1,0,0,0,111,615,1,0,0,0,113,622, - 1,0,0,0,115,629,1,0,0,0,117,635,1,0,0,0,119,641,1,0,0,0,121,648,1,0,0, - 0,123,657,1,0,0,0,125,669,1,0,0,0,127,674,1,0,0,0,129,680,1,0,0,0,131, - 687,1,0,0,0,133,690,1,0,0,0,135,694,1,0,0,0,137,699,1,0,0,0,139,706,1, - 0,0,0,141,713,1,0,0,0,143,718,1,0,0,0,145,725,1,0,0,0,147,733,1,0,0,0, - 149,737,1,0,0,0,151,748,1,0,0,0,153,756,1,0,0,0,155,767,1,0,0,0,157,774, - 1,0,0,0,159,781,1,0,0,0,161,789,1,0,0,0,163,798,1,0,0,0,165,805,1,0,0, - 0,167,809,1,0,0,0,169,817,1,0,0,0,171,820,1,0,0,0,173,827,1,0,0,0,175, - 837,1,0,0,0,177,846,1,0,0,0,179,859,1,0,0,0,181,865,1,0,0,0,183,875,1, - 0,0,0,185,880,1,0,0,0,187,892,1,0,0,0,189,897,1,0,0,0,191,906,1,0,0,0, - 193,913,1,0,0,0,195,919,1,0,0,0,197,924,1,0,0,0,199,934,1,0,0,0,201,945, - 1,0,0,0,203,953,1,0,0,0,205,958,1,0,0,0,207,962,1,0,0,0,209,967,1,0,0, - 0,211,972,1,0,0,0,213,978,1,0,0,0,215,982,1,0,0,0,217,986,1,0,0,0,219, - 991,1,0,0,0,221,997,1,0,0,0,223,1004,1,0,0,0,225,1014,1,0,0,0,227,1024, - 1,0,0,0,229,1034,1,0,0,0,231,1042,1,0,0,0,233,1050,1,0,0,0,235,1055,1, - 0,0,0,237,1062,1,0,0,0,239,1069,1,0,0,0,241,1074,1,0,0,0,243,1082,1,0, - 0,0,245,1088,1,0,0,0,247,1095,1,0,0,0,249,1097,1,0,0,0,251,1099,1,0,0, - 0,253,1101,1,0,0,0,255,1103,1,0,0,0,257,1105,1,0,0,0,259,1107,1,0,0,0, - 261,1109,1,0,0,0,263,1115,1,0,0,0,265,1117,1,0,0,0,267,1120,1,0,0,0,269, - 1123,1,0,0,0,271,1125,1,0,0,0,273,1127,1,0,0,0,275,1129,1,0,0,0,277,1131, - 1,0,0,0,279,1133,1,0,0,0,281,1135,1,0,0,0,283,1137,1,0,0,0,285,1139,1, - 0,0,0,287,1158,1,0,0,0,289,1160,1,0,0,0,291,1168,1,0,0,0,293,1202,1,0, - 0,0,295,1204,1,0,0,0,297,1233,1,0,0,0,299,1235,1,0,0,0,301,1270,1,0,0, - 0,303,1273,1,0,0,0,305,1301,1,0,0,0,307,1303,1,0,0,0,309,1311,1,0,0,0, - 311,1319,1,0,0,0,313,1330,1,0,0,0,315,1336,1,0,0,0,317,1348,1,0,0,0,319, - 320,7,0,0,0,320,321,7,1,0,0,321,322,7,2,0,0,322,323,7,1,0,0,323,324,7, - 3,0,0,324,325,7,4,0,0,325,2,1,0,0,0,326,327,7,5,0,0,327,328,7,6,0,0,328, - 329,7,7,0,0,329,330,7,8,0,0,330,4,1,0,0,0,331,332,7,9,0,0,332,333,7,10, - 0,0,333,334,7,1,0,0,334,335,7,6,0,0,335,336,7,1,0,0,336,6,1,0,0,0,337, - 338,7,4,0,0,338,339,7,7,0,0,339,340,7,11,0,0,340,8,1,0,0,0,341,342,7,12, - 0,0,342,343,7,0,0,0,343,10,1,0,0,0,344,345,7,12,0,0,345,346,7,13,0,0,346, - 347,7,14,0,0,347,12,1,0,0,0,348,349,7,7,0,0,349,350,7,6,0,0,350,14,1,0, - 0,0,351,352,7,13,0,0,352,353,7,7,0,0,353,354,7,4,0,0,354,16,1,0,0,0,355, - 356,7,15,0,0,356,357,7,7,0,0,357,358,7,6,0,0,358,18,1,0,0,0,359,360,7, - 1,0,0,360,361,7,16,0,0,361,362,7,17,0,0,362,20,1,0,0,0,363,364,7,18,0, - 0,364,365,7,8,0,0,365,366,7,11,0,0,366,22,1,0,0,0,367,368,7,19,0,0,368, - 369,7,12,0,0,369,370,7,13,0,0,370,371,7,14,0,0,371,24,1,0,0,0,372,373, - 7,19,0,0,373,374,7,7,0,0,374,375,7,6,0,0,375,26,1,0,0,0,376,377,7,19,0, - 0,377,378,7,15,0,0,378,379,7,7,0,0,379,380,7,6,0,0,380,28,1,0,0,0,381, - 382,7,19,0,0,382,383,7,13,0,0,383,384,7,7,0,0,384,385,7,4,0,0,385,30,1, - 0,0,0,386,387,7,2,0,0,387,388,7,18,0,0,388,389,7,20,0,0,389,390,7,1,0, - 0,390,32,1,0,0,0,391,392,7,8,0,0,392,393,7,7,0,0,393,394,7,14,0,0,394, - 34,1,0,0,0,395,396,7,18,0,0,396,397,7,13,0,0,397,398,7,13,0,0,398,399, - 7,1,0,0,399,400,7,6,0,0,400,36,1,0,0,0,401,402,7,2,0,0,402,403,7,1,0,0, - 403,404,7,5,0,0,404,405,7,4,0,0,405,38,1,0,0,0,406,407,7,6,0,0,407,408, - 7,18,0,0,408,409,7,21,0,0,409,410,7,10,0,0,410,411,7,4,0,0,411,40,1,0, - 0,0,412,413,7,5,0,0,413,414,7,22,0,0,414,415,7,2,0,0,415,416,7,2,0,0,416, - 42,1,0,0,0,417,418,7,7,0,0,418,419,7,22,0,0,419,420,7,4,0,0,420,421,7, - 1,0,0,421,422,7,6,0,0,422,44,1,0,0,0,423,424,7,23,0,0,424,425,7,7,0,0, - 425,426,7,18,0,0,426,427,7,13,0,0,427,46,1,0,0,0,428,429,7,18,0,0,429, - 430,7,13,0,0,430,48,1,0,0,0,431,432,7,7,0,0,432,433,7,13,0,0,433,50,1, - 0,0,0,434,435,7,7,0,0,435,436,7,6,0,0,436,437,7,14,0,0,437,438,7,1,0,0, - 438,439,7,6,0,0,439,52,1,0,0,0,440,441,7,21,0,0,441,442,7,6,0,0,442,443, - 7,7,0,0,443,444,7,22,0,0,444,445,7,11,0,0,445,54,1,0,0,0,446,447,7,18, - 0,0,447,448,7,0,0,0,448,56,1,0,0,0,449,450,7,19,0,0,450,451,7,24,0,0,451, - 58,1,0,0,0,452,453,7,10,0,0,453,454,7,12,0,0,454,455,7,17,0,0,455,456, - 7,18,0,0,456,457,7,13,0,0,457,458,7,21,0,0,458,60,1,0,0,0,459,460,7,1, - 0,0,460,461,7,15,0,0,461,462,7,18,0,0,462,463,7,0,0,0,463,464,7,4,0,0, - 464,465,7,0,0,0,465,62,1,0,0,0,466,467,7,18,0,0,467,468,7,5,0,0,468,64, - 1,0,0,0,469,470,7,4,0,0,470,471,7,10,0,0,471,472,7,1,0,0,472,473,7,13, - 0,0,473,66,1,0,0,0,474,475,7,14,0,0,475,476,7,18,0,0,476,477,7,0,0,0,477, - 478,7,4,0,0,478,479,7,18,0,0,479,480,7,13,0,0,480,481,7,3,0,0,481,482, - 7,4,0,0,482,483,7,6,0,0,483,484,7,7,0,0,484,485,7,9,0,0,485,68,1,0,0,0, - 486,487,7,14,0,0,487,488,7,18,0,0,488,489,7,0,0,0,489,490,7,4,0,0,490, - 491,7,18,0,0,491,492,7,13,0,0,492,493,7,3,0,0,493,494,7,4,0,0,494,70,1, - 0,0,0,495,496,7,11,0,0,496,497,7,1,0,0,497,498,7,6,0,0,498,499,7,3,0,0, - 499,500,7,1,0,0,500,501,7,13,0,0,501,502,7,4,0,0,502,72,1,0,0,0,503,504, - 7,3,0,0,504,505,7,6,0,0,505,506,7,7,0,0,506,507,7,0,0,0,507,508,7,0,0, - 0,508,74,1,0,0,0,509,510,7,12,0,0,510,511,7,11,0,0,511,512,7,11,0,0,512, - 513,7,2,0,0,513,514,7,24,0,0,514,76,1,0,0,0,515,516,7,7,0,0,516,517,7, - 17,0,0,517,518,7,1,0,0,518,519,7,6,0,0,519,78,1,0,0,0,520,521,7,11,0,0, - 521,522,7,12,0,0,522,523,7,6,0,0,523,524,7,4,0,0,524,525,7,18,0,0,525, - 526,7,4,0,0,526,527,7,18,0,0,527,528,7,7,0,0,528,529,7,13,0,0,529,80,1, - 0,0,0,530,531,7,3,0,0,531,532,7,12,0,0,532,533,7,0,0,0,533,534,7,1,0,0, - 534,82,1,0,0,0,535,536,7,9,0,0,536,537,7,10,0,0,537,538,7,1,0,0,538,539, - 7,13,0,0,539,84,1,0,0,0,540,541,7,1,0,0,541,542,7,2,0,0,542,543,7,0,0, - 0,543,544,7,1,0,0,544,86,1,0,0,0,545,546,7,1,0,0,546,547,7,13,0,0,547, - 548,7,14,0,0,548,88,1,0,0,0,549,550,7,7,0,0,550,551,7,5,0,0,551,552,7, - 5,0,0,552,553,7,0,0,0,553,554,7,1,0,0,554,555,7,4,0,0,555,90,1,0,0,0,556, - 557,7,5,0,0,557,558,7,1,0,0,558,559,7,4,0,0,559,560,7,3,0,0,560,561,7, - 10,0,0,561,92,1,0,0,0,562,563,7,13,0,0,563,564,7,1,0,0,564,565,7,15,0, - 0,565,566,7,4,0,0,566,94,1,0,0,0,567,568,7,5,0,0,568,569,7,18,0,0,569, - 570,7,6,0,0,570,571,7,0,0,0,571,572,7,4,0,0,572,96,1,0,0,0,573,574,7,6, - 0,0,574,575,7,7,0,0,575,576,7,9,0,0,576,577,7,0,0,0,577,98,1,0,0,0,578, - 579,7,6,0,0,579,580,7,7,0,0,580,581,7,9,0,0,581,100,1,0,0,0,582,583,7, - 7,0,0,583,584,7,13,0,0,584,585,7,2,0,0,585,586,7,24,0,0,586,102,1,0,0, - 0,587,588,7,19,0,0,588,589,7,1,0,0,589,590,7,4,0,0,590,591,7,9,0,0,591, - 592,7,1,0,0,592,593,7,1,0,0,593,594,7,13,0,0,594,104,1,0,0,0,595,596,7, - 22,0,0,596,597,7,13,0,0,597,598,7,18,0,0,598,599,7,7,0,0,599,600,7,13, - 0,0,600,106,1,0,0,0,601,602,7,12,0,0,602,603,7,2,0,0,603,604,7,2,0,0,604, - 108,1,0,0,0,605,606,7,18,0,0,606,607,7,13,0,0,607,608,7,4,0,0,608,609, - 7,1,0,0,609,610,7,6,0,0,610,611,7,0,0,0,611,612,7,1,0,0,612,613,7,3,0, - 0,613,614,7,4,0,0,614,110,1,0,0,0,615,616,7,1,0,0,616,617,7,15,0,0,617, - 618,7,3,0,0,618,619,7,1,0,0,619,620,7,11,0,0,620,621,7,4,0,0,621,112,1, - 0,0,0,622,623,7,3,0,0,623,624,7,6,0,0,624,625,7,1,0,0,625,626,7,12,0,0, - 626,627,7,4,0,0,627,628,7,1,0,0,628,114,1,0,0,0,629,630,7,4,0,0,630,631, - 7,12,0,0,631,632,7,19,0,0,632,633,7,2,0,0,633,634,7,1,0,0,634,116,1,0, - 0,0,635,636,7,19,0,0,636,637,7,1,0,0,637,638,7,21,0,0,638,639,7,18,0,0, - 639,640,7,13,0,0,640,118,1,0,0,0,641,642,7,3,0,0,642,643,7,7,0,0,643,644, - 7,8,0,0,644,645,7,8,0,0,645,646,7,18,0,0,646,647,7,4,0,0,647,120,1,0,0, - 0,648,649,7,6,0,0,649,650,7,7,0,0,650,651,7,2,0,0,651,652,7,2,0,0,652, - 653,7,19,0,0,653,654,7,12,0,0,654,655,7,3,0,0,655,656,7,20,0,0,656,122, - 1,0,0,0,657,658,7,4,0,0,658,659,7,6,0,0,659,660,7,12,0,0,660,661,7,13, - 0,0,661,662,7,0,0,0,662,663,7,12,0,0,663,664,7,3,0,0,664,665,7,4,0,0,665, - 666,7,18,0,0,666,667,7,7,0,0,667,668,7,13,0,0,668,124,1,0,0,0,669,670, - 7,9,0,0,670,671,7,7,0,0,671,672,7,6,0,0,672,673,7,20,0,0,673,126,1,0,0, - 0,674,675,7,12,0,0,675,676,7,2,0,0,676,677,7,4,0,0,677,678,7,1,0,0,678, - 679,7,6,0,0,679,128,1,0,0,0,680,681,7,6,0,0,681,682,7,1,0,0,682,683,7, - 13,0,0,683,684,7,12,0,0,684,685,7,8,0,0,685,686,7,1,0,0,686,130,1,0,0, - 0,687,688,7,4,0,0,688,689,7,7,0,0,689,132,1,0,0,0,690,691,7,12,0,0,691, - 692,7,14,0,0,692,693,7,14,0,0,693,134,1,0,0,0,694,695,7,14,0,0,695,696, - 7,6,0,0,696,697,7,7,0,0,697,698,7,11,0,0,698,136,1,0,0,0,699,700,7,3,0, - 0,700,701,7,7,0,0,701,702,7,2,0,0,702,703,7,22,0,0,703,704,7,8,0,0,704, - 705,7,13,0,0,705,138,1,0,0,0,706,707,7,18,0,0,707,708,7,13,0,0,708,709, - 7,0,0,0,709,710,7,1,0,0,710,711,7,6,0,0,711,712,7,4,0,0,712,140,1,0,0, - 0,713,714,7,18,0,0,714,715,7,13,0,0,715,716,7,4,0,0,716,717,7,7,0,0,717, - 142,1,0,0,0,718,719,7,17,0,0,719,720,7,12,0,0,720,721,7,2,0,0,721,722, - 7,22,0,0,722,723,7,1,0,0,723,724,7,0,0,0,724,144,1,0,0,0,725,726,7,11, - 0,0,726,727,7,6,0,0,727,728,7,18,0,0,728,729,7,8,0,0,729,730,7,12,0,0, - 730,731,7,6,0,0,731,732,7,24,0,0,732,146,1,0,0,0,733,734,7,20,0,0,734, - 735,7,1,0,0,735,736,7,24,0,0,736,148,1,0,0,0,737,738,7,3,0,0,738,739,7, - 7,0,0,739,740,7,13,0,0,740,741,7,0,0,0,741,742,7,4,0,0,742,743,7,6,0,0, - 743,744,7,12,0,0,744,745,7,18,0,0,745,746,7,13,0,0,746,747,7,4,0,0,747, - 150,1,0,0,0,748,749,7,5,0,0,749,750,7,7,0,0,750,751,7,6,0,0,751,752,7, - 1,0,0,752,753,7,18,0,0,753,754,7,21,0,0,754,755,7,13,0,0,755,152,1,0,0, - 0,756,757,7,6,0,0,757,758,7,1,0,0,758,759,7,5,0,0,759,760,7,1,0,0,760, - 761,7,6,0,0,761,762,7,1,0,0,762,763,7,13,0,0,763,764,7,3,0,0,764,765,7, - 1,0,0,765,766,7,0,0,0,766,154,1,0,0,0,767,768,7,14,0,0,768,769,7,1,0,0, - 769,770,7,2,0,0,770,771,7,1,0,0,771,772,7,4,0,0,772,773,7,1,0,0,773,156, - 1,0,0,0,774,775,7,22,0,0,775,776,7,11,0,0,776,777,7,14,0,0,777,778,7,12, - 0,0,778,779,7,4,0,0,779,780,7,1,0,0,780,158,1,0,0,0,781,782,7,3,0,0,782, - 783,7,12,0,0,783,784,7,0,0,0,784,785,7,3,0,0,785,786,7,12,0,0,786,787, - 7,14,0,0,787,788,7,1,0,0,788,160,1,0,0,0,789,790,7,6,0,0,790,791,7,1,0, - 0,791,792,7,0,0,0,792,793,7,4,0,0,793,794,7,6,0,0,794,795,7,18,0,0,795, - 796,7,3,0,0,796,797,7,4,0,0,797,162,1,0,0,0,798,799,7,12,0,0,799,800,7, - 3,0,0,800,801,7,4,0,0,801,802,7,18,0,0,802,803,7,7,0,0,803,804,7,13,0, - 0,804,164,1,0,0,0,805,806,7,0,0,0,806,807,7,1,0,0,807,808,7,4,0,0,808, - 166,1,0,0,0,809,810,7,14,0,0,810,811,7,1,0,0,811,812,7,5,0,0,812,813,7, - 12,0,0,813,814,7,22,0,0,814,815,7,2,0,0,815,816,7,4,0,0,816,168,1,0,0, - 0,817,818,7,13,0,0,818,819,7,7,0,0,819,170,1,0,0,0,820,821,7,22,0,0,821, - 822,7,13,0,0,822,823,7,18,0,0,823,824,7,16,0,0,824,825,7,22,0,0,825,826, - 7,1,0,0,826,172,1,0,0,0,827,828,7,3,0,0,828,829,7,2,0,0,829,830,7,22,0, - 0,830,831,7,0,0,0,831,832,7,4,0,0,832,833,7,1,0,0,833,834,7,6,0,0,834, - 835,7,1,0,0,835,836,7,14,0,0,836,174,1,0,0,0,837,838,7,18,0,0,838,839, - 7,14,0,0,839,840,7,1,0,0,840,841,7,13,0,0,841,842,7,4,0,0,842,843,7,18, - 0,0,843,844,7,4,0,0,844,845,7,24,0,0,845,176,1,0,0,0,846,847,7,13,0,0, - 847,848,7,7,0,0,848,849,7,13,0,0,849,850,7,3,0,0,850,851,7,2,0,0,851,852, - 7,22,0,0,852,853,7,0,0,0,853,854,7,4,0,0,854,855,7,1,0,0,855,856,7,6,0, - 0,856,857,7,1,0,0,857,858,7,14,0,0,858,178,1,0,0,0,859,860,7,18,0,0,860, - 861,7,13,0,0,861,862,7,14,0,0,862,863,7,1,0,0,863,864,7,15,0,0,864,180, - 1,0,0,0,865,866,7,4,0,0,866,867,7,1,0,0,867,868,7,8,0,0,868,869,7,11,0, - 0,869,870,7,7,0,0,870,871,7,6,0,0,871,872,7,12,0,0,872,873,7,6,0,0,873, - 874,7,24,0,0,874,182,1,0,0,0,875,876,7,9,0,0,876,877,7,18,0,0,877,878, - 7,4,0,0,878,879,7,10,0,0,879,184,1,0,0,0,880,881,7,3,0,0,881,882,7,7,0, - 0,882,883,7,8,0,0,883,884,7,11,0,0,884,885,7,6,0,0,885,886,7,1,0,0,886, - 887,7,0,0,0,887,888,7,0,0,0,888,889,7,18,0,0,889,890,7,7,0,0,890,891,7, - 13,0,0,891,186,1,0,0,0,892,893,7,3,0,0,893,894,7,7,0,0,894,895,7,8,0,0, - 895,896,7,11,0,0,896,188,1,0,0,0,897,898,7,14,0,0,898,899,7,18,0,0,899, - 900,7,0,0,0,900,901,7,12,0,0,901,902,7,2,0,0,902,903,7,2,0,0,903,904,7, - 7,0,0,904,905,7,9,0,0,905,190,1,0,0,0,906,907,7,18,0,0,907,908,7,21,0, - 0,908,909,7,13,0,0,909,910,7,7,0,0,910,911,7,6,0,0,911,912,7,1,0,0,912, - 192,1,0,0,0,913,914,7,3,0,0,914,915,7,10,0,0,915,916,7,1,0,0,916,917,7, - 3,0,0,917,918,7,20,0,0,918,194,1,0,0,0,919,920,7,17,0,0,920,921,7,18,0, - 0,921,922,7,1,0,0,922,923,7,9,0,0,923,196,1,0,0,0,924,925,7,11,0,0,925, - 926,7,6,0,0,926,927,7,7,0,0,927,928,7,3,0,0,928,929,7,1,0,0,929,930,7, - 14,0,0,930,931,7,22,0,0,931,932,7,6,0,0,932,933,7,1,0,0,933,198,1,0,0, - 0,934,935,7,11,0,0,935,936,7,12,0,0,936,937,7,6,0,0,937,938,7,12,0,0,938, - 939,7,8,0,0,939,940,7,1,0,0,940,941,7,4,0,0,941,942,7,1,0,0,942,943,7, - 6,0,0,943,944,7,0,0,0,944,200,1,0,0,0,945,946,7,1,0,0,946,947,7,15,0,0, - 947,948,7,1,0,0,948,949,7,3,0,0,949,950,7,22,0,0,950,951,7,4,0,0,951,952, - 7,1,0,0,952,202,1,0,0,0,953,954,7,1,0,0,954,955,7,15,0,0,955,956,7,1,0, - 0,956,957,7,3,0,0,957,204,1,0,0,0,958,959,7,12,0,0,959,960,7,0,0,0,960, - 961,7,3,0,0,961,206,1,0,0,0,962,963,7,14,0,0,963,964,7,1,0,0,964,965,7, - 0,0,0,965,966,7,3,0,0,966,208,1,0,0,0,967,968,7,4,0,0,968,969,7,6,0,0, - 969,970,7,22,0,0,970,971,7,1,0,0,971,210,1,0,0,0,972,973,7,5,0,0,973,974, - 7,12,0,0,974,975,7,2,0,0,975,976,7,0,0,0,976,977,7,1,0,0,977,212,1,0,0, - 0,978,979,7,24,0,0,979,980,7,1,0,0,980,981,7,0,0,0,981,214,1,0,0,0,982, - 983,7,7,0,0,983,984,7,5,0,0,984,985,7,5,0,0,985,216,1,0,0,0,986,987,7, - 13,0,0,987,988,7,22,0,0,988,989,7,2,0,0,989,990,7,2,0,0,990,218,1,0,0, - 0,991,992,7,6,0,0,992,993,7,12,0,0,993,994,7,13,0,0,994,995,7,21,0,0,995, - 996,7,1,0,0,996,220,1,0,0,0,997,998,7,21,0,0,998,999,7,6,0,0,999,1000, - 7,7,0,0,1000,1001,7,22,0,0,1001,1002,7,11,0,0,1002,1003,7,0,0,0,1003,222, - 1,0,0,0,1004,1005,7,22,0,0,1005,1006,7,13,0,0,1006,1007,7,19,0,0,1007, - 1008,7,7,0,0,1008,1009,7,22,0,0,1009,1010,7,13,0,0,1010,1011,7,14,0,0, - 1011,1012,7,1,0,0,1012,1013,7,14,0,0,1013,224,1,0,0,0,1014,1015,7,11,0, - 0,1015,1016,7,6,0,0,1016,1017,7,1,0,0,1017,1018,7,3,0,0,1018,1019,7,1, - 0,0,1019,1020,7,14,0,0,1020,1021,7,18,0,0,1021,1022,7,13,0,0,1022,1023, - 7,21,0,0,1023,226,1,0,0,0,1024,1025,7,5,0,0,1025,1026,7,7,0,0,1026,1027, - 7,2,0,0,1027,1028,7,2,0,0,1028,1029,7,7,0,0,1029,1030,7,9,0,0,1030,1031, - 7,18,0,0,1031,1032,7,13,0,0,1032,1033,7,21,0,0,1033,228,1,0,0,0,1034,1035, - 7,3,0,0,1035,1036,7,22,0,0,1036,1037,7,6,0,0,1037,1038,7,6,0,0,1038,1039, - 7,1,0,0,1039,1040,7,13,0,0,1040,1041,7,4,0,0,1041,230,1,0,0,0,1042,1043, - 7,1,0,0,1043,1044,7,15,0,0,1044,1045,7,3,0,0,1045,1046,7,2,0,0,1046,1047, - 7,22,0,0,1047,1048,7,14,0,0,1048,1049,7,1,0,0,1049,232,1,0,0,0,1050,1051, - 7,4,0,0,1051,1052,7,18,0,0,1052,1053,7,1,0,0,1053,1054,7,0,0,0,1054,234, - 1,0,0,0,1055,1056,7,7,0,0,1056,1057,7,4,0,0,1057,1058,7,10,0,0,1058,1059, - 7,1,0,0,1059,1060,7,6,0,0,1060,1061,7,0,0,0,1061,236,1,0,0,0,1062,1063, - 7,9,0,0,1063,1064,7,18,0,0,1064,1065,7,4,0,0,1065,1066,7,10,0,0,1066,1067, - 7,18,0,0,1067,1068,7,13,0,0,1068,238,1,0,0,0,1069,1070,7,2,0,0,1070,1071, - 7,12,0,0,1071,1072,7,0,0,0,1072,1073,7,4,0,0,1073,240,1,0,0,0,1074,1075, - 7,6,0,0,1075,1076,7,1,0,0,1076,1077,7,0,0,0,1077,1078,7,11,0,0,1078,1079, - 7,1,0,0,1079,1080,7,3,0,0,1080,1081,7,4,0,0,1081,242,1,0,0,0,1082,1083, - 7,13,0,0,1083,1084,7,22,0,0,1084,1085,7,2,0,0,1085,1086,7,2,0,0,1086,1087, - 7,0,0,0,1087,244,1,0,0,0,1088,1089,7,5,0,0,1089,1090,7,18,0,0,1090,1091, - 7,2,0,0,1091,1092,7,4,0,0,1092,1093,7,1,0,0,1093,1094,7,6,0,0,1094,246, - 1,0,0,0,1095,1096,5,42,0,0,1096,248,1,0,0,0,1097,1098,5,47,0,0,1098,250, - 1,0,0,0,1099,1100,5,92,0,0,1100,252,1,0,0,0,1101,1102,5,94,0,0,1102,254, - 1,0,0,0,1103,1104,5,43,0,0,1104,256,1,0,0,0,1105,1106,5,45,0,0,1106,258, - 1,0,0,0,1107,1108,5,38,0,0,1108,260,1,0,0,0,1109,1110,5,61,0,0,1110,262, - 1,0,0,0,1111,1112,5,60,0,0,1112,1116,5,62,0,0,1113,1114,5,33,0,0,1114, - 1116,5,61,0,0,1115,1111,1,0,0,0,1115,1113,1,0,0,0,1116,264,1,0,0,0,1117, - 1118,5,60,0,0,1118,1119,5,61,0,0,1119,266,1,0,0,0,1120,1121,5,62,0,0,1121, - 1122,5,61,0,0,1122,268,1,0,0,0,1123,1124,5,60,0,0,1124,270,1,0,0,0,1125, - 1126,5,62,0,0,1126,272,1,0,0,0,1127,1128,5,40,0,0,1128,274,1,0,0,0,1129, - 1130,5,41,0,0,1130,276,1,0,0,0,1131,1132,5,44,0,0,1132,278,1,0,0,0,1133, - 1134,5,46,0,0,1134,280,1,0,0,0,1135,1136,5,33,0,0,1136,282,1,0,0,0,1137, - 1138,5,59,0,0,1138,284,1,0,0,0,1139,1140,5,64,0,0,1140,1141,5,64,0,0,1141, - 1142,1,0,0,0,1142,1146,7,25,0,0,1143,1145,7,26,0,0,1144,1143,1,0,0,0,1145, - 1148,1,0,0,0,1146,1144,1,0,0,0,1146,1147,1,0,0,0,1147,286,1,0,0,0,1148, - 1146,1,0,0,0,1149,1159,5,63,0,0,1150,1151,5,64,0,0,1151,1155,7,25,0,0, - 1152,1154,7,26,0,0,1153,1152,1,0,0,0,1154,1157,1,0,0,0,1155,1153,1,0,0, - 0,1155,1156,1,0,0,0,1156,1159,1,0,0,0,1157,1155,1,0,0,0,1158,1149,1,0, - 0,0,1158,1150,1,0,0,0,1159,288,1,0,0,0,1160,1161,5,48,0,0,1161,1163,7, - 15,0,0,1162,1164,7,27,0,0,1163,1162,1,0,0,0,1164,1165,1,0,0,0,1165,1163, - 1,0,0,0,1165,1166,1,0,0,0,1166,290,1,0,0,0,1167,1169,7,28,0,0,1168,1167, - 1,0,0,0,1169,1170,1,0,0,0,1170,1168,1,0,0,0,1170,1171,1,0,0,0,1171,292, - 1,0,0,0,1172,1174,7,28,0,0,1173,1172,1,0,0,0,1174,1175,1,0,0,0,1175,1173, - 1,0,0,0,1175,1176,1,0,0,0,1176,1177,1,0,0,0,1177,1181,5,46,0,0,1178,1180, - 7,28,0,0,1179,1178,1,0,0,0,1180,1183,1,0,0,0,1181,1179,1,0,0,0,1181,1182, - 1,0,0,0,1182,1185,1,0,0,0,1183,1181,1,0,0,0,1184,1186,3,295,147,0,1185, - 1184,1,0,0,0,1185,1186,1,0,0,0,1186,1203,1,0,0,0,1187,1189,5,46,0,0,1188, - 1190,7,28,0,0,1189,1188,1,0,0,0,1190,1191,1,0,0,0,1191,1189,1,0,0,0,1191, - 1192,1,0,0,0,1192,1194,1,0,0,0,1193,1195,3,295,147,0,1194,1193,1,0,0,0, - 1194,1195,1,0,0,0,1195,1203,1,0,0,0,1196,1198,7,28,0,0,1197,1196,1,0,0, - 0,1198,1199,1,0,0,0,1199,1197,1,0,0,0,1199,1200,1,0,0,0,1200,1201,1,0, - 0,0,1201,1203,3,295,147,0,1202,1173,1,0,0,0,1202,1187,1,0,0,0,1202,1197, - 1,0,0,0,1203,294,1,0,0,0,1204,1206,7,1,0,0,1205,1207,7,29,0,0,1206,1205, - 1,0,0,0,1206,1207,1,0,0,0,1207,1209,1,0,0,0,1208,1210,7,28,0,0,1209,1208, - 1,0,0,0,1210,1211,1,0,0,0,1211,1209,1,0,0,0,1211,1212,1,0,0,0,1212,296, - 1,0,0,0,1213,1219,5,34,0,0,1214,1218,8,30,0,0,1215,1216,5,34,0,0,1216, - 1218,5,34,0,0,1217,1214,1,0,0,0,1217,1215,1,0,0,0,1218,1221,1,0,0,0,1219, - 1217,1,0,0,0,1219,1220,1,0,0,0,1220,1222,1,0,0,0,1221,1219,1,0,0,0,1222, - 1234,5,34,0,0,1223,1229,5,39,0,0,1224,1228,8,31,0,0,1225,1226,5,39,0,0, - 1226,1228,5,39,0,0,1227,1224,1,0,0,0,1227,1225,1,0,0,0,1228,1231,1,0,0, - 0,1229,1227,1,0,0,0,1229,1230,1,0,0,0,1230,1232,1,0,0,0,1231,1229,1,0, - 0,0,1232,1234,5,39,0,0,1233,1213,1,0,0,0,1233,1223,1,0,0,0,1234,298,1, - 0,0,0,1235,1239,5,35,0,0,1236,1238,8,32,0,0,1237,1236,1,0,0,0,1238,1241, - 1,0,0,0,1239,1237,1,0,0,0,1239,1240,1,0,0,0,1240,1242,1,0,0,0,1241,1239, - 1,0,0,0,1242,1243,5,35,0,0,1243,300,1,0,0,0,1244,1245,5,123,0,0,1245,1246, - 3,303,151,0,1246,1247,5,125,0,0,1247,1271,1,0,0,0,1248,1249,5,123,0,0, - 1249,1250,7,21,0,0,1250,1251,7,22,0,0,1251,1252,7,18,0,0,1252,1256,7,14, - 0,0,1253,1255,7,33,0,0,1254,1253,1,0,0,0,1255,1258,1,0,0,0,1256,1254,1, - 0,0,0,1256,1257,1,0,0,0,1257,1259,1,0,0,0,1258,1256,1,0,0,0,1259,1260, - 5,123,0,0,1260,1261,3,303,151,0,1261,1265,5,125,0,0,1262,1264,7,33,0,0, - 1263,1262,1,0,0,0,1264,1267,1,0,0,0,1265,1263,1,0,0,0,1265,1266,1,0,0, - 0,1266,1268,1,0,0,0,1267,1265,1,0,0,0,1268,1269,5,125,0,0,1269,1271,1, - 0,0,0,1270,1244,1,0,0,0,1270,1248,1,0,0,0,1271,302,1,0,0,0,1272,1274,3, - 305,152,0,1273,1272,1,0,0,0,1274,1275,1,0,0,0,1275,1273,1,0,0,0,1275,1276, - 1,0,0,0,1276,1277,1,0,0,0,1277,1279,5,45,0,0,1278,1280,3,305,152,0,1279, - 1278,1,0,0,0,1280,1281,1,0,0,0,1281,1279,1,0,0,0,1281,1282,1,0,0,0,1282, - 1283,1,0,0,0,1283,1285,5,45,0,0,1284,1286,3,305,152,0,1285,1284,1,0,0, - 0,1286,1287,1,0,0,0,1287,1285,1,0,0,0,1287,1288,1,0,0,0,1288,1289,1,0, - 0,0,1289,1291,5,45,0,0,1290,1292,3,305,152,0,1291,1290,1,0,0,0,1292,1293, - 1,0,0,0,1293,1291,1,0,0,0,1293,1294,1,0,0,0,1294,1295,1,0,0,0,1295,1297, - 5,45,0,0,1296,1298,3,305,152,0,1297,1296,1,0,0,0,1298,1299,1,0,0,0,1299, - 1297,1,0,0,0,1299,1300,1,0,0,0,1300,304,1,0,0,0,1301,1302,7,27,0,0,1302, - 306,1,0,0,0,1303,1305,5,91,0,0,1304,1306,8,34,0,0,1305,1304,1,0,0,0,1306, - 1307,1,0,0,0,1307,1305,1,0,0,0,1307,1308,1,0,0,0,1308,1309,1,0,0,0,1309, - 1310,5,93,0,0,1310,308,1,0,0,0,1311,1313,5,96,0,0,1312,1314,8,35,0,0,1313, - 1312,1,0,0,0,1314,1315,1,0,0,0,1315,1313,1,0,0,0,1315,1316,1,0,0,0,1316, - 1317,1,0,0,0,1317,1318,5,96,0,0,1318,310,1,0,0,0,1319,1323,7,36,0,0,1320, - 1322,7,37,0,0,1321,1320,1,0,0,0,1322,1325,1,0,0,0,1323,1321,1,0,0,0,1323, - 1324,1,0,0,0,1324,1327,1,0,0,0,1325,1323,1,0,0,0,1326,1328,5,36,0,0,1327, - 1326,1,0,0,0,1327,1328,1,0,0,0,1328,312,1,0,0,0,1329,1331,7,38,0,0,1330, - 1329,1,0,0,0,1331,1332,1,0,0,0,1332,1330,1,0,0,0,1332,1333,1,0,0,0,1333, - 1334,1,0,0,0,1334,1335,6,156,0,0,1335,314,1,0,0,0,1336,1337,5,45,0,0,1337, - 1338,5,45,0,0,1338,1339,1,0,0,0,1339,1343,4,157,0,0,1340,1342,8,39,0,0, - 1341,1340,1,0,0,0,1342,1345,1,0,0,0,1343,1341,1,0,0,0,1343,1344,1,0,0, - 0,1344,1346,1,0,0,0,1345,1343,1,0,0,0,1346,1347,6,157,0,0,1347,316,1,0, - 0,0,1348,1349,5,47,0,0,1349,1350,5,42,0,0,1350,1354,1,0,0,0,1351,1353, - 9,0,0,0,1352,1351,1,0,0,0,1353,1356,1,0,0,0,1354,1355,1,0,0,0,1354,1352, - 1,0,0,0,1355,1357,1,0,0,0,1356,1354,1,0,0,0,1357,1358,5,42,0,0,1358,1359, - 5,47,0,0,1359,1360,1,0,0,0,1360,1361,6,158,0,0,1361,318,1,0,0,0,37,0,1115, - 1146,1155,1158,1165,1170,1175,1181,1185,1191,1194,1199,1202,1206,1211, - 1217,1219,1227,1229,1233,1239,1256,1265,1270,1275,1281,1287,1293,1299, - 1307,1315,1323,1327,1332,1343,1354,1,6,0,0 + 0,0,0,293,1,0,0,0,0,295,1,0,0,0,0,297,1,0,0,0,0,301,1,0,0,0,0,303,1,0, + 0,0,0,305,1,0,0,0,0,311,1,0,0,0,0,313,1,0,0,0,0,315,1,0,0,0,0,317,1,0, + 0,0,0,319,1,0,0,0,0,321,1,0,0,0,1,323,1,0,0,0,3,330,1,0,0,0,5,335,1,0, + 0,0,7,341,1,0,0,0,9,345,1,0,0,0,11,348,1,0,0,0,13,352,1,0,0,0,15,355,1, + 0,0,0,17,359,1,0,0,0,19,363,1,0,0,0,21,367,1,0,0,0,23,371,1,0,0,0,25,376, + 1,0,0,0,27,380,1,0,0,0,29,385,1,0,0,0,31,390,1,0,0,0,33,395,1,0,0,0,35, + 399,1,0,0,0,37,405,1,0,0,0,39,410,1,0,0,0,41,416,1,0,0,0,43,421,1,0,0, + 0,45,427,1,0,0,0,47,432,1,0,0,0,49,435,1,0,0,0,51,438,1,0,0,0,53,444,1, + 0,0,0,55,450,1,0,0,0,57,453,1,0,0,0,59,456,1,0,0,0,61,463,1,0,0,0,63,470, + 1,0,0,0,65,473,1,0,0,0,67,478,1,0,0,0,69,490,1,0,0,0,71,499,1,0,0,0,73, + 507,1,0,0,0,75,513,1,0,0,0,77,519,1,0,0,0,79,524,1,0,0,0,81,534,1,0,0, + 0,83,539,1,0,0,0,85,544,1,0,0,0,87,549,1,0,0,0,89,553,1,0,0,0,91,560,1, + 0,0,0,93,566,1,0,0,0,95,571,1,0,0,0,97,577,1,0,0,0,99,582,1,0,0,0,101, + 586,1,0,0,0,103,591,1,0,0,0,105,599,1,0,0,0,107,605,1,0,0,0,109,609,1, + 0,0,0,111,619,1,0,0,0,113,626,1,0,0,0,115,633,1,0,0,0,117,639,1,0,0,0, + 119,645,1,0,0,0,121,652,1,0,0,0,123,661,1,0,0,0,125,673,1,0,0,0,127,678, + 1,0,0,0,129,684,1,0,0,0,131,691,1,0,0,0,133,694,1,0,0,0,135,698,1,0,0, + 0,137,703,1,0,0,0,139,710,1,0,0,0,141,717,1,0,0,0,143,722,1,0,0,0,145, + 729,1,0,0,0,147,737,1,0,0,0,149,741,1,0,0,0,151,752,1,0,0,0,153,760,1, + 0,0,0,155,771,1,0,0,0,157,778,1,0,0,0,159,785,1,0,0,0,161,793,1,0,0,0, + 163,802,1,0,0,0,165,809,1,0,0,0,167,813,1,0,0,0,169,821,1,0,0,0,171,824, + 1,0,0,0,173,831,1,0,0,0,175,841,1,0,0,0,177,850,1,0,0,0,179,863,1,0,0, + 0,181,869,1,0,0,0,183,879,1,0,0,0,185,884,1,0,0,0,187,896,1,0,0,0,189, + 901,1,0,0,0,191,910,1,0,0,0,193,917,1,0,0,0,195,923,1,0,0,0,197,928,1, + 0,0,0,199,938,1,0,0,0,201,949,1,0,0,0,203,957,1,0,0,0,205,962,1,0,0,0, + 207,966,1,0,0,0,209,971,1,0,0,0,211,976,1,0,0,0,213,982,1,0,0,0,215,986, + 1,0,0,0,217,990,1,0,0,0,219,995,1,0,0,0,221,1001,1,0,0,0,223,1008,1,0, + 0,0,225,1018,1,0,0,0,227,1028,1,0,0,0,229,1038,1,0,0,0,231,1046,1,0,0, + 0,233,1054,1,0,0,0,235,1059,1,0,0,0,237,1066,1,0,0,0,239,1073,1,0,0,0, + 241,1078,1,0,0,0,243,1086,1,0,0,0,245,1092,1,0,0,0,247,1099,1,0,0,0,249, + 1111,1,0,0,0,251,1118,1,0,0,0,253,1120,1,0,0,0,255,1122,1,0,0,0,257,1124, + 1,0,0,0,259,1126,1,0,0,0,261,1128,1,0,0,0,263,1130,1,0,0,0,265,1132,1, + 0,0,0,267,1138,1,0,0,0,269,1140,1,0,0,0,271,1143,1,0,0,0,273,1146,1,0, + 0,0,275,1148,1,0,0,0,277,1150,1,0,0,0,279,1152,1,0,0,0,281,1154,1,0,0, + 0,283,1156,1,0,0,0,285,1158,1,0,0,0,287,1160,1,0,0,0,289,1162,1,0,0,0, + 291,1181,1,0,0,0,293,1183,1,0,0,0,295,1191,1,0,0,0,297,1225,1,0,0,0,299, + 1227,1,0,0,0,301,1256,1,0,0,0,303,1258,1,0,0,0,305,1293,1,0,0,0,307,1296, + 1,0,0,0,309,1324,1,0,0,0,311,1326,1,0,0,0,313,1334,1,0,0,0,315,1342,1, + 0,0,0,317,1353,1,0,0,0,319,1359,1,0,0,0,321,1371,1,0,0,0,323,324,7,0,0, + 0,324,325,7,1,0,0,325,326,7,2,0,0,326,327,7,1,0,0,327,328,7,3,0,0,328, + 329,7,4,0,0,329,2,1,0,0,0,330,331,7,5,0,0,331,332,7,6,0,0,332,333,7,7, + 0,0,333,334,7,8,0,0,334,4,1,0,0,0,335,336,7,9,0,0,336,337,7,10,0,0,337, + 338,7,1,0,0,338,339,7,6,0,0,339,340,7,1,0,0,340,6,1,0,0,0,341,342,7,4, + 0,0,342,343,7,7,0,0,343,344,7,11,0,0,344,8,1,0,0,0,345,346,7,12,0,0,346, + 347,7,0,0,0,347,10,1,0,0,0,348,349,7,12,0,0,349,350,7,13,0,0,350,351,7, + 14,0,0,351,12,1,0,0,0,352,353,7,7,0,0,353,354,7,6,0,0,354,14,1,0,0,0,355, + 356,7,13,0,0,356,357,7,7,0,0,357,358,7,4,0,0,358,16,1,0,0,0,359,360,7, + 15,0,0,360,361,7,7,0,0,361,362,7,6,0,0,362,18,1,0,0,0,363,364,7,1,0,0, + 364,365,7,16,0,0,365,366,7,17,0,0,366,20,1,0,0,0,367,368,7,18,0,0,368, + 369,7,8,0,0,369,370,7,11,0,0,370,22,1,0,0,0,371,372,7,19,0,0,372,373,7, + 12,0,0,373,374,7,13,0,0,374,375,7,14,0,0,375,24,1,0,0,0,376,377,7,19,0, + 0,377,378,7,7,0,0,378,379,7,6,0,0,379,26,1,0,0,0,380,381,7,19,0,0,381, + 382,7,15,0,0,382,383,7,7,0,0,383,384,7,6,0,0,384,28,1,0,0,0,385,386,7, + 19,0,0,386,387,7,13,0,0,387,388,7,7,0,0,388,389,7,4,0,0,389,30,1,0,0,0, + 390,391,7,2,0,0,391,392,7,18,0,0,392,393,7,20,0,0,393,394,7,1,0,0,394, + 32,1,0,0,0,395,396,7,8,0,0,396,397,7,7,0,0,397,398,7,14,0,0,398,34,1,0, + 0,0,399,400,7,18,0,0,400,401,7,13,0,0,401,402,7,13,0,0,402,403,7,1,0,0, + 403,404,7,6,0,0,404,36,1,0,0,0,405,406,7,2,0,0,406,407,7,1,0,0,407,408, + 7,5,0,0,408,409,7,4,0,0,409,38,1,0,0,0,410,411,7,6,0,0,411,412,7,18,0, + 0,412,413,7,21,0,0,413,414,7,10,0,0,414,415,7,4,0,0,415,40,1,0,0,0,416, + 417,7,5,0,0,417,418,7,22,0,0,418,419,7,2,0,0,419,420,7,2,0,0,420,42,1, + 0,0,0,421,422,7,7,0,0,422,423,7,22,0,0,423,424,7,4,0,0,424,425,7,1,0,0, + 425,426,7,6,0,0,426,44,1,0,0,0,427,428,7,23,0,0,428,429,7,7,0,0,429,430, + 7,18,0,0,430,431,7,13,0,0,431,46,1,0,0,0,432,433,7,18,0,0,433,434,7,13, + 0,0,434,48,1,0,0,0,435,436,7,7,0,0,436,437,7,13,0,0,437,50,1,0,0,0,438, + 439,7,7,0,0,439,440,7,6,0,0,440,441,7,14,0,0,441,442,7,1,0,0,442,443,7, + 6,0,0,443,52,1,0,0,0,444,445,7,21,0,0,445,446,7,6,0,0,446,447,7,7,0,0, + 447,448,7,22,0,0,448,449,7,11,0,0,449,54,1,0,0,0,450,451,7,18,0,0,451, + 452,7,0,0,0,452,56,1,0,0,0,453,454,7,19,0,0,454,455,7,24,0,0,455,58,1, + 0,0,0,456,457,7,10,0,0,457,458,7,12,0,0,458,459,7,17,0,0,459,460,7,18, + 0,0,460,461,7,13,0,0,461,462,7,21,0,0,462,60,1,0,0,0,463,464,7,1,0,0,464, + 465,7,15,0,0,465,466,7,18,0,0,466,467,7,0,0,0,467,468,7,4,0,0,468,469, + 7,0,0,0,469,62,1,0,0,0,470,471,7,18,0,0,471,472,7,5,0,0,472,64,1,0,0,0, + 473,474,7,4,0,0,474,475,7,10,0,0,475,476,7,1,0,0,476,477,7,13,0,0,477, + 66,1,0,0,0,478,479,7,14,0,0,479,480,7,18,0,0,480,481,7,0,0,0,481,482,7, + 4,0,0,482,483,7,18,0,0,483,484,7,13,0,0,484,485,7,3,0,0,485,486,7,4,0, + 0,486,487,7,6,0,0,487,488,7,7,0,0,488,489,7,9,0,0,489,68,1,0,0,0,490,491, + 7,14,0,0,491,492,7,18,0,0,492,493,7,0,0,0,493,494,7,4,0,0,494,495,7,18, + 0,0,495,496,7,13,0,0,496,497,7,3,0,0,497,498,7,4,0,0,498,70,1,0,0,0,499, + 500,7,11,0,0,500,501,7,1,0,0,501,502,7,6,0,0,502,503,7,3,0,0,503,504,7, + 1,0,0,504,505,7,13,0,0,505,506,7,4,0,0,506,72,1,0,0,0,507,508,7,3,0,0, + 508,509,7,6,0,0,509,510,7,7,0,0,510,511,7,0,0,0,511,512,7,0,0,0,512,74, + 1,0,0,0,513,514,7,12,0,0,514,515,7,11,0,0,515,516,7,11,0,0,516,517,7,2, + 0,0,517,518,7,24,0,0,518,76,1,0,0,0,519,520,7,7,0,0,520,521,7,17,0,0,521, + 522,7,1,0,0,522,523,7,6,0,0,523,78,1,0,0,0,524,525,7,11,0,0,525,526,7, + 12,0,0,526,527,7,6,0,0,527,528,7,4,0,0,528,529,7,18,0,0,529,530,7,4,0, + 0,530,531,7,18,0,0,531,532,7,7,0,0,532,533,7,13,0,0,533,80,1,0,0,0,534, + 535,7,3,0,0,535,536,7,12,0,0,536,537,7,0,0,0,537,538,7,1,0,0,538,82,1, + 0,0,0,539,540,7,9,0,0,540,541,7,10,0,0,541,542,7,1,0,0,542,543,7,13,0, + 0,543,84,1,0,0,0,544,545,7,1,0,0,545,546,7,2,0,0,546,547,7,0,0,0,547,548, + 7,1,0,0,548,86,1,0,0,0,549,550,7,1,0,0,550,551,7,13,0,0,551,552,7,14,0, + 0,552,88,1,0,0,0,553,554,7,7,0,0,554,555,7,5,0,0,555,556,7,5,0,0,556,557, + 7,0,0,0,557,558,7,1,0,0,558,559,7,4,0,0,559,90,1,0,0,0,560,561,7,5,0,0, + 561,562,7,1,0,0,562,563,7,4,0,0,563,564,7,3,0,0,564,565,7,10,0,0,565,92, + 1,0,0,0,566,567,7,13,0,0,567,568,7,1,0,0,568,569,7,15,0,0,569,570,7,4, + 0,0,570,94,1,0,0,0,571,572,7,5,0,0,572,573,7,18,0,0,573,574,7,6,0,0,574, + 575,7,0,0,0,575,576,7,4,0,0,576,96,1,0,0,0,577,578,7,6,0,0,578,579,7,7, + 0,0,579,580,7,9,0,0,580,581,7,0,0,0,581,98,1,0,0,0,582,583,7,6,0,0,583, + 584,7,7,0,0,584,585,7,9,0,0,585,100,1,0,0,0,586,587,7,7,0,0,587,588,7, + 13,0,0,588,589,7,2,0,0,589,590,7,24,0,0,590,102,1,0,0,0,591,592,7,19,0, + 0,592,593,7,1,0,0,593,594,7,4,0,0,594,595,7,9,0,0,595,596,7,1,0,0,596, + 597,7,1,0,0,597,598,7,13,0,0,598,104,1,0,0,0,599,600,7,22,0,0,600,601, + 7,13,0,0,601,602,7,18,0,0,602,603,7,7,0,0,603,604,7,13,0,0,604,106,1,0, + 0,0,605,606,7,12,0,0,606,607,7,2,0,0,607,608,7,2,0,0,608,108,1,0,0,0,609, + 610,7,18,0,0,610,611,7,13,0,0,611,612,7,4,0,0,612,613,7,1,0,0,613,614, + 7,6,0,0,614,615,7,0,0,0,615,616,7,1,0,0,616,617,7,3,0,0,617,618,7,4,0, + 0,618,110,1,0,0,0,619,620,7,1,0,0,620,621,7,15,0,0,621,622,7,3,0,0,622, + 623,7,1,0,0,623,624,7,11,0,0,624,625,7,4,0,0,625,112,1,0,0,0,626,627,7, + 3,0,0,627,628,7,6,0,0,628,629,7,1,0,0,629,630,7,12,0,0,630,631,7,4,0,0, + 631,632,7,1,0,0,632,114,1,0,0,0,633,634,7,4,0,0,634,635,7,12,0,0,635,636, + 7,19,0,0,636,637,7,2,0,0,637,638,7,1,0,0,638,116,1,0,0,0,639,640,7,19, + 0,0,640,641,7,1,0,0,641,642,7,21,0,0,642,643,7,18,0,0,643,644,7,13,0,0, + 644,118,1,0,0,0,645,646,7,3,0,0,646,647,7,7,0,0,647,648,7,8,0,0,648,649, + 7,8,0,0,649,650,7,18,0,0,650,651,7,4,0,0,651,120,1,0,0,0,652,653,7,6,0, + 0,653,654,7,7,0,0,654,655,7,2,0,0,655,656,7,2,0,0,656,657,7,19,0,0,657, + 658,7,12,0,0,658,659,7,3,0,0,659,660,7,20,0,0,660,122,1,0,0,0,661,662, + 7,4,0,0,662,663,7,6,0,0,663,664,7,12,0,0,664,665,7,13,0,0,665,666,7,0, + 0,0,666,667,7,12,0,0,667,668,7,3,0,0,668,669,7,4,0,0,669,670,7,18,0,0, + 670,671,7,7,0,0,671,672,7,13,0,0,672,124,1,0,0,0,673,674,7,9,0,0,674,675, + 7,7,0,0,675,676,7,6,0,0,676,677,7,20,0,0,677,126,1,0,0,0,678,679,7,12, + 0,0,679,680,7,2,0,0,680,681,7,4,0,0,681,682,7,1,0,0,682,683,7,6,0,0,683, + 128,1,0,0,0,684,685,7,6,0,0,685,686,7,1,0,0,686,687,7,13,0,0,687,688,7, + 12,0,0,688,689,7,8,0,0,689,690,7,1,0,0,690,130,1,0,0,0,691,692,7,4,0,0, + 692,693,7,7,0,0,693,132,1,0,0,0,694,695,7,12,0,0,695,696,7,14,0,0,696, + 697,7,14,0,0,697,134,1,0,0,0,698,699,7,14,0,0,699,700,7,6,0,0,700,701, + 7,7,0,0,701,702,7,11,0,0,702,136,1,0,0,0,703,704,7,3,0,0,704,705,7,7,0, + 0,705,706,7,2,0,0,706,707,7,22,0,0,707,708,7,8,0,0,708,709,7,13,0,0,709, + 138,1,0,0,0,710,711,7,18,0,0,711,712,7,13,0,0,712,713,7,0,0,0,713,714, + 7,1,0,0,714,715,7,6,0,0,715,716,7,4,0,0,716,140,1,0,0,0,717,718,7,18,0, + 0,718,719,7,13,0,0,719,720,7,4,0,0,720,721,7,7,0,0,721,142,1,0,0,0,722, + 723,7,17,0,0,723,724,7,12,0,0,724,725,7,2,0,0,725,726,7,22,0,0,726,727, + 7,1,0,0,727,728,7,0,0,0,728,144,1,0,0,0,729,730,7,11,0,0,730,731,7,6,0, + 0,731,732,7,18,0,0,732,733,7,8,0,0,733,734,7,12,0,0,734,735,7,6,0,0,735, + 736,7,24,0,0,736,146,1,0,0,0,737,738,7,20,0,0,738,739,7,1,0,0,739,740, + 7,24,0,0,740,148,1,0,0,0,741,742,7,3,0,0,742,743,7,7,0,0,743,744,7,13, + 0,0,744,745,7,0,0,0,745,746,7,4,0,0,746,747,7,6,0,0,747,748,7,12,0,0,748, + 749,7,18,0,0,749,750,7,13,0,0,750,751,7,4,0,0,751,150,1,0,0,0,752,753, + 7,5,0,0,753,754,7,7,0,0,754,755,7,6,0,0,755,756,7,1,0,0,756,757,7,18,0, + 0,757,758,7,21,0,0,758,759,7,13,0,0,759,152,1,0,0,0,760,761,7,6,0,0,761, + 762,7,1,0,0,762,763,7,5,0,0,763,764,7,1,0,0,764,765,7,6,0,0,765,766,7, + 1,0,0,766,767,7,13,0,0,767,768,7,3,0,0,768,769,7,1,0,0,769,770,7,0,0,0, + 770,154,1,0,0,0,771,772,7,14,0,0,772,773,7,1,0,0,773,774,7,2,0,0,774,775, + 7,1,0,0,775,776,7,4,0,0,776,777,7,1,0,0,777,156,1,0,0,0,778,779,7,22,0, + 0,779,780,7,11,0,0,780,781,7,14,0,0,781,782,7,12,0,0,782,783,7,4,0,0,783, + 784,7,1,0,0,784,158,1,0,0,0,785,786,7,3,0,0,786,787,7,12,0,0,787,788,7, + 0,0,0,788,789,7,3,0,0,789,790,7,12,0,0,790,791,7,14,0,0,791,792,7,1,0, + 0,792,160,1,0,0,0,793,794,7,6,0,0,794,795,7,1,0,0,795,796,7,0,0,0,796, + 797,7,4,0,0,797,798,7,6,0,0,798,799,7,18,0,0,799,800,7,3,0,0,800,801,7, + 4,0,0,801,162,1,0,0,0,802,803,7,12,0,0,803,804,7,3,0,0,804,805,7,4,0,0, + 805,806,7,18,0,0,806,807,7,7,0,0,807,808,7,13,0,0,808,164,1,0,0,0,809, + 810,7,0,0,0,810,811,7,1,0,0,811,812,7,4,0,0,812,166,1,0,0,0,813,814,7, + 14,0,0,814,815,7,1,0,0,815,816,7,5,0,0,816,817,7,12,0,0,817,818,7,22,0, + 0,818,819,7,2,0,0,819,820,7,4,0,0,820,168,1,0,0,0,821,822,7,13,0,0,822, + 823,7,7,0,0,823,170,1,0,0,0,824,825,7,22,0,0,825,826,7,13,0,0,826,827, + 7,18,0,0,827,828,7,16,0,0,828,829,7,22,0,0,829,830,7,1,0,0,830,172,1,0, + 0,0,831,832,7,3,0,0,832,833,7,2,0,0,833,834,7,22,0,0,834,835,7,0,0,0,835, + 836,7,4,0,0,836,837,7,1,0,0,837,838,7,6,0,0,838,839,7,1,0,0,839,840,7, + 14,0,0,840,174,1,0,0,0,841,842,7,18,0,0,842,843,7,14,0,0,843,844,7,1,0, + 0,844,845,7,13,0,0,845,846,7,4,0,0,846,847,7,18,0,0,847,848,7,4,0,0,848, + 849,7,24,0,0,849,176,1,0,0,0,850,851,7,13,0,0,851,852,7,7,0,0,852,853, + 7,13,0,0,853,854,7,3,0,0,854,855,7,2,0,0,855,856,7,22,0,0,856,857,7,0, + 0,0,857,858,7,4,0,0,858,859,7,1,0,0,859,860,7,6,0,0,860,861,7,1,0,0,861, + 862,7,14,0,0,862,178,1,0,0,0,863,864,7,18,0,0,864,865,7,13,0,0,865,866, + 7,14,0,0,866,867,7,1,0,0,867,868,7,15,0,0,868,180,1,0,0,0,869,870,7,4, + 0,0,870,871,7,1,0,0,871,872,7,8,0,0,872,873,7,11,0,0,873,874,7,7,0,0,874, + 875,7,6,0,0,875,876,7,12,0,0,876,877,7,6,0,0,877,878,7,24,0,0,878,182, + 1,0,0,0,879,880,7,9,0,0,880,881,7,18,0,0,881,882,7,4,0,0,882,883,7,10, + 0,0,883,184,1,0,0,0,884,885,7,3,0,0,885,886,7,7,0,0,886,887,7,8,0,0,887, + 888,7,11,0,0,888,889,7,6,0,0,889,890,7,1,0,0,890,891,7,0,0,0,891,892,7, + 0,0,0,892,893,7,18,0,0,893,894,7,7,0,0,894,895,7,13,0,0,895,186,1,0,0, + 0,896,897,7,3,0,0,897,898,7,7,0,0,898,899,7,8,0,0,899,900,7,11,0,0,900, + 188,1,0,0,0,901,902,7,14,0,0,902,903,7,18,0,0,903,904,7,0,0,0,904,905, + 7,12,0,0,905,906,7,2,0,0,906,907,7,2,0,0,907,908,7,7,0,0,908,909,7,9,0, + 0,909,190,1,0,0,0,910,911,7,18,0,0,911,912,7,21,0,0,912,913,7,13,0,0,913, + 914,7,7,0,0,914,915,7,6,0,0,915,916,7,1,0,0,916,192,1,0,0,0,917,918,7, + 3,0,0,918,919,7,10,0,0,919,920,7,1,0,0,920,921,7,3,0,0,921,922,7,20,0, + 0,922,194,1,0,0,0,923,924,7,17,0,0,924,925,7,18,0,0,925,926,7,1,0,0,926, + 927,7,9,0,0,927,196,1,0,0,0,928,929,7,11,0,0,929,930,7,6,0,0,930,931,7, + 7,0,0,931,932,7,3,0,0,932,933,7,1,0,0,933,934,7,14,0,0,934,935,7,22,0, + 0,935,936,7,6,0,0,936,937,7,1,0,0,937,198,1,0,0,0,938,939,7,11,0,0,939, + 940,7,12,0,0,940,941,7,6,0,0,941,942,7,12,0,0,942,943,7,8,0,0,943,944, + 7,1,0,0,944,945,7,4,0,0,945,946,7,1,0,0,946,947,7,6,0,0,947,948,7,0,0, + 0,948,200,1,0,0,0,949,950,7,1,0,0,950,951,7,15,0,0,951,952,7,1,0,0,952, + 953,7,3,0,0,953,954,7,22,0,0,954,955,7,4,0,0,955,956,7,1,0,0,956,202,1, + 0,0,0,957,958,7,1,0,0,958,959,7,15,0,0,959,960,7,1,0,0,960,961,7,3,0,0, + 961,204,1,0,0,0,962,963,7,12,0,0,963,964,7,0,0,0,964,965,7,3,0,0,965,206, + 1,0,0,0,966,967,7,14,0,0,967,968,7,1,0,0,968,969,7,0,0,0,969,970,7,3,0, + 0,970,208,1,0,0,0,971,972,7,4,0,0,972,973,7,6,0,0,973,974,7,22,0,0,974, + 975,7,1,0,0,975,210,1,0,0,0,976,977,7,5,0,0,977,978,7,12,0,0,978,979,7, + 2,0,0,979,980,7,0,0,0,980,981,7,1,0,0,981,212,1,0,0,0,982,983,7,24,0,0, + 983,984,7,1,0,0,984,985,7,0,0,0,985,214,1,0,0,0,986,987,7,7,0,0,987,988, + 7,5,0,0,988,989,7,5,0,0,989,216,1,0,0,0,990,991,7,13,0,0,991,992,7,22, + 0,0,992,993,7,2,0,0,993,994,7,2,0,0,994,218,1,0,0,0,995,996,7,6,0,0,996, + 997,7,12,0,0,997,998,7,13,0,0,998,999,7,21,0,0,999,1000,7,1,0,0,1000,220, + 1,0,0,0,1001,1002,7,21,0,0,1002,1003,7,6,0,0,1003,1004,7,7,0,0,1004,1005, + 7,22,0,0,1005,1006,7,11,0,0,1006,1007,7,0,0,0,1007,222,1,0,0,0,1008,1009, + 7,22,0,0,1009,1010,7,13,0,0,1010,1011,7,19,0,0,1011,1012,7,7,0,0,1012, + 1013,7,22,0,0,1013,1014,7,13,0,0,1014,1015,7,14,0,0,1015,1016,7,1,0,0, + 1016,1017,7,14,0,0,1017,224,1,0,0,0,1018,1019,7,11,0,0,1019,1020,7,6,0, + 0,1020,1021,7,1,0,0,1021,1022,7,3,0,0,1022,1023,7,1,0,0,1023,1024,7,14, + 0,0,1024,1025,7,18,0,0,1025,1026,7,13,0,0,1026,1027,7,21,0,0,1027,226, + 1,0,0,0,1028,1029,7,5,0,0,1029,1030,7,7,0,0,1030,1031,7,2,0,0,1031,1032, + 7,2,0,0,1032,1033,7,7,0,0,1033,1034,7,9,0,0,1034,1035,7,18,0,0,1035,1036, + 7,13,0,0,1036,1037,7,21,0,0,1037,228,1,0,0,0,1038,1039,7,3,0,0,1039,1040, + 7,22,0,0,1040,1041,7,6,0,0,1041,1042,7,6,0,0,1042,1043,7,1,0,0,1043,1044, + 7,13,0,0,1044,1045,7,4,0,0,1045,230,1,0,0,0,1046,1047,7,1,0,0,1047,1048, + 7,15,0,0,1048,1049,7,3,0,0,1049,1050,7,2,0,0,1050,1051,7,22,0,0,1051,1052, + 7,14,0,0,1052,1053,7,1,0,0,1053,232,1,0,0,0,1054,1055,7,4,0,0,1055,1056, + 7,18,0,0,1056,1057,7,1,0,0,1057,1058,7,0,0,0,1058,234,1,0,0,0,1059,1060, + 7,7,0,0,1060,1061,7,4,0,0,1061,1062,7,10,0,0,1062,1063,7,1,0,0,1063,1064, + 7,6,0,0,1064,1065,7,0,0,0,1065,236,1,0,0,0,1066,1067,7,9,0,0,1067,1068, + 7,18,0,0,1068,1069,7,4,0,0,1069,1070,7,10,0,0,1070,1071,7,18,0,0,1071, + 1072,7,13,0,0,1072,238,1,0,0,0,1073,1074,7,2,0,0,1074,1075,7,12,0,0,1075, + 1076,7,0,0,0,1076,1077,7,4,0,0,1077,240,1,0,0,0,1078,1079,7,6,0,0,1079, + 1080,7,1,0,0,1080,1081,7,0,0,0,1081,1082,7,11,0,0,1082,1083,7,1,0,0,1083, + 1084,7,3,0,0,1084,1085,7,4,0,0,1085,242,1,0,0,0,1086,1087,7,13,0,0,1087, + 1088,7,22,0,0,1088,1089,7,2,0,0,1089,1090,7,2,0,0,1090,1091,7,0,0,0,1091, + 244,1,0,0,0,1092,1093,7,5,0,0,1093,1094,7,18,0,0,1094,1095,7,2,0,0,1095, + 1096,7,4,0,0,1096,1097,7,1,0,0,1097,1098,7,6,0,0,1098,246,1,0,0,0,1099, + 1100,7,7,0,0,1100,1101,7,9,0,0,1101,1102,7,13,0,0,1102,1103,7,1,0,0,1103, + 1104,7,6,0,0,1104,1105,7,12,0,0,1105,1106,7,3,0,0,1106,1107,7,3,0,0,1107, + 1108,7,1,0,0,1108,1109,7,0,0,0,1109,1110,7,0,0,0,1110,248,1,0,0,0,1111, + 1112,7,7,0,0,1112,1113,7,11,0,0,1113,1114,7,4,0,0,1114,1115,7,18,0,0,1115, + 1116,7,7,0,0,1116,1117,7,13,0,0,1117,250,1,0,0,0,1118,1119,5,42,0,0,1119, + 252,1,0,0,0,1120,1121,5,47,0,0,1121,254,1,0,0,0,1122,1123,5,92,0,0,1123, + 256,1,0,0,0,1124,1125,5,94,0,0,1125,258,1,0,0,0,1126,1127,5,43,0,0,1127, + 260,1,0,0,0,1128,1129,5,45,0,0,1129,262,1,0,0,0,1130,1131,5,38,0,0,1131, + 264,1,0,0,0,1132,1133,5,61,0,0,1133,266,1,0,0,0,1134,1135,5,60,0,0,1135, + 1139,5,62,0,0,1136,1137,5,33,0,0,1137,1139,5,61,0,0,1138,1134,1,0,0,0, + 1138,1136,1,0,0,0,1139,268,1,0,0,0,1140,1141,5,60,0,0,1141,1142,5,61,0, + 0,1142,270,1,0,0,0,1143,1144,5,62,0,0,1144,1145,5,61,0,0,1145,272,1,0, + 0,0,1146,1147,5,60,0,0,1147,274,1,0,0,0,1148,1149,5,62,0,0,1149,276,1, + 0,0,0,1150,1151,5,40,0,0,1151,278,1,0,0,0,1152,1153,5,41,0,0,1153,280, + 1,0,0,0,1154,1155,5,44,0,0,1155,282,1,0,0,0,1156,1157,5,46,0,0,1157,284, + 1,0,0,0,1158,1159,5,33,0,0,1159,286,1,0,0,0,1160,1161,5,59,0,0,1161,288, + 1,0,0,0,1162,1163,5,64,0,0,1163,1164,5,64,0,0,1164,1165,1,0,0,0,1165,1169, + 7,25,0,0,1166,1168,7,26,0,0,1167,1166,1,0,0,0,1168,1171,1,0,0,0,1169,1167, + 1,0,0,0,1169,1170,1,0,0,0,1170,290,1,0,0,0,1171,1169,1,0,0,0,1172,1182, + 5,63,0,0,1173,1174,5,64,0,0,1174,1178,7,25,0,0,1175,1177,7,26,0,0,1176, + 1175,1,0,0,0,1177,1180,1,0,0,0,1178,1176,1,0,0,0,1178,1179,1,0,0,0,1179, + 1182,1,0,0,0,1180,1178,1,0,0,0,1181,1172,1,0,0,0,1181,1173,1,0,0,0,1182, + 292,1,0,0,0,1183,1184,5,48,0,0,1184,1186,7,15,0,0,1185,1187,7,27,0,0,1186, + 1185,1,0,0,0,1187,1188,1,0,0,0,1188,1186,1,0,0,0,1188,1189,1,0,0,0,1189, + 294,1,0,0,0,1190,1192,7,28,0,0,1191,1190,1,0,0,0,1192,1193,1,0,0,0,1193, + 1191,1,0,0,0,1193,1194,1,0,0,0,1194,296,1,0,0,0,1195,1197,7,28,0,0,1196, + 1195,1,0,0,0,1197,1198,1,0,0,0,1198,1196,1,0,0,0,1198,1199,1,0,0,0,1199, + 1200,1,0,0,0,1200,1204,5,46,0,0,1201,1203,7,28,0,0,1202,1201,1,0,0,0,1203, + 1206,1,0,0,0,1204,1202,1,0,0,0,1204,1205,1,0,0,0,1205,1208,1,0,0,0,1206, + 1204,1,0,0,0,1207,1209,3,299,149,0,1208,1207,1,0,0,0,1208,1209,1,0,0,0, + 1209,1226,1,0,0,0,1210,1212,5,46,0,0,1211,1213,7,28,0,0,1212,1211,1,0, + 0,0,1213,1214,1,0,0,0,1214,1212,1,0,0,0,1214,1215,1,0,0,0,1215,1217,1, + 0,0,0,1216,1218,3,299,149,0,1217,1216,1,0,0,0,1217,1218,1,0,0,0,1218,1226, + 1,0,0,0,1219,1221,7,28,0,0,1220,1219,1,0,0,0,1221,1222,1,0,0,0,1222,1220, + 1,0,0,0,1222,1223,1,0,0,0,1223,1224,1,0,0,0,1224,1226,3,299,149,0,1225, + 1196,1,0,0,0,1225,1210,1,0,0,0,1225,1220,1,0,0,0,1226,298,1,0,0,0,1227, + 1229,7,1,0,0,1228,1230,7,29,0,0,1229,1228,1,0,0,0,1229,1230,1,0,0,0,1230, + 1232,1,0,0,0,1231,1233,7,28,0,0,1232,1231,1,0,0,0,1233,1234,1,0,0,0,1234, + 1232,1,0,0,0,1234,1235,1,0,0,0,1235,300,1,0,0,0,1236,1242,5,34,0,0,1237, + 1241,8,30,0,0,1238,1239,5,34,0,0,1239,1241,5,34,0,0,1240,1237,1,0,0,0, + 1240,1238,1,0,0,0,1241,1244,1,0,0,0,1242,1240,1,0,0,0,1242,1243,1,0,0, + 0,1243,1245,1,0,0,0,1244,1242,1,0,0,0,1245,1257,5,34,0,0,1246,1252,5,39, + 0,0,1247,1251,8,31,0,0,1248,1249,5,39,0,0,1249,1251,5,39,0,0,1250,1247, + 1,0,0,0,1250,1248,1,0,0,0,1251,1254,1,0,0,0,1252,1250,1,0,0,0,1252,1253, + 1,0,0,0,1253,1255,1,0,0,0,1254,1252,1,0,0,0,1255,1257,5,39,0,0,1256,1236, + 1,0,0,0,1256,1246,1,0,0,0,1257,302,1,0,0,0,1258,1262,5,35,0,0,1259,1261, + 8,32,0,0,1260,1259,1,0,0,0,1261,1264,1,0,0,0,1262,1260,1,0,0,0,1262,1263, + 1,0,0,0,1263,1265,1,0,0,0,1264,1262,1,0,0,0,1265,1266,5,35,0,0,1266,304, + 1,0,0,0,1267,1268,5,123,0,0,1268,1269,3,307,153,0,1269,1270,5,125,0,0, + 1270,1294,1,0,0,0,1271,1272,5,123,0,0,1272,1273,7,21,0,0,1273,1274,7,22, + 0,0,1274,1275,7,18,0,0,1275,1279,7,14,0,0,1276,1278,7,33,0,0,1277,1276, + 1,0,0,0,1278,1281,1,0,0,0,1279,1277,1,0,0,0,1279,1280,1,0,0,0,1280,1282, + 1,0,0,0,1281,1279,1,0,0,0,1282,1283,5,123,0,0,1283,1284,3,307,153,0,1284, + 1288,5,125,0,0,1285,1287,7,33,0,0,1286,1285,1,0,0,0,1287,1290,1,0,0,0, + 1288,1286,1,0,0,0,1288,1289,1,0,0,0,1289,1291,1,0,0,0,1290,1288,1,0,0, + 0,1291,1292,5,125,0,0,1292,1294,1,0,0,0,1293,1267,1,0,0,0,1293,1271,1, + 0,0,0,1294,306,1,0,0,0,1295,1297,3,309,154,0,1296,1295,1,0,0,0,1297,1298, + 1,0,0,0,1298,1296,1,0,0,0,1298,1299,1,0,0,0,1299,1300,1,0,0,0,1300,1302, + 5,45,0,0,1301,1303,3,309,154,0,1302,1301,1,0,0,0,1303,1304,1,0,0,0,1304, + 1302,1,0,0,0,1304,1305,1,0,0,0,1305,1306,1,0,0,0,1306,1308,5,45,0,0,1307, + 1309,3,309,154,0,1308,1307,1,0,0,0,1309,1310,1,0,0,0,1310,1308,1,0,0,0, + 1310,1311,1,0,0,0,1311,1312,1,0,0,0,1312,1314,5,45,0,0,1313,1315,3,309, + 154,0,1314,1313,1,0,0,0,1315,1316,1,0,0,0,1316,1314,1,0,0,0,1316,1317, + 1,0,0,0,1317,1318,1,0,0,0,1318,1320,5,45,0,0,1319,1321,3,309,154,0,1320, + 1319,1,0,0,0,1321,1322,1,0,0,0,1322,1320,1,0,0,0,1322,1323,1,0,0,0,1323, + 308,1,0,0,0,1324,1325,7,27,0,0,1325,310,1,0,0,0,1326,1328,5,91,0,0,1327, + 1329,8,34,0,0,1328,1327,1,0,0,0,1329,1330,1,0,0,0,1330,1328,1,0,0,0,1330, + 1331,1,0,0,0,1331,1332,1,0,0,0,1332,1333,5,93,0,0,1333,312,1,0,0,0,1334, + 1336,5,96,0,0,1335,1337,8,35,0,0,1336,1335,1,0,0,0,1337,1338,1,0,0,0,1338, + 1336,1,0,0,0,1338,1339,1,0,0,0,1339,1340,1,0,0,0,1340,1341,5,96,0,0,1341, + 314,1,0,0,0,1342,1346,7,36,0,0,1343,1345,7,37,0,0,1344,1343,1,0,0,0,1345, + 1348,1,0,0,0,1346,1344,1,0,0,0,1346,1347,1,0,0,0,1347,1350,1,0,0,0,1348, + 1346,1,0,0,0,1349,1351,5,36,0,0,1350,1349,1,0,0,0,1350,1351,1,0,0,0,1351, + 316,1,0,0,0,1352,1354,7,38,0,0,1353,1352,1,0,0,0,1354,1355,1,0,0,0,1355, + 1353,1,0,0,0,1355,1356,1,0,0,0,1356,1357,1,0,0,0,1357,1358,6,158,0,0,1358, + 318,1,0,0,0,1359,1360,5,45,0,0,1360,1361,5,45,0,0,1361,1362,1,0,0,0,1362, + 1366,4,159,0,0,1363,1365,8,39,0,0,1364,1363,1,0,0,0,1365,1368,1,0,0,0, + 1366,1364,1,0,0,0,1366,1367,1,0,0,0,1367,1369,1,0,0,0,1368,1366,1,0,0, + 0,1369,1370,6,159,0,0,1370,320,1,0,0,0,1371,1372,5,47,0,0,1372,1373,5, + 42,0,0,1373,1377,1,0,0,0,1374,1376,9,0,0,0,1375,1374,1,0,0,0,1376,1379, + 1,0,0,0,1377,1378,1,0,0,0,1377,1375,1,0,0,0,1378,1380,1,0,0,0,1379,1377, + 1,0,0,0,1380,1381,5,42,0,0,1381,1382,5,47,0,0,1382,1383,1,0,0,0,1383,1384, + 6,160,0,0,1384,322,1,0,0,0,37,0,1138,1169,1178,1181,1188,1193,1198,1204, + 1208,1214,1217,1222,1225,1229,1234,1240,1242,1250,1252,1256,1262,1279, + 1288,1293,1298,1304,1310,1316,1322,1330,1338,1346,1350,1355,1366,1377, + 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 f9af4432..c7a82bbe 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs @@ -54,12 +54,13 @@ public const int ASC=103, DESC=104, TRUE=105, FALSE=106, YES=107, OFF=108, NULL=109, RANGE=110, GROUPS=111, UNBOUNDED=112, PRECEDING=113, FOLLOWING=114, CURRENT=115, EXCLUDE=116, TIES=117, OTHERS=118, WITHIN=119, LAST=120, RESPECT=121, - NULLS=122, FILTER=123, STAR=124, SLASH=125, BACKSLASH=126, CARET=127, - PLUS=128, MINUS=129, AMP=130, EQ=131, NEQ=132, LTE=133, GTE=134, LT=135, - GT=136, LPAREN=137, RPAREN=138, COMMA=139, DOT=140, BANG=141, SEMI=142, - SYSVAR=143, PARAM=144, HEX_LITERAL=145, INTEGER_LITERAL=146, NUMBER_LITERAL=147, - STRING_LITERAL=148, DATE_LITERAL=149, GUID_LITERAL=150, BRACKET_ID=151, - BACKTICK_ID=152, IDENTIFIER=153, WS=154, LINE_COMMENT=155, BLOCK_COMMENT=156; + NULLS=122, FILTER=123, OWNERACCESS=124, OPTION=125, STAR=126, SLASH=127, + BACKSLASH=128, CARET=129, PLUS=130, MINUS=131, AMP=132, EQ=133, NEQ=134, + LTE=135, GTE=136, LT=137, GT=138, LPAREN=139, RPAREN=140, COMMA=141, DOT=142, + BANG=143, SEMI=144, SYSVAR=145, PARAM=146, HEX_LITERAL=147, INTEGER_LITERAL=148, + NUMBER_LITERAL=149, STRING_LITERAL=150, DATE_LITERAL=151, GUID_LITERAL=152, + BRACKET_ID=153, BACKTICK_ID=154, IDENTIFIER=155, WS=156, LINE_COMMENT=157, + BLOCK_COMMENT=158; public const int RULE_statement = 0, RULE_ifThenStatement = 1, RULE_thenBody = 2, RULE_executeStatement = 3, RULE_updateStatement = 4, RULE_assignment = 5, RULE_deleteStatement = 6, @@ -72,20 +73,21 @@ public const int RULE_dataType = 25, RULE_signedInteger = 26, RULE_columnConstraint = 27, RULE_tableConstraint = 28, RULE_checkBody = 29, RULE_foreignKeyAction = 30, RULE_referentialAction = 31, RULE_insertStatement = 32, RULE_rowValues = 33, - RULE_rowValue = 34, RULE_queryExpression = 35, RULE_queryTerm = 36, RULE_setOperator = 37, - RULE_querySpecification = 38, RULE_selectPredicate = 39, RULE_groupByClause = 40, - RULE_havingClause = 41, RULE_topClause = 42, RULE_topOperand = 43, RULE_offsetFetchClause = 44, - RULE_rowKeyword = 45, RULE_selectList = 46, RULE_selectItem = 47, RULE_fromClause = 48, - RULE_tableSource = 49, RULE_tablePrimary = 50, RULE_joinClause = 51, RULE_joinType = 52, - RULE_whereClause = 53, RULE_orderByClause = 54, RULE_orderByItem = 55, - RULE_expression = 56, RULE_primary = 57, RULE_caseExpression = 58, RULE_caseWhen = 59, - RULE_functionCall = 60, RULE_functionName = 61, RULE_columnRef = 62, RULE_memberName = 63, - RULE_identifier = 64, RULE_literal = 65, RULE_transactionStatement = 66, - RULE_standaloneExpression = 67, RULE_windowSpecification = 68, RULE_clusteredOption = 69, - RULE_windowFrame = 70, RULE_frameBound = 71, RULE_frameExclusion = 72, - RULE_nonReservedKeyword = 73, RULE_reservedKeyword = 74, RULE_filterClause = 75, - RULE_withinGroup = 76, RULE_nthRowFrom = 77, RULE_nullTreatment = 78, - RULE_derivedColumns = 79; + RULE_rowValue = 34, RULE_queryExpression = 35, RULE_ownerAccessOption = 36, + RULE_queryTerm = 37, RULE_setOperator = 38, RULE_querySpecification = 39, + RULE_selectPredicate = 40, RULE_groupByClause = 41, RULE_havingClause = 42, + RULE_topClause = 43, RULE_topOperand = 44, RULE_offsetFetchClause = 45, + RULE_rowKeyword = 46, RULE_selectList = 47, RULE_selectItem = 48, RULE_fromClause = 49, + RULE_tableSource = 50, RULE_tablePrimary = 51, RULE_joinClause = 52, RULE_joinType = 53, + RULE_whereClause = 54, RULE_orderByClause = 55, RULE_orderByItem = 56, + RULE_expression = 57, RULE_primary = 58, RULE_caseExpression = 59, RULE_caseWhen = 60, + RULE_functionCall = 61, RULE_functionName = 62, RULE_columnRef = 63, RULE_memberName = 64, + RULE_identifier = 65, RULE_literal = 66, RULE_transactionStatement = 67, + RULE_standaloneExpression = 68, RULE_windowSpecification = 69, RULE_clusteredOption = 70, + RULE_windowFrame = 71, RULE_frameBound = 72, RULE_frameExclusion = 73, + RULE_nonReservedKeyword = 74, RULE_reservedKeyword = 75, RULE_filterClause = 76, + RULE_withinGroup = 77, RULE_nthRowFrom = 78, RULE_nullTreatment = 79, + RULE_derivedColumns = 80; public static readonly string[] ruleNames = { "statement", "ifThenStatement", "thenBody", "executeStatement", "updateStatement", "assignment", "deleteStatement", "systemVariableSelect", "sysVarItem", @@ -95,15 +97,16 @@ public const int "indexColumn", "withOption", "columnDefinition", "calculatedClause", "dataType", "signedInteger", "columnConstraint", "tableConstraint", "checkBody", "foreignKeyAction", "referentialAction", "insertStatement", "rowValues", "rowValue", "queryExpression", - "queryTerm", "setOperator", "querySpecification", "selectPredicate", "groupByClause", - "havingClause", "topClause", "topOperand", "offsetFetchClause", "rowKeyword", - "selectList", "selectItem", "fromClause", "tableSource", "tablePrimary", - "joinClause", "joinType", "whereClause", "orderByClause", "orderByItem", - "expression", "primary", "caseExpression", "caseWhen", "functionCall", - "functionName", "columnRef", "memberName", "identifier", "literal", "transactionStatement", - "standaloneExpression", "windowSpecification", "clusteredOption", "windowFrame", - "frameBound", "frameExclusion", "nonReservedKeyword", "reservedKeyword", - "filterClause", "withinGroup", "nthRowFrom", "nullTreatment", "derivedColumns" + "ownerAccessOption", "queryTerm", "setOperator", "querySpecification", + "selectPredicate", "groupByClause", "havingClause", "topClause", "topOperand", + "offsetFetchClause", "rowKeyword", "selectList", "selectItem", "fromClause", + "tableSource", "tablePrimary", "joinClause", "joinType", "whereClause", + "orderByClause", "orderByItem", "expression", "primary", "caseExpression", + "caseWhen", "functionCall", "functionName", "columnRef", "memberName", + "identifier", "literal", "transactionStatement", "standaloneExpression", + "windowSpecification", "clusteredOption", "windowFrame", "frameBound", + "frameExclusion", "nonReservedKeyword", "reservedKeyword", "filterClause", + "withinGroup", "nthRowFrom", "nullTreatment", "derivedColumns" }; private static readonly string[] _LiteralNames = { @@ -117,9 +120,9 @@ 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, null, null, null, null, "'*'", "'/'", "'\\'", "'^'", "'+'", + "'-'", "'&'", "'='", null, "'<='", "'>='", "'<'", "'>'", "'('", "')'", + "','", "'.'", "'!'", "';'" }; private static readonly string[] _SymbolicNames = { null, "SELECT", "FROM", "WHERE", "TOP", "AS", "AND", "OR", "NOT", "XOR", @@ -137,12 +140,12 @@ public const int "CHECK", "VIEW", "PROCEDURE", "PARAMETERS", "EXECUTE", "EXEC", "ASC", "DESC", "TRUE", "FALSE", "YES", "OFF", "NULL", "RANGE", "GROUPS", "UNBOUNDED", "PRECEDING", "FOLLOWING", "CURRENT", "EXCLUDE", "TIES", "OTHERS", "WITHIN", - "LAST", "RESPECT", "NULLS", "FILTER", "STAR", "SLASH", "BACKSLASH", "CARET", - "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", "LT", "GT", "LPAREN", - "RPAREN", "COMMA", "DOT", "BANG", "SEMI", "SYSVAR", "PARAM", "HEX_LITERAL", - "INTEGER_LITERAL", "NUMBER_LITERAL", "STRING_LITERAL", "DATE_LITERAL", - "GUID_LITERAL", "BRACKET_ID", "BACKTICK_ID", "IDENTIFIER", "WS", "LINE_COMMENT", - "BLOCK_COMMENT" + "LAST", "RESPECT", "NULLS", "FILTER", "OWNERACCESS", "OPTION", "STAR", + "SLASH", "BACKSLASH", "CARET", "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", + "GTE", "LT", "GT", "LPAREN", "RPAREN", "COMMA", "DOT", "BANG", "SEMI", + "SYSVAR", "PARAM", "HEX_LITERAL", "INTEGER_LITERAL", "NUMBER_LITERAL", + "STRING_LITERAL", "DATE_LITERAL", "GUID_LITERAL", "BRACKET_ID", "BACKTICK_ID", + "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT" }; public static readonly IVocabulary DefaultVocabulary = new Vocabulary(_LiteralNames, _SymbolicNames); @@ -257,115 +260,115 @@ public StatementContext statement() { try { EnterOuterAlt(_localctx, 1); { - State = 161; + State = 163; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PARAMETERS) { { - State = 160; + State = 162; parametersClause(); } } - State = 177; + State = 179; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,1,Context) ) { case 1: { - State = 163; + State = 165; ifThenStatement(); } break; case 2: { - State = 164; + State = 166; createTableStatement(); } break; case 3: { - State = 165; + State = 167; createIndexStatement(); } break; case 4: { - State = 166; + State = 168; createViewStatement(); } break; case 5: { - State = 167; + State = 169; createProcedureStatement(); } break; case 6: { - State = 168; + State = 170; alterTableStatement(); } break; case 7: { - State = 169; + State = 171; dropStatement(); } break; case 8: { - State = 170; + State = 172; insertStatement(); } break; case 9: { - State = 171; + State = 173; updateStatement(); } break; case 10: { - State = 172; + State = 174; deleteStatement(); } break; case 11: { - State = 173; + State = 175; transactionStatement(); } break; case 12: { - State = 174; + State = 176; executeStatement(); } break; case 13: { - State = 175; + State = 177; systemVariableSelect(); } break; case 14: { - State = 176; + State = 178; queryExpression(); } break; } - State = 180; + State = 182; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==SEMI) { { - State = 179; + State = 181; Match(SEMI); } } - State = 182; + State = 184; Match(Eof); } } @@ -415,29 +418,29 @@ public IfThenStatementContext ifThenStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 184; - Match(IF); State = 186; + Match(IF); + State = 188; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 185; + State = 187; _localctx.not = Match(NOT); } } - State = 188; + State = 190; Match(EXISTS); - State = 189; + State = 191; Match(LPAREN); - State = 190; + State = 192; queryExpression(); - State = 191; + State = 193; Match(RPAREN); - State = 192; + State = 194; Match(THEN); - State = 193; + State = 195; thenBody(); } } @@ -504,83 +507,83 @@ public ThenBodyContext thenBody() { ThenBodyContext _localctx = new ThenBodyContext(Context, State); EnterRule(_localctx, 4, RULE_thenBody); try { - State = 206; + State = 208; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,4,Context) ) { case 1: EnterOuterAlt(_localctx, 1); { - State = 195; + State = 197; createTableStatement(); } break; case 2: EnterOuterAlt(_localctx, 2); { - State = 196; + State = 198; createIndexStatement(); } break; case 3: EnterOuterAlt(_localctx, 3); { - State = 197; + State = 199; createViewStatement(); } break; case 4: EnterOuterAlt(_localctx, 4); { - State = 198; + State = 200; createProcedureStatement(); } break; case 5: EnterOuterAlt(_localctx, 5); { - State = 199; + State = 201; alterTableStatement(); } break; case 6: EnterOuterAlt(_localctx, 6); { - State = 200; + State = 202; dropStatement(); } break; case 7: EnterOuterAlt(_localctx, 7); { - State = 201; + State = 203; insertStatement(); } break; case 8: EnterOuterAlt(_localctx, 8); { - State = 202; + State = 204; updateStatement(); } break; case 9: EnterOuterAlt(_localctx, 9); { - State = 203; + State = 205; deleteStatement(); } break; case 10: EnterOuterAlt(_localctx, 10); { - State = 204; + State = 206; executeStatement(); } break; case 11: EnterOuterAlt(_localctx, 11); { - State = 205; + State = 207; queryExpression(); } break; @@ -635,7 +638,7 @@ public ExecuteStatementContext executeStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 208; + State = 210; _la = TokenStream.LA(1); if ( !(_la==EXECUTE || _la==EXEC) ) { ErrorHandler.RecoverInline(this); @@ -644,28 +647,28 @@ public ExecuteStatementContext executeStatement() { ErrorHandler.ReportMatch(this); Consume(); } - State = 209; + State = 211; _localctx.name = identifier(); - State = 218; + State = 220; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775694237952L) != 0) || ((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & -283699838490247167L) != 0) || ((((_la - 149)) & ~0x3f) == 0 && ((1L << (_la - 149)) & 31L) != 0)) { + if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775694237952L) != 0) || ((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & -1134799353958629375L) != 0) || ((((_la - 149)) & ~0x3f) == 0 && ((1L << (_la - 149)) & 127L) != 0)) { { - State = 210; + State = 212; expression(0); - State = 215; + State = 217; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 211; + State = 213; Match(COMMA); - State = 212; + State = 214; expression(0); } } - State = 217; + State = 219; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -707,6 +710,9 @@ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMMA(int i) { [System.Diagnostics.DebuggerNonUserCode] public WhereClauseContext whereClause() { return GetRuleContext(0); } + [System.Diagnostics.DebuggerNonUserCode] public OwnerAccessOptionContext ownerAccessOption() { + return GetRuleContext(0); + } public UpdateStatementContext(ParserRuleContext parent, int invokingState) : base(parent, invokingState) { @@ -728,56 +734,66 @@ public UpdateStatementContext updateStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 220; + State = 222; Match(UPDATE); - State = 221; + State = 223; tableSource(); - State = 226; + State = 228; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 222; + State = 224; Match(COMMA); - State = 223; + State = 225; tableSource(); } } - State = 228; + State = 230; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 229; + State = 231; Match(SET); - State = 230; + State = 232; assignment(); - State = 235; + State = 237; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 231; + State = 233; Match(COMMA); - State = 232; + State = 234; assignment(); } } - State = 237; + State = 239; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 239; + State = 241; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 238; + State = 240; whereClause(); } } + State = 244; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + if (_la==WITH) { + { + State = 243; + ownerAccessOption(); + } + } + } } catch (RecognitionException re) { @@ -820,11 +836,11 @@ public AssignmentContext assignment() { try { EnterOuterAlt(_localctx, 1); { - State = 241; + State = 246; _localctx.target = columnRef(); - State = 242; + State = 247; Match(EQ); - State = 243; + State = 248; expression(0); } } @@ -858,6 +874,9 @@ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMMA(int i) { [System.Diagnostics.DebuggerNonUserCode] public WhereClauseContext whereClause() { return GetRuleContext(0); } + [System.Diagnostics.DebuggerNonUserCode] public OwnerAccessOptionContext ownerAccessOption() { + return GetRuleContext(0); + } [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() { return GetRuleContext(0); } @@ -882,9 +901,9 @@ public DeleteStatementContext deleteStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 245; + State = 250; Match(DELETE); - State = 251; + State = 256; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -903,21 +922,23 @@ public DeleteStatementContext deleteStatement() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case BRACKET_ID: case BACKTICK_ID: case IDENTIFIER: { - State = 246; + State = 251; _localctx.target = identifier(); - State = 247; + State = 252; Match(DOT); - State = 248; + State = 253; Match(STAR); } break; case STAR: { - State = 250; + State = 255; Match(STAR); } break; @@ -926,36 +947,46 @@ public DeleteStatementContext deleteStatement() { default: break; } - State = 253; + State = 258; Match(FROM); - State = 254; - tableSource(); State = 259; + tableSource(); + State = 264; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 255; + State = 260; Match(COMMA); - State = 256; + State = 261; tableSource(); } } - State = 261; + State = 266; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 263; + State = 268; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 262; + State = 267; whereClause(); } } + State = 271; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + if (_la==WITH) { + { + State = 270; + ownerAccessOption(); + } + } + } } catch (RecognitionException re) { @@ -1002,23 +1033,23 @@ public SystemVariableSelectContext systemVariableSelect() { try { EnterOuterAlt(_localctx, 1); { - State = 265; + State = 273; Match(SELECT); - State = 266; + State = 274; sysVarItem(); - State = 271; + State = 279; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 267; + State = 275; Match(COMMA); - State = 268; + State = 276; sysVarItem(); } } - State = 273; + State = 281; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -1063,24 +1094,24 @@ public SysVarItemContext sysVarItem() { try { EnterOuterAlt(_localctx, 1); { - State = 274; + State = 282; Match(SYSVAR); - State = 279; + State = 287; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { + if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 492581209767931L) != 0)) { { - State = 276; + State = 284; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 275; + State = 283; Match(AS); } } - State = 278; + State = 286; _localctx.alias = identifier(); } } @@ -1132,27 +1163,27 @@ public ParametersClauseContext parametersClause() { try { EnterOuterAlt(_localctx, 1); { - State = 281; + State = 289; Match(PARAMETERS); - State = 282; + State = 290; procParam(); - State = 287; + State = 295; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 283; + State = 291; Match(COMMA); - State = 284; + State = 292; procParam(); } } - State = 289; + State = 297; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 290; + State = 298; Match(SEMI); } } @@ -1216,61 +1247,61 @@ public CreateTableStatementContext createTableStatement() { int _alt; EnterOuterAlt(_localctx, 1); { - State = 292; + State = 300; Match(CREATE); - State = 294; + State = 302; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TEMPORARY) { { - State = 293; + State = 301; _localctx.temp = Match(TEMPORARY); } } - State = 296; + State = 304; Match(TABLE); - State = 297; + State = 305; _localctx.table = identifier(); - State = 298; + State = 306; Match(LPAREN); - State = 299; + State = 307; columnDefinition(); - State = 304; + State = 312; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,18,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,20,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 300; + State = 308; Match(COMMA); - State = 301; + State = 309; columnDefinition(); } } } - State = 306; + State = 314; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,18,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,20,Context); } - State = 311; + State = 319; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 307; + State = 315; Match(COMMA); - State = 308; + State = 316; tableConstraint(); } } - State = 313; + State = 321; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 314; + State = 322; Match(RPAREN); } } @@ -1329,47 +1360,47 @@ public CreateViewStatementContext createViewStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 316; + State = 324; Match(CREATE); - State = 317; + State = 325; Match(VIEW); - State = 318; + State = 326; _localctx.name = identifier(); - State = 330; + State = 338; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 319; + State = 327; Match(LPAREN); - State = 320; + State = 328; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); - State = 325; + State = 333; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 321; + State = 329; Match(COMMA); - State = 322; + State = 330; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); } } - State = 327; + State = 335; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 328; + State = 336; Match(RPAREN); } } - State = 332; + State = 340; Match(AS); - State = 333; + State = 341; _localctx.query = queryExpression(); } } @@ -1420,25 +1451,25 @@ public CreateProcedureStatementContext createProcedureStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 335; + State = 343; Match(CREATE); - State = 336; + State = 344; Match(PROCEDURE); - State = 337; + State = 345; _localctx.name = identifier(); - State = 339; + State = 347; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123283815137275L) != 0)) { + if (((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 493135260549115L) != 0)) { { - State = 338; + State = 346; procParamList(); } } - State = 341; + State = 349; Match(AS); - State = 342; + State = 350; _localctx.body = procedureBody(); } } @@ -1485,33 +1516,33 @@ public ProcParamListContext procParamList() { EnterRule(_localctx, 26, RULE_procParamList); int _la; try { - State = 363; + State = 371; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case LPAREN: EnterOuterAlt(_localctx, 1); { - State = 344; + State = 352; Match(LPAREN); - State = 345; + State = 353; procParam(); - State = 350; + State = 358; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 346; + State = 354; Match(COMMA); - State = 347; + State = 355; procParam(); } } - State = 352; + State = 360; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 353; + State = 361; Match(RPAREN); } break; @@ -1531,27 +1562,29 @@ public ProcParamListContext procParamList() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case PARAM: case BRACKET_ID: case BACKTICK_ID: case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 355; + State = 363; procParam(); - State = 360; + State = 368; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 356; + State = 364; Match(COMMA); - State = 357; + State = 365; procParam(); } } - State = 362; + State = 370; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -1600,9 +1633,9 @@ public ProcParamContext procParam() { try { EnterOuterAlt(_localctx, 1); { - State = 365; + State = 373; _localctx.pname = procParamName(); - State = 366; + State = 374; dataType(); } } @@ -1640,7 +1673,7 @@ public ProcParamNameContext procParamName() { ProcParamNameContext _localctx = new ProcParamNameContext(Context, State); EnterRule(_localctx, 30, RULE_procParamName); try { - State = 370; + State = 378; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -1659,19 +1692,21 @@ public ProcParamNameContext procParamName() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case BRACKET_ID: case BACKTICK_ID: case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 368; + State = 376; columnRef(); } break; case PARAM: EnterOuterAlt(_localctx, 2); { - State = 369; + State = 377; Match(PARAM); } break; @@ -1720,13 +1755,13 @@ public AlterTableStatementContext alterTableStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 372; + State = 380; Match(ALTER); - State = 373; + State = 381; Match(TABLE); - State = 374; + State = 382; _localctx.table = identifier(); - State = 375; + State = 383; alterTableAction(); } } @@ -1933,26 +1968,26 @@ public AlterTableActionContext alterTableAction() { EnterRule(_localctx, 34, RULE_alterTableAction); int _la; try { - State = 434; + State = 442; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,32,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,34,Context) ) { case 1: _localctx = new AddColumnActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 377; + State = 385; Match(ADD); - State = 379; + State = 387; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 378; + State = 386; Match(COLUMN); } } - State = 381; + State = 389; columnDefinition(); } break; @@ -1960,9 +1995,9 @@ public AlterTableActionContext alterTableAction() { _localctx = new AddConstraintActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 382; + State = 390; Match(ADD); - State = 383; + State = 391; tableConstraint(); } break; @@ -1970,33 +2005,33 @@ public AlterTableActionContext alterTableAction() { _localctx = new AlterColumnActionContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 384; + State = 392; Match(ALTER); - State = 386; + State = 394; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 385; + State = 393; Match(COLUMN); } } - State = 388; + State = 396; ((AlterColumnActionContext)_localctx).field = identifier(); - State = 389; + State = 397; dataType(); - State = 393; + State = 401; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 68736821269L) != 0)) { { { - State = 390; + State = 398; columnConstraint(); } } - State = 395; + State = 403; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -2006,25 +2041,25 @@ public AlterTableActionContext alterTableAction() { _localctx = new AlterColumnSetDefaultActionContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 396; + State = 404; Match(ALTER); - State = 398; + State = 406; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 397; + State = 405; Match(COLUMN); } } - State = 400; + State = 408; ((AlterColumnSetDefaultActionContext)_localctx).field = identifier(); - State = 401; + State = 409; Match(SET); - State = 402; + State = 410; Match(DEFAULT); - State = 403; + State = 411; expression(0); } break; @@ -2032,23 +2067,23 @@ public AlterTableActionContext alterTableAction() { _localctx = new AlterColumnDropDefaultActionContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 405; + State = 413; Match(ALTER); - State = 407; + State = 415; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COLUMN) { { - State = 406; + State = 414; Match(COLUMN); } } - State = 409; + State = 417; ((AlterColumnDropDefaultActionContext)_localctx).field = identifier(); - State = 410; + State = 418; Match(DROP); - State = 411; + State = 419; Match(DEFAULT); } break; @@ -2056,11 +2091,11 @@ public AlterTableActionContext alterTableAction() { _localctx = new DropColumnActionContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 413; + State = 421; Match(DROP); - State = 414; + State = 422; Match(COLUMN); - State = 415; + State = 423; ((DropColumnActionContext)_localctx).field = identifier(); } break; @@ -2068,11 +2103,11 @@ public AlterTableActionContext alterTableAction() { _localctx = new DropConstraintActionContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 416; + State = 424; Match(DROP); - State = 417; + State = 425; Match(CONSTRAINT); - State = 418; + State = 426; ((DropConstraintActionContext)_localctx).cname = identifier(); } break; @@ -2080,11 +2115,11 @@ public AlterTableActionContext alterTableAction() { _localctx = new RenameTableActionContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 419; + State = 427; Match(RENAME); - State = 420; + State = 428; Match(TO); - State = 421; + State = 429; ((RenameTableActionContext)_localctx).newName = identifier(); } break; @@ -2092,15 +2127,15 @@ public AlterTableActionContext alterTableAction() { _localctx = new RenameColumnActionContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 422; + State = 430; Match(RENAME); - State = 423; + State = 431; Match(COLUMN); - State = 424; + State = 432; ((RenameColumnActionContext)_localctx).field = identifier(); - State = 425; + State = 433; Match(TO); - State = 426; + State = 434; ((RenameColumnActionContext)_localctx).newName = identifier(); } break; @@ -2108,15 +2143,15 @@ public AlterTableActionContext alterTableAction() { _localctx = new RenameIndexActionContext(_localctx); EnterOuterAlt(_localctx, 10); { - State = 428; + State = 436; Match(RENAME); - State = 429; + State = 437; Match(INDEX); - State = 430; + State = 438; ((RenameIndexActionContext)_localctx).index = identifier(); - State = 431; + State = 439; Match(TO); - State = 432; + State = 440; ((RenameIndexActionContext)_localctx).newName = identifier(); } break; @@ -2167,7 +2202,7 @@ public ProcedureBodyContext procedureBody() { ProcedureBodyContext _localctx = new ProcedureBodyContext(Context, State); EnterRule(_localctx, 36, RULE_procedureBody); try { - State = 441; + State = 449; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: @@ -2175,35 +2210,35 @@ public ProcedureBodyContext procedureBody() { case LPAREN: EnterOuterAlt(_localctx, 1); { - State = 436; + State = 444; queryExpression(); } break; case INSERT: EnterOuterAlt(_localctx, 2); { - State = 437; + State = 445; insertStatement(); } break; case UPDATE: EnterOuterAlt(_localctx, 3); { - State = 438; + State = 446; updateStatement(); } break; case DELETE: EnterOuterAlt(_localctx, 4); { - State = 439; + State = 447; deleteStatement(); } break; case CREATE: EnterOuterAlt(_localctx, 5); { - State = 440; + State = 448; createTableStatement(); } break; @@ -2273,56 +2308,56 @@ public CreateIndexStatementContext createIndexStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 443; + State = 451; Match(CREATE); - State = 445; + State = 453; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==UNIQUE) { { - State = 444; + State = 452; _localctx.unique = Match(UNIQUE); } } - State = 447; + State = 455; Match(INDEX); - State = 448; + State = 456; _localctx.name = identifier(); - State = 449; + State = 457; Match(ON); - State = 450; + State = 458; _localctx.table = identifier(); - State = 451; + State = 459; Match(LPAREN); - State = 452; + State = 460; indexColumn(); - State = 457; + State = 465; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 453; + State = 461; Match(COMMA); - State = 454; + State = 462; indexColumn(); } } - State = 459; + State = 467; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 460; + State = 468; Match(RPAREN); - State = 463; + State = 471; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WITH) { { - State = 461; + State = 469; Match(WITH); - State = 462; + State = 470; withOption(); } } @@ -2423,18 +2458,18 @@ public DropStatementContext dropStatement() { DropStatementContext _localctx = new DropStatementContext(Context, State); EnterRule(_localctx, 40, RULE_dropStatement); try { - State = 480; + State = 488; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,37,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,39,Context) ) { case 1: _localctx = new DropTableStatementContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 465; + State = 473; Match(DROP); - State = 466; + State = 474; Match(TABLE); - State = 467; + State = 475; ((DropTableStatementContext)_localctx).table = identifier(); } break; @@ -2442,15 +2477,15 @@ public DropStatementContext dropStatement() { _localctx = new DropIndexStatementContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 468; + State = 476; Match(DROP); - State = 469; + State = 477; Match(INDEX); - State = 470; + State = 478; ((DropIndexStatementContext)_localctx).index = identifier(); - State = 471; + State = 479; Match(ON); - State = 472; + State = 480; ((DropIndexStatementContext)_localctx).table = identifier(); } break; @@ -2458,11 +2493,11 @@ public DropStatementContext dropStatement() { _localctx = new DropProcedureStatementContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 474; + State = 482; Match(DROP); - State = 475; + State = 483; Match(PROCEDURE); - State = 476; + State = 484; ((DropProcedureStatementContext)_localctx).proc = identifier(); } break; @@ -2470,11 +2505,11 @@ public DropStatementContext dropStatement() { _localctx = new DropViewStatementContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 477; + State = 485; Match(DROP); - State = 478; + State = 486; Match(VIEW); - State = 479; + State = 487; ((DropViewStatementContext)_localctx).view = identifier(); } break; @@ -2520,14 +2555,14 @@ public IndexColumnContext indexColumn() { try { EnterOuterAlt(_localctx, 1); { - State = 482; + State = 490; _localctx.col = identifier(); - State = 484; + State = 492; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ASC || _la==DESC) { { - State = 483; + State = 491; _localctx.dir = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -2603,14 +2638,14 @@ public WithOptionContext withOption() { WithOptionContext _localctx = new WithOptionContext(Context, State); EnterRule(_localctx, 44, RULE_withOption); try { - State = 491; + State = 499; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case PRIMARY: _localctx = new WithPrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 486; + State = 494; Match(PRIMARY); } break; @@ -2618,9 +2653,9 @@ public WithOptionContext withOption() { _localctx = new WithDisallowNullContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 487; + State = 495; Match(DISALLOW); - State = 488; + State = 496; Match(NULL); } break; @@ -2628,9 +2663,9 @@ public WithOptionContext withOption() { _localctx = new WithIgnoreNullContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 489; + State = 497; Match(IGNORE); - State = 490; + State = 498; Match(NULL); } break; @@ -2687,31 +2722,31 @@ public ColumnDefinitionContext columnDefinition() { try { EnterOuterAlt(_localctx, 1); { - State = 493; + State = 501; _localctx.name = identifier(); - State = 494; + State = 502; dataType(); - State = 496; + State = 504; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 495; + State = 503; calculatedClause(); } } - State = 501; + State = 509; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 68736821269L) != 0)) { { { - State = 498; + State = 506; columnConstraint(); } } - State = 503; + State = 511; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -2755,13 +2790,13 @@ public CalculatedClauseContext calculatedClause() { try { EnterOuterAlt(_localctx, 1); { - State = 504; + State = 512; Match(AS); - State = 505; + State = 513; Match(LPAREN); - State = 506; + State = 514; expression(0); - State = 507; + State = 515; Match(RPAREN); } } @@ -2820,7 +2855,7 @@ public DataTypeContext dataType() { try { EnterOuterAlt(_localctx, 1); { - State = 511; + State = 519; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -2839,65 +2874,67 @@ public DataTypeContext dataType() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case BRACKET_ID: case BACKTICK_ID: case IDENTIFIER: { - State = 509; + State = 517; _localctx.typeName = identifier(); } break; case IDENTITY: { - State = 510; + State = 518; _localctx.identityType = Match(IDENTITY); } break; default: throw new NoViableAltException(this); } - State = 514; + State = 522; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,43,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,45,Context) ) { case 1: { - State = 513; + State = 521; _localctx.extra = identifier(); } break; } - State = 517; + State = 525; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { + if (((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 492581209767931L) != 0)) { { - State = 516; + State = 524; _localctx.extra2 = identifier(); } } - State = 527; + State = 535; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 519; + State = 527; Match(LPAREN); - State = 520; + State = 528; _localctx.size = signedInteger(); - State = 523; + State = 531; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COMMA) { { - State = 521; + State = 529; Match(COMMA); - State = 522; + State = 530; _localctx.scale = signedInteger(); } } - State = 525; + State = 533; Match(RPAREN); } } @@ -2939,17 +2976,17 @@ public SignedIntegerContext signedInteger() { try { EnterOuterAlt(_localctx, 1); { - State = 530; + State = 538; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==MINUS) { { - State = 529; + State = 537; Match(MINUS); } } - State = 532; + State = 540; Match(INTEGER_LITERAL); } } @@ -3140,16 +3177,16 @@ public ColumnConstraintContext columnConstraint() { EnterRule(_localctx, 54, RULE_columnConstraint); int _la; try { - State = 603; + State = 611; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,59,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,61,Context) ) { case 1: _localctx = new NotNullConstraintContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 534; + State = 542; Match(NOT); - State = 535; + State = 543; Match(NULL); } break; @@ -3157,7 +3194,7 @@ public ColumnConstraintContext columnConstraint() { _localctx = new NullableConstraintContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 536; + State = 544; Match(NULL); } break; @@ -3165,9 +3202,9 @@ public ColumnConstraintContext columnConstraint() { _localctx = new DefaultConstraintContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 537; + State = 545; Match(DEFAULT); - State = 538; + State = 546; expression(0); } break; @@ -3175,9 +3212,9 @@ public ColumnConstraintContext columnConstraint() { _localctx = new CompressionConstraintContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 539; + State = 547; Match(WITH); - State = 540; + State = 548; _la = TokenStream.LA(1); if ( !(_la==COMPRESSION || _la==COMP) ) { ErrorHandler.RecoverInline(this); @@ -3192,25 +3229,25 @@ public ColumnConstraintContext columnConstraint() { _localctx = new CheckColumnConstraintContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 543; + State = 551; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 541; + State = 549; Match(CONSTRAINT); - State = 542; + State = 550; ((CheckColumnConstraintContext)_localctx).cname = identifier(); } } - State = 545; + State = 553; Match(CHECK); - State = 546; + State = 554; Match(LPAREN); - State = 547; + State = 555; checkBody(); - State = 548; + State = 556; Match(RPAREN); } break; @@ -3218,28 +3255,28 @@ public ColumnConstraintContext columnConstraint() { _localctx = new PrimaryKeyConstraintContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 552; + State = 560; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 550; + State = 558; Match(CONSTRAINT); - State = 551; + State = 559; ((PrimaryKeyConstraintContext)_localctx).cname = identifier(); } } - State = 554; + State = 562; Match(PRIMARY); - State = 555; + State = 563; Match(KEY); - State = 557; + State = 565; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 556; + State = 564; clusteredOption(); } } @@ -3250,26 +3287,26 @@ public ColumnConstraintContext columnConstraint() { _localctx = new UniqueColumnConstraintContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 561; + State = 569; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 559; + State = 567; Match(CONSTRAINT); - State = 560; + State = 568; ((UniqueColumnConstraintContext)_localctx).cname = identifier(); } } - State = 563; + State = 571; Match(UNIQUE); - State = 565; + State = 573; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 564; + State = 572; clusteredOption(); } } @@ -3280,65 +3317,65 @@ public ColumnConstraintContext columnConstraint() { _localctx = new ColumnReferencesConstraintContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 569; + State = 577; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 567; + State = 575; Match(CONSTRAINT); - State = 568; + State = 576; ((ColumnReferencesConstraintContext)_localctx).cname = identifier(); } } - State = 571; + State = 579; Match(REFERENCES); - State = 572; + State = 580; ((ColumnReferencesConstraintContext)_localctx).refTable = identifier(); - State = 584; + State = 592; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 573; + State = 581; Match(LPAREN); - State = 574; + State = 582; ((ColumnReferencesConstraintContext)_localctx)._identifier = identifier(); ((ColumnReferencesConstraintContext)_localctx)._refColumns.Add(((ColumnReferencesConstraintContext)_localctx)._identifier); - State = 579; + State = 587; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 575; + State = 583; Match(COMMA); - State = 576; + State = 584; ((ColumnReferencesConstraintContext)_localctx)._identifier = identifier(); ((ColumnReferencesConstraintContext)_localctx)._refColumns.Add(((ColumnReferencesConstraintContext)_localctx)._identifier); } } - State = 581; + State = 589; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 582; + State = 590; Match(RPAREN); } } - State = 589; + State = 597; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==ON) { { { - State = 586; + State = 594; foreignKeyAction(); } } - State = 591; + State = 599; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -3348,30 +3385,30 @@ public ColumnConstraintContext columnConstraint() { _localctx = new IdentityConstraintContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 592; + State = 600; Match(IDENTITY); - State = 601; + State = 609; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 593; + State = 601; Match(LPAREN); - State = 594; + State = 602; ((IdentityConstraintContext)_localctx).seed = signedInteger(); - State = 597; + State = 605; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COMMA) { { - State = 595; + State = 603; Match(COMMA); - State = 596; + State = 604; ((IdentityConstraintContext)_localctx).increment = signedInteger(); } } - State = 599; + State = 607; Match(RPAREN); } } @@ -3534,62 +3571,62 @@ public TableConstraintContext tableConstraint() { EnterRule(_localctx, 56, RULE_tableConstraint); int _la; try { - State = 694; + State = 702; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,73,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,75,Context) ) { case 1: _localctx = new PrimaryKeyTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 607; + State = 615; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 605; + State = 613; Match(CONSTRAINT); - State = 606; + State = 614; ((PrimaryKeyTableConstraintContext)_localctx).name = identifier(); } } - State = 609; + State = 617; Match(PRIMARY); - State = 610; + State = 618; Match(KEY); - State = 612; + State = 620; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 611; + State = 619; clusteredOption(); } } - State = 614; + State = 622; Match(LPAREN); - State = 615; + State = 623; ((PrimaryKeyTableConstraintContext)_localctx)._identifier = identifier(); ((PrimaryKeyTableConstraintContext)_localctx)._columns.Add(((PrimaryKeyTableConstraintContext)_localctx)._identifier); - State = 620; + State = 628; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 616; + State = 624; Match(COMMA); - State = 617; + State = 625; ((PrimaryKeyTableConstraintContext)_localctx)._identifier = identifier(); ((PrimaryKeyTableConstraintContext)_localctx)._columns.Add(((PrimaryKeyTableConstraintContext)_localctx)._identifier); } } - State = 622; + State = 630; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 623; + State = 631; Match(RPAREN); } break; @@ -3597,53 +3634,53 @@ public TableConstraintContext tableConstraint() { _localctx = new UniqueTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 627; + State = 635; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 625; + State = 633; Match(CONSTRAINT); - State = 626; + State = 634; ((UniqueTableConstraintContext)_localctx).name = identifier(); } } - State = 629; + State = 637; Match(UNIQUE); - State = 631; + State = 639; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 630; + State = 638; clusteredOption(); } } - State = 633; + State = 641; Match(LPAREN); - State = 634; + State = 642; ((UniqueTableConstraintContext)_localctx)._identifier = identifier(); ((UniqueTableConstraintContext)_localctx)._columns.Add(((UniqueTableConstraintContext)_localctx)._identifier); - State = 639; + State = 647; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 635; + State = 643; Match(COMMA); - State = 636; + State = 644; ((UniqueTableConstraintContext)_localctx)._identifier = identifier(); ((UniqueTableConstraintContext)_localctx)._columns.Add(((UniqueTableConstraintContext)_localctx)._identifier); } } - State = 641; + State = 649; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 642; + State = 650; Match(RPAREN); } break; @@ -3651,105 +3688,105 @@ public TableConstraintContext tableConstraint() { _localctx = new ForeignKeyTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 646; + State = 654; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 644; + State = 652; Match(CONSTRAINT); - State = 645; + State = 653; ((ForeignKeyTableConstraintContext)_localctx).name = identifier(); } } - State = 648; + State = 656; Match(FOREIGN); - State = 649; + State = 657; Match(KEY); - State = 652; + State = 660; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NO) { { - State = 650; + State = 658; ((ForeignKeyTableConstraintContext)_localctx).noIndex = Match(NO); - State = 651; + State = 659; Match(INDEX); } } - State = 654; + State = 662; Match(LPAREN); - State = 655; + State = 663; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._columns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); - State = 660; + State = 668; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 656; + State = 664; Match(COMMA); - State = 657; + State = 665; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._columns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); } } - State = 662; + State = 670; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 663; + State = 671; Match(RPAREN); - State = 664; + State = 672; Match(REFERENCES); - State = 665; + State = 673; ((ForeignKeyTableConstraintContext)_localctx).refTable = identifier(); - State = 677; + State = 685; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 666; + State = 674; Match(LPAREN); - State = 667; + State = 675; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._refColumns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); - State = 672; + State = 680; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 668; + State = 676; Match(COMMA); - State = 669; + State = 677; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._refColumns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); } } - State = 674; + State = 682; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 675; + State = 683; Match(RPAREN); } } - State = 682; + State = 690; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==ON) { { { - State = 679; + State = 687; foreignKeyAction(); } } - State = 684; + State = 692; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -3759,25 +3796,25 @@ public TableConstraintContext tableConstraint() { _localctx = new CheckTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 687; + State = 695; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 685; + State = 693; Match(CONSTRAINT); - State = 686; + State = 694; ((CheckTableConstraintContext)_localctx).name = identifier(); } } - State = 689; + State = 697; Match(CHECK); - State = 690; + State = 698; Match(LPAREN); - State = 691; + State = 699; checkBody(); - State = 692; + State = 700; Match(RPAREN); } break; @@ -3830,12 +3867,12 @@ public CheckBodyContext checkBody() { try { EnterOuterAlt(_localctx, 1); { - State = 703; + State = 711; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - while ((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & -1L) != 0) || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 536869887L) != 0)) { + while ((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & -1L) != 0) || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 2147479551L) != 0)) { { - State = 701; + State = 709; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: @@ -3961,6 +3998,8 @@ public CheckBodyContext checkBody() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case STAR: case SLASH: case BACKSLASH: @@ -3993,7 +4032,7 @@ public CheckBodyContext checkBody() { case LINE_COMMENT: case BLOCK_COMMENT: { - State = 696; + State = 704; _la = TokenStream.LA(1); if ( _la <= 0 || (_la==LPAREN || _la==RPAREN) ) { ErrorHandler.RecoverInline(this); @@ -4006,11 +4045,11 @@ public CheckBodyContext checkBody() { break; case LPAREN: { - State = 697; + State = 705; Match(LPAREN); - State = 698; + State = 706; checkBody(); - State = 699; + State = 707; Match(RPAREN); } break; @@ -4018,7 +4057,7 @@ public CheckBodyContext checkBody() { throw new NoViableAltException(this); } } - State = 705; + State = 713; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4081,18 +4120,18 @@ public ForeignKeyActionContext foreignKeyAction() { ForeignKeyActionContext _localctx = new ForeignKeyActionContext(Context, State); EnterRule(_localctx, 60, RULE_foreignKeyAction); try { - State = 712; + State = 720; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,76,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,78,Context) ) { case 1: _localctx = new OnUpdateActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 706; + State = 714; Match(ON); - State = 707; + State = 715; Match(UPDATE); - State = 708; + State = 716; referentialAction(); } break; @@ -4100,11 +4139,11 @@ public ForeignKeyActionContext foreignKeyAction() { _localctx = new OnDeleteActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 709; + State = 717; Match(ON); - State = 710; + State = 718; Match(DELETE); - State = 711; + State = 719; referentialAction(); } break; @@ -4192,14 +4231,14 @@ public ReferentialActionContext referentialAction() { ReferentialActionContext _localctx = new ReferentialActionContext(Context, State); EnterRule(_localctx, 62, RULE_referentialAction); try { - State = 722; + State = 730; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,77,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,79,Context) ) { case 1: _localctx = new CascadeActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 714; + State = 722; Match(CASCADE); } break; @@ -4207,9 +4246,9 @@ public ReferentialActionContext referentialAction() { _localctx = new NoActionActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 715; + State = 723; Match(NO); - State = 716; + State = 724; Match(ACTION); } break; @@ -4217,7 +4256,7 @@ public ReferentialActionContext referentialAction() { _localctx = new RestrictActionContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 717; + State = 725; Match(RESTRICT); } break; @@ -4225,9 +4264,9 @@ public ReferentialActionContext referentialAction() { _localctx = new SetNullActionContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 718; + State = 726; Match(SET); - State = 719; + State = 727; Match(NULL); } break; @@ -4235,9 +4274,9 @@ public ReferentialActionContext referentialAction() { _localctx = new SetDefaultActionContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 720; + State = 728; Match(SET); - State = 721; + State = 729; Match(DEFAULT); } break; @@ -4299,60 +4338,60 @@ public InsertStatementContext insertStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 724; + State = 732; Match(INSERT); - State = 725; + State = 733; Match(INTO); - State = 726; + State = 734; _localctx.table = identifier(); - State = 743; + State = 751; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: case VALUES: case LPAREN: { - State = 738; + State = 746; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,79,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,81,Context) ) { case 1: { - State = 727; + State = 735; Match(LPAREN); - State = 728; + State = 736; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); - State = 733; + State = 741; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 729; + State = 737; Match(COMMA); - State = 730; + State = 738; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); } } - State = 735; + State = 743; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 736; + State = 744; Match(RPAREN); } break; } - State = 740; + State = 748; _localctx.source = queryExpression(); } break; case DEFAULT: { - State = 741; + State = 749; Match(DEFAULT); - State = 742; + State = 750; Match(VALUES); } break; @@ -4406,27 +4445,27 @@ public RowValuesContext rowValues() { try { EnterOuterAlt(_localctx, 1); { - State = 745; + State = 753; Match(LPAREN); - State = 746; + State = 754; rowValue(); - State = 751; + State = 759; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 747; + State = 755; Match(COMMA); - State = 748; + State = 756; rowValue(); } } - State = 753; + State = 761; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 754; + State = 762; Match(RPAREN); } } @@ -4464,13 +4503,13 @@ public RowValueContext rowValue() { RowValueContext _localctx = new RowValueContext(Context, State); EnterRule(_localctx, 68, RULE_rowValue); try { - State = 758; + State = 766; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case DEFAULT: EnterOuterAlt(_localctx, 1); { - State = 756; + State = 764; Match(DEFAULT); } break; @@ -4504,6 +4543,8 @@ public RowValueContext rowValue() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case PLUS: case MINUS: case LPAREN: @@ -4520,7 +4561,7 @@ public RowValueContext rowValue() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 757; + State = 765; expression(0); } break; @@ -4558,6 +4599,9 @@ [System.Diagnostics.DebuggerNonUserCode] public OrderByClauseContext orderByClau [System.Diagnostics.DebuggerNonUserCode] public OffsetFetchClauseContext offsetFetchClause() { return GetRuleContext(0); } + [System.Diagnostics.DebuggerNonUserCode] public OwnerAccessOptionContext ownerAccessOption() { + return GetRuleContext(0); + } public QueryExpressionContext(ParserRuleContext parent, int invokingState) : base(parent, invokingState) { @@ -4579,44 +4623,97 @@ public QueryExpressionContext queryExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 760; + State = 768; queryTerm(); - State = 766; + State = 774; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 117093590311632896L) != 0)) { { { - State = 761; + State = 769; setOperator(); - State = 762; + State = 770; queryTerm(); } } - State = 768; + State = 776; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 770; + State = 778; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ORDER) { { - State = 769; + State = 777; orderByClause(); } } - State = 773; + State = 781; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OFFSET || _la==FETCH) { { - State = 772; + State = 780; offsetFetchClause(); } } + State = 784; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + if (_la==WITH) { + { + State = 783; + ownerAccessOption(); + } + } + + } + } + catch (RecognitionException re) { + _localctx.exception = re; + ErrorHandler.ReportError(this, re); + ErrorHandler.Recover(this, re); + } + finally { + ExitRule(); + } + return _localctx; + } + + public partial class OwnerAccessOptionContext : ParserRuleContext { + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode WITH() { return GetToken(AccessSqlParser.WITH, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OWNERACCESS() { return GetToken(AccessSqlParser.OWNERACCESS, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OPTION() { return GetToken(AccessSqlParser.OPTION, 0); } + public OwnerAccessOptionContext(ParserRuleContext parent, int invokingState) + : base(parent, invokingState) + { + } + public override int RuleIndex { get { return RULE_ownerAccessOption; } } + [System.Diagnostics.DebuggerNonUserCode] + public override TResult Accept(IParseTreeVisitor visitor) { + IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor; + if (typedVisitor != null) return typedVisitor.VisitOwnerAccessOption(this); + else return visitor.VisitChildren(this); + } + } + + [RuleVersion(0)] + public OwnerAccessOptionContext ownerAccessOption() { + OwnerAccessOptionContext _localctx = new OwnerAccessOptionContext(Context, State); + EnterRule(_localctx, 72, RULE_ownerAccessOption); + try { + EnterOuterAlt(_localctx, 1); + { + State = 786; + Match(WITH); + State = 787; + Match(OWNERACCESS); + State = 788; + Match(OPTION); } } catch (RecognitionException re) { @@ -4692,17 +4789,17 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public QueryTermContext queryTerm() { QueryTermContext _localctx = new QueryTermContext(Context, State); - EnterRule(_localctx, 72, RULE_queryTerm); + EnterRule(_localctx, 74, RULE_queryTerm); int _la; try { - State = 789; + State = 804; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: _localctx = new SelectTermContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 775; + State = 790; querySpecification(); } break; @@ -4710,11 +4807,11 @@ public QueryTermContext queryTerm() { _localctx = new ParenTermContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 776; + State = 791; Match(LPAREN); - State = 777; + State = 792; queryExpression(); - State = 778; + State = 793; Match(RPAREN); } break; @@ -4722,23 +4819,23 @@ public QueryTermContext queryTerm() { _localctx = new ValuesTermContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 780; + State = 795; Match(VALUES); - State = 781; + State = 796; rowValues(); - State = 786; + State = 801; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 782; + State = 797; Match(COMMA); - State = 783; + State = 798; rowValues(); } } - State = 788; + State = 803; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4780,23 +4877,23 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public SetOperatorContext setOperator() { SetOperatorContext _localctx = new SetOperatorContext(Context, State); - EnterRule(_localctx, 74, RULE_setOperator); + EnterRule(_localctx, 76, RULE_setOperator); int _la; try { - State = 797; + State = 812; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case UNION: EnterOuterAlt(_localctx, 1); { - State = 791; + State = 806; Match(UNION); - State = 793; + State = 808; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ALL) { { - State = 792; + State = 807; Match(ALL); } } @@ -4806,14 +4903,14 @@ public SetOperatorContext setOperator() { case INTERSECT: EnterOuterAlt(_localctx, 2); { - State = 795; + State = 810; Match(INTERSECT); } break; case EXCEPT: EnterOuterAlt(_localctx, 3); { - State = 796; + State = 811; Match(EXCEPT); } break; @@ -4855,6 +4952,9 @@ [System.Diagnostics.DebuggerNonUserCode] public GroupByClauseContext groupByClau [System.Diagnostics.DebuggerNonUserCode] public HavingClauseContext havingClause() { return GetRuleContext(0); } + [System.Diagnostics.DebuggerNonUserCode] public OwnerAccessOptionContext ownerAccessOption() { + return GetRuleContext(0); + } [System.Diagnostics.DebuggerNonUserCode] public SelectPredicateContext selectPredicate() { return GetRuleContext(0); } @@ -4877,87 +4977,97 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public QuerySpecificationContext querySpecification() { QuerySpecificationContext _localctx = new QuerySpecificationContext(Context, State); - EnterRule(_localctx, 76, RULE_querySpecification); + EnterRule(_localctx, 78, RULE_querySpecification); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 799; + State = 814; Match(SELECT); - State = 801; + State = 816; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 18014450049089536L) != 0)) { { - State = 800; + State = 815; _localctx.predicate = selectPredicate(); } } - State = 804; + State = 819; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TOP) { { - State = 803; + State = 818; topClause(); } } - State = 806; + State = 821; selectList(); - State = 809; + State = 824; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==INTO) { { - State = 807; + State = 822; Match(INTO); - State = 808; + State = 823; _localctx.into = identifier(); } } - State = 812; + State = 827; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FROM) { { - State = 811; + State = 826; fromClause(); } } - State = 815; + State = 830; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 814; + State = 829; whereClause(); } } - State = 818; + State = 833; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==GROUP) { { - State = 817; + State = 832; groupByClause(); } } - State = 821; + State = 836; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==HAVING) { { - State = 820; + State = 835; havingClause(); } } + State = 839; + ErrorHandler.Sync(this); + switch ( Interpreter.AdaptivePredict(TokenStream,100,Context) ) { + case 1: + { + State = 838; + ownerAccessOption(); + } + break; + } } } catch (RecognitionException re) { @@ -4991,12 +5101,12 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public SelectPredicateContext selectPredicate() { SelectPredicateContext _localctx = new SelectPredicateContext(Context, State); - EnterRule(_localctx, 78, RULE_selectPredicate); + EnterRule(_localctx, 80, RULE_selectPredicate); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 823; + State = 841; _la = TokenStream.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 18014450049089536L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -5047,30 +5157,30 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public GroupByClauseContext groupByClause() { GroupByClauseContext _localctx = new GroupByClauseContext(Context, State); - EnterRule(_localctx, 80, RULE_groupByClause); + EnterRule(_localctx, 82, RULE_groupByClause); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 825; + State = 843; Match(GROUP); - State = 826; + State = 844; Match(BY); - State = 827; + State = 845; expression(0); - State = 832; + State = 850; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 828; + State = 846; Match(COMMA); - State = 829; + State = 847; expression(0); } } - State = 834; + State = 852; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5108,13 +5218,13 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public HavingClauseContext havingClause() { HavingClauseContext _localctx = new HavingClauseContext(Context, State); - EnterRule(_localctx, 82, RULE_havingClause); + EnterRule(_localctx, 84, RULE_havingClause); try { EnterOuterAlt(_localctx, 1); { - State = 835; + State = 853; Match(HAVING); - State = 836; + State = 854; expression(0); } } @@ -5166,24 +5276,24 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public TopClauseContext topClause() { TopClauseContext _localctx = new TopClauseContext(Context, State); - EnterRule(_localctx, 84, RULE_topClause); + EnterRule(_localctx, 86, RULE_topClause); int _la; try { int _alt; EnterOuterAlt(_localctx, 1); { - State = 838; + State = 856; Match(TOP); - State = 839; + State = 857; topOperand(); - State = 844; + State = 862; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,98,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,102,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 840; + State = 858; _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { ErrorHandler.RecoverInline(this); @@ -5192,33 +5302,33 @@ public TopClauseContext topClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 841; + State = 859; topOperand(); } } } - State = 846; + State = 864; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,98,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,102,Context); } - State = 848; + State = 866; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PERCENT) { { - State = 847; + State = 865; _localctx.percent = Match(PERCENT); } } - State = 852; + State = 870; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WITH) { { - State = 850; + State = 868; Match(WITH); - State = 851; + State = 869; _localctx.ties = Match(TIES); } } @@ -5260,33 +5370,33 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public TopOperandContext topOperand() { TopOperandContext _localctx = new TopOperandContext(Context, State); - EnterRule(_localctx, 86, RULE_topOperand); + EnterRule(_localctx, 88, RULE_topOperand); try { - State = 860; + State = 878; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: EnterOuterAlt(_localctx, 1); { - State = 854; + State = 872; Match(INTEGER_LITERAL); } break; case PARAM: EnterOuterAlt(_localctx, 2); { - State = 855; + State = 873; Match(PARAM); } break; case LPAREN: EnterOuterAlt(_localctx, 3); { - State = 856; + State = 874; Match(LPAREN); - State = 857; + State = 875; expression(0); - State = 858; + State = 876; Match(RPAREN); } break; @@ -5344,29 +5454,29 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public OffsetFetchClauseContext offsetFetchClause() { OffsetFetchClauseContext _localctx = new OffsetFetchClauseContext(Context, State); - EnterRule(_localctx, 88, RULE_offsetFetchClause); + EnterRule(_localctx, 90, RULE_offsetFetchClause); int _la; try { - State = 885; + State = 903; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case OFFSET: EnterOuterAlt(_localctx, 1); { - State = 862; + State = 880; Match(OFFSET); - State = 863; + State = 881; _localctx.offset = expression(0); - State = 864; + State = 882; rowKeyword(); - State = 874; + State = 892; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FETCH) { { - State = 865; + State = 883; Match(FETCH); - State = 866; + State = 884; _la = TokenStream.LA(1); if ( !(_la==NEXT || _la==FIRST) ) { ErrorHandler.RecoverInline(this); @@ -5375,24 +5485,24 @@ public OffsetFetchClauseContext offsetFetchClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 867; + State = 885; _localctx.limit = expression(0); - State = 868; + State = 886; rowKeyword(); - State = 872; + State = 890; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case ONLY: { - State = 869; + State = 887; Match(ONLY); } break; case WITH: { - State = 870; + State = 888; Match(WITH); - State = 871; + State = 889; _localctx.ties = Match(TIES); } break; @@ -5407,9 +5517,9 @@ public OffsetFetchClauseContext offsetFetchClause() { case FETCH: EnterOuterAlt(_localctx, 2); { - State = 876; + State = 894; Match(FETCH); - State = 877; + State = 895; _la = TokenStream.LA(1); if ( !(_la==NEXT || _la==FIRST) ) { ErrorHandler.RecoverInline(this); @@ -5418,24 +5528,24 @@ public OffsetFetchClauseContext offsetFetchClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 878; + State = 896; _localctx.limit = expression(0); - State = 879; + State = 897; rowKeyword(); - State = 883; + State = 901; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case ONLY: { - State = 880; + State = 898; Match(ONLY); } break; case WITH: { - State = 881; + State = 899; Match(WITH); - State = 882; + State = 900; _localctx.ties = Match(TIES); } break; @@ -5478,12 +5588,12 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public RowKeywordContext rowKeyword() { RowKeywordContext _localctx = new RowKeywordContext(Context, State); - EnterRule(_localctx, 90, RULE_rowKeyword); + EnterRule(_localctx, 92, RULE_rowKeyword); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 887; + State = 905; _la = TokenStream.LA(1); if ( !(_la==ROWS || _la==ROW) ) { ErrorHandler.RecoverInline(this); @@ -5533,16 +5643,16 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public SelectListContext selectList() { SelectListContext _localctx = new SelectListContext(Context, State); - EnterRule(_localctx, 92, RULE_selectList); + EnterRule(_localctx, 94, RULE_selectList); int _la; try { - State = 898; + State = 916; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: EnterOuterAlt(_localctx, 1); { - State = 889; + State = 907; Match(STAR); } break; @@ -5576,6 +5686,8 @@ public SelectListContext selectList() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case PLUS: case MINUS: case LPAREN: @@ -5592,21 +5704,21 @@ public SelectListContext selectList() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 890; + State = 908; selectItem(); - State = 895; + State = 913; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 891; + State = 909; Match(COMMA); - State = 892; + State = 910; selectItem(); } } - State = 897; + State = 915; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5675,21 +5787,21 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public SelectItemContext selectItem() { SelectItemContext _localctx = new SelectItemContext(Context, State); - EnterRule(_localctx, 94, RULE_selectItem); + EnterRule(_localctx, 96, RULE_selectItem); int _la; try { - State = 911; + State = 929; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,110,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,114,Context) ) { case 1: _localctx = new QualifiedStarSelectItemContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 900; + State = 918; ((QualifiedStarSelectItemContext)_localctx).qualifier = identifier(); - State = 901; + State = 919; Match(DOT); - State = 902; + State = 920; Match(STAR); } break; @@ -5697,24 +5809,24 @@ public SelectItemContext selectItem() { _localctx = new ExpressionSelectItemContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 904; + State = 922; expression(0); - State = 909; + State = 927; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { + if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 492581209767931L) != 0)) { { - State = 906; + State = 924; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 905; + State = 923; Match(AS); } } - State = 908; + State = 926; ((ExpressionSelectItemContext)_localctx).alias = identifier(); } } @@ -5762,28 +5874,28 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public FromClauseContext fromClause() { FromClauseContext _localctx = new FromClauseContext(Context, State); - EnterRule(_localctx, 96, RULE_fromClause); + EnterRule(_localctx, 98, RULE_fromClause); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 913; + State = 931; Match(FROM); - State = 914; + State = 932; tableSource(); - State = 919; + State = 937; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 915; + State = 933; Match(COMMA); - State = 916; + State = 934; tableSource(); } } - State = 921; + State = 939; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5826,24 +5938,24 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public TableSourceContext tableSource() { TableSourceContext _localctx = new TableSourceContext(Context, State); - EnterRule(_localctx, 98, RULE_tableSource); + EnterRule(_localctx, 100, RULE_tableSource); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 922; + State = 940; tablePrimary(); - State = 926; + State = 944; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 137455468544L) != 0)) { { { - State = 923; + State = 941; joinClause(); } } - State = 928; + State = 946; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5930,34 +6042,34 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public TablePrimaryContext tablePrimary() { TablePrimaryContext _localctx = new TablePrimaryContext(Context, State); - EnterRule(_localctx, 100, RULE_tablePrimary); + EnterRule(_localctx, 102, RULE_tablePrimary); int _la; try { - State = 952; + State = 970; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,118,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,122,Context) ) { case 1: _localctx = new NamedTablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 929; + State = 947; ((NamedTablePrimaryContext)_localctx).table = identifier(); - State = 934; + State = 952; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { + if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 492581209767931L) != 0)) { { - State = 931; + State = 949; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 930; + State = 948; Match(AS); } } - State = 933; + State = 951; ((NamedTablePrimaryContext)_localctx).alias = identifier(); } } @@ -5968,35 +6080,35 @@ public TablePrimaryContext tablePrimary() { _localctx = new SubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 936; + State = 954; Match(LPAREN); - State = 937; + State = 955; queryExpression(); - State = 938; + State = 956; Match(RPAREN); - State = 946; + State = 964; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 123145302441979L) != 0)) { + if (_la==AS || ((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 492581209767931L) != 0)) { { - State = 940; + State = 958; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 939; + State = 957; Match(AS); } } - State = 942; + State = 960; ((SubqueryPrimaryContext)_localctx).alias = identifier(); - State = 944; + State = 962; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 943; + State = 961; derivedColumns(); } } @@ -6010,11 +6122,11 @@ public TablePrimaryContext tablePrimary() { _localctx = new ParenJoinPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 948; + State = 966; Match(LPAREN); - State = 949; + State = 967; tableSource(); - State = 950; + State = 968; Match(RPAREN); } break; @@ -6109,24 +6221,24 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public JoinClauseContext joinClause() { JoinClauseContext _localctx = new JoinClauseContext(Context, State); - EnterRule(_localctx, 102, RULE_joinClause); + EnterRule(_localctx, 104, RULE_joinClause); try { - State = 969; + State = 987; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,119,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,123,Context) ) { case 1: _localctx = new ConditionalJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 954; + State = 972; joinType(); - State = 955; + State = 973; Match(JOIN); - State = 956; + State = 974; tablePrimary(); - State = 957; + State = 975; Match(ON); - State = 958; + State = 976; expression(0); } break; @@ -6134,11 +6246,11 @@ public JoinClauseContext joinClause() { _localctx = new CrossJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 960; + State = 978; Match(CROSS); - State = 961; + State = 979; Match(JOIN); - State = 962; + State = 980; tablePrimary(); } break; @@ -6146,11 +6258,11 @@ public JoinClauseContext joinClause() { _localctx = new CrossApplyContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 963; + State = 981; Match(CROSS); - State = 964; + State = 982; Match(APPLY); - State = 965; + State = 983; tablePrimary(); } break; @@ -6158,11 +6270,11 @@ public JoinClauseContext joinClause() { _localctx = new OuterApplyContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 966; + State = 984; Match(OUTER); - State = 967; + State = 985; Match(APPLY); - State = 968; + State = 986; tablePrimary(); } break; @@ -6238,10 +6350,10 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public JoinTypeContext joinType() { JoinTypeContext _localctx = new JoinTypeContext(Context, State); - EnterRule(_localctx, 104, RULE_joinType); + EnterRule(_localctx, 106, RULE_joinType); int _la; try { - State = 986; + State = 1004; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INNER: @@ -6249,12 +6361,12 @@ public JoinTypeContext joinType() { _localctx = new InnerJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 972; + State = 990; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==INNER) { { - State = 971; + State = 989; Match(INNER); } } @@ -6265,14 +6377,14 @@ public JoinTypeContext joinType() { _localctx = new LeftJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 974; + State = 992; Match(LEFT); - State = 976; + State = 994; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 975; + State = 993; Match(OUTER); } } @@ -6283,14 +6395,14 @@ public JoinTypeContext joinType() { _localctx = new RightJoinContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 978; + State = 996; Match(RIGHT); - State = 980; + State = 998; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 979; + State = 997; Match(OUTER); } } @@ -6301,14 +6413,14 @@ public JoinTypeContext joinType() { _localctx = new FullJoinContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 982; + State = 1000; Match(FULL); - State = 984; + State = 1002; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 983; + State = 1001; Match(OUTER); } } @@ -6351,13 +6463,13 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public WhereClauseContext whereClause() { WhereClauseContext _localctx = new WhereClauseContext(Context, State); - EnterRule(_localctx, 106, RULE_whereClause); + EnterRule(_localctx, 108, RULE_whereClause); try { EnterOuterAlt(_localctx, 1); { - State = 988; + State = 1006; Match(WHERE); - State = 989; + State = 1007; expression(0); } } @@ -6401,30 +6513,30 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public OrderByClauseContext orderByClause() { OrderByClauseContext _localctx = new OrderByClauseContext(Context, State); - EnterRule(_localctx, 108, RULE_orderByClause); + EnterRule(_localctx, 110, RULE_orderByClause); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 991; + State = 1009; Match(ORDER); - State = 992; + State = 1010; Match(BY); - State = 993; + State = 1011; orderByItem(); - State = 998; + State = 1016; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 994; + State = 1012; Match(COMMA); - State = 995; + State = 1013; orderByItem(); } } - State = 1000; + State = 1018; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -6464,19 +6576,19 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public OrderByItemContext orderByItem() { OrderByItemContext _localctx = new OrderByItemContext(Context, State); - EnterRule(_localctx, 110, RULE_orderByItem); + EnterRule(_localctx, 112, RULE_orderByItem); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1001; + State = 1019; expression(0); - State = 1003; + State = 1021; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ASC || _la==DESC) { { - State = 1002; + State = 1020; _localctx.dir = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -6953,14 +7065,14 @@ private ExpressionContext expression(int _p) { int _parentState = State; ExpressionContext _localctx = new ExpressionContext(Context, _parentState); ExpressionContext _prevctx = _localctx; - int _startState = 112; - EnterRecursionRule(_localctx, 112, RULE_expression, _p); + int _startState = 114; + EnterRecursionRule(_localctx, 114, RULE_expression, _p); int _la; try { int _alt; EnterOuterAlt(_localctx, 1); { - State = 1011; + State = 1029; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case PLUS: @@ -6970,7 +7082,7 @@ private ExpressionContext expression(int _p) { Context = _localctx; _prevctx = _localctx; - State = 1006; + State = 1024; ((NegateExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -6980,7 +7092,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1007; + State = 1025; expression(21); } break; @@ -6990,7 +7102,7 @@ private ExpressionContext expression(int _p) { _localctx = new NotExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 1008; + State = 1026; ((NotExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==NOT || _la==BNOT) ) { @@ -7000,7 +7112,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1009; + State = 1027; expression(7); } break; @@ -7032,6 +7144,8 @@ private ExpressionContext expression(int _p) { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case LPAREN: case SYSVAR: case PARAM: @@ -7048,7 +7162,7 @@ private ExpressionContext expression(int _p) { _localctx = new PrimaryExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 1010; + State = 1028; primary(); } break; @@ -7056,28 +7170,28 @@ private ExpressionContext expression(int _p) { throw new NoViableAltException(this); } Context.Stop = TokenStream.LT(-1); - State = 1111; + State = 1129; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,137,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,141,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { if ( ParseListeners!=null ) TriggerExitRuleEvent(); _prevctx = _localctx; { - State = 1109; + State = 1127; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,136,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,140,Context) ) { case 1: { _localctx = new PowExprContext(new ExpressionContext(_parentctx, _parentState)); ((PowExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1013; + State = 1031; if (!(Precpred(Context, 22))) throw new FailedPredicateException(this, "Precpred(Context, 22)"); - State = 1014; + State = 1032; Match(CARET); - State = 1015; + State = 1033; ((PowExprContext)_localctx).right = expression(23); } break; @@ -7086,9 +7200,9 @@ private ExpressionContext expression(int _p) { _localctx = new MulDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((MulDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1016; + State = 1034; if (!(Precpred(Context, 20))) throw new FailedPredicateException(this, "Precpred(Context, 20)"); - State = 1017; + State = 1035; ((MulDivExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==STAR || _la==SLASH) ) { @@ -7098,7 +7212,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1018; + State = 1036; ((MulDivExprContext)_localctx).right = expression(21); } break; @@ -7107,11 +7221,11 @@ private ExpressionContext expression(int _p) { _localctx = new IntDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((IntDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1019; + State = 1037; if (!(Precpred(Context, 19))) throw new FailedPredicateException(this, "Precpred(Context, 19)"); - State = 1020; + State = 1038; ((IntDivExprContext)_localctx).op = Match(BACKSLASH); - State = 1021; + State = 1039; ((IntDivExprContext)_localctx).right = expression(20); } break; @@ -7120,11 +7234,11 @@ private ExpressionContext expression(int _p) { _localctx = new ModExprContext(new ExpressionContext(_parentctx, _parentState)); ((ModExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1022; + State = 1040; if (!(Precpred(Context, 18))) throw new FailedPredicateException(this, "Precpred(Context, 18)"); - State = 1023; + State = 1041; ((ModExprContext)_localctx).op = Match(MOD); - State = 1024; + State = 1042; ((ModExprContext)_localctx).right = expression(19); } break; @@ -7133,9 +7247,9 @@ private ExpressionContext expression(int _p) { _localctx = new AddSubExprContext(new ExpressionContext(_parentctx, _parentState)); ((AddSubExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1025; + State = 1043; if (!(Precpred(Context, 17))) throw new FailedPredicateException(this, "Precpred(Context, 17)"); - State = 1026; + State = 1044; ((AddSubExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -7145,7 +7259,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1027; + State = 1045; ((AddSubExprContext)_localctx).right = expression(18); } break; @@ -7154,11 +7268,11 @@ private ExpressionContext expression(int _p) { _localctx = new ConcatExprContext(new ExpressionContext(_parentctx, _parentState)); ((ConcatExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1028; + State = 1046; if (!(Precpred(Context, 16))) throw new FailedPredicateException(this, "Precpred(Context, 16)"); - State = 1029; + State = 1047; ((ConcatExprContext)_localctx).op = Match(AMP); - State = 1030; + State = 1048; ((ConcatExprContext)_localctx).right = expression(17); } break; @@ -7167,19 +7281,19 @@ private ExpressionContext expression(int _p) { _localctx = new ComparisonExprContext(new ExpressionContext(_parentctx, _parentState)); ((ComparisonExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1031; + State = 1049; if (!(Precpred(Context, 15))) throw new FailedPredicateException(this, "Precpred(Context, 15)"); - State = 1032; + State = 1050; ((ComparisonExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); - if ( !(((((_la - 131)) & ~0x3f) == 0 && ((1L << (_la - 131)) & 63L) != 0)) ) { + if ( !(((((_la - 133)) & ~0x3f) == 0 && ((1L << (_la - 133)) & 63L) != 0)) ) { ((ComparisonExprContext)_localctx).op = ErrorHandler.RecoverInline(this); } else { ErrorHandler.ReportMatch(this); Consume(); } - State = 1033; + State = 1051; ((ComparisonExprContext)_localctx).right = expression(16); } break; @@ -7188,25 +7302,25 @@ private ExpressionContext expression(int _p) { _localctx = new BetweenExprContext(new ExpressionContext(_parentctx, _parentState)); ((BetweenExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1034; + State = 1052; if (!(Precpred(Context, 14))) throw new FailedPredicateException(this, "Precpred(Context, 14)"); - State = 1036; + State = 1054; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1035; + State = 1053; ((BetweenExprContext)_localctx).not = Match(NOT); } } - State = 1038; + State = 1056; Match(BETWEEN); - State = 1039; + State = 1057; ((BetweenExprContext)_localctx).lo = expression(0); - State = 1040; + State = 1058; Match(AND); - State = 1041; + State = 1059; ((BetweenExprContext)_localctx).hi = expression(15); } break; @@ -7215,21 +7329,21 @@ private ExpressionContext expression(int _p) { _localctx = new LikeExprContext(new ExpressionContext(_parentctx, _parentState)); ((LikeExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1043; + State = 1061; if (!(Precpred(Context, 13))) throw new FailedPredicateException(this, "Precpred(Context, 13)"); - State = 1045; + State = 1063; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1044; + State = 1062; ((LikeExprContext)_localctx).not = Match(NOT); } } - State = 1047; + State = 1065; Match(LIKE); - State = 1048; + State = 1066; ((LikeExprContext)_localctx).right = expression(14); } break; @@ -7238,25 +7352,25 @@ private ExpressionContext expression(int _p) { _localctx = new IsDistinctFromExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsDistinctFromExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1049; + State = 1067; if (!(Precpred(Context, 8))) throw new FailedPredicateException(this, "Precpred(Context, 8)"); - State = 1050; + State = 1068; Match(IS); - State = 1052; + State = 1070; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1051; + State = 1069; ((IsDistinctFromExprContext)_localctx).not = Match(NOT); } } - State = 1054; + State = 1072; Match(DISTINCT); - State = 1055; + State = 1073; Match(FROM); - State = 1056; + State = 1074; ((IsDistinctFromExprContext)_localctx).right = expression(9); } break; @@ -7265,9 +7379,9 @@ private ExpressionContext expression(int _p) { _localctx = new AndExprContext(new ExpressionContext(_parentctx, _parentState)); ((AndExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1057; + State = 1075; if (!(Precpred(Context, 6))) throw new FailedPredicateException(this, "Precpred(Context, 6)"); - State = 1058; + State = 1076; ((AndExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==AND || _la==BAND) ) { @@ -7277,7 +7391,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1059; + State = 1077; ((AndExprContext)_localctx).right = expression(7); } break; @@ -7286,9 +7400,9 @@ private ExpressionContext expression(int _p) { _localctx = new OrExprContext(new ExpressionContext(_parentctx, _parentState)); ((OrExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1060; + State = 1078; if (!(Precpred(Context, 5))) throw new FailedPredicateException(this, "Precpred(Context, 5)"); - State = 1061; + State = 1079; ((OrExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==OR || _la==BOR) ) { @@ -7298,7 +7412,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1062; + State = 1080; ((OrExprContext)_localctx).right = expression(6); } break; @@ -7307,9 +7421,9 @@ private ExpressionContext expression(int _p) { _localctx = new XorExprContext(new ExpressionContext(_parentctx, _parentState)); ((XorExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1063; + State = 1081; if (!(Precpred(Context, 4))) throw new FailedPredicateException(this, "Precpred(Context, 4)"); - State = 1064; + State = 1082; ((XorExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==XOR || _la==BXOR) ) { @@ -7319,7 +7433,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1065; + State = 1083; ((XorExprContext)_localctx).right = expression(5); } break; @@ -7328,11 +7442,11 @@ private ExpressionContext expression(int _p) { _localctx = new EqvExprContext(new ExpressionContext(_parentctx, _parentState)); ((EqvExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1066; + State = 1084; if (!(Precpred(Context, 3))) throw new FailedPredicateException(this, "Precpred(Context, 3)"); - State = 1067; + State = 1085; ((EqvExprContext)_localctx).op = Match(EQV); - State = 1068; + State = 1086; ((EqvExprContext)_localctx).right = expression(4); } break; @@ -7341,11 +7455,11 @@ private ExpressionContext expression(int _p) { _localctx = new ImpExprContext(new ExpressionContext(_parentctx, _parentState)); ((ImpExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1069; + State = 1087; if (!(Precpred(Context, 2))) throw new FailedPredicateException(this, "Precpred(Context, 2)"); - State = 1070; + State = 1088; ((ImpExprContext)_localctx).op = Match(IMP); - State = 1071; + State = 1089; ((ImpExprContext)_localctx).right = expression(3); } break; @@ -7354,25 +7468,25 @@ private ExpressionContext expression(int _p) { _localctx = new InSubqueryExprContext(new ExpressionContext(_parentctx, _parentState)); ((InSubqueryExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1072; + State = 1090; if (!(Precpred(Context, 12))) throw new FailedPredicateException(this, "Precpred(Context, 12)"); - State = 1074; + State = 1092; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1073; + State = 1091; ((InSubqueryExprContext)_localctx).not = Match(NOT); } } - State = 1076; + State = 1094; Match(IN); - State = 1077; + State = 1095; Match(LPAREN); - State = 1078; + State = 1096; ((InSubqueryExprContext)_localctx).sub = queryExpression(); - State = 1079; + State = 1097; Match(RPAREN); } break; @@ -7381,43 +7495,43 @@ private ExpressionContext expression(int _p) { _localctx = new InExprContext(new ExpressionContext(_parentctx, _parentState)); ((InExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1081; + State = 1099; if (!(Precpred(Context, 11))) throw new FailedPredicateException(this, "Precpred(Context, 11)"); - State = 1083; + State = 1101; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1082; + State = 1100; ((InExprContext)_localctx).not = Match(NOT); } } - State = 1085; + State = 1103; Match(IN); - State = 1086; + State = 1104; Match(LPAREN); - State = 1087; + State = 1105; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); - State = 1092; + State = 1110; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1088; + State = 1106; Match(COMMA); - State = 1089; + State = 1107; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); } } - State = 1094; + State = 1112; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 1095; + State = 1113; Match(RPAREN); } break; @@ -7426,21 +7540,21 @@ private ExpressionContext expression(int _p) { _localctx = new IsNullExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsNullExprContext)_localctx).operand = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1097; + State = 1115; if (!(Precpred(Context, 10))) throw new FailedPredicateException(this, "Precpred(Context, 10)"); - State = 1098; + State = 1116; Match(IS); - State = 1100; + State = 1118; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1099; + State = 1117; ((IsNullExprContext)_localctx).not = Match(NOT); } } - State = 1102; + State = 1120; Match(NULL); } break; @@ -7449,21 +7563,21 @@ private ExpressionContext expression(int _p) { _localctx = new IsTruthExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsTruthExprContext)_localctx).operand = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1103; + State = 1121; if (!(Precpred(Context, 9))) throw new FailedPredicateException(this, "Precpred(Context, 9)"); - State = 1104; + State = 1122; Match(IS); - State = 1106; + State = 1124; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1105; + State = 1123; ((IsTruthExprContext)_localctx).not = Match(NOT); } } - State = 1108; + State = 1126; ((IsTruthExprContext)_localctx).truth = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==TRUE || _la==FALSE) ) { @@ -7478,9 +7592,9 @@ private ExpressionContext expression(int _p) { } } } - State = 1113; + State = 1131; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,137,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,141,Context); } } } @@ -7622,16 +7736,16 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public PrimaryContext primary() { PrimaryContext _localctx = new PrimaryContext(Context, State); - EnterRule(_localctx, 114, RULE_primary); + EnterRule(_localctx, 116, RULE_primary); try { - State = 1133; + State = 1151; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,138,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,142,Context) ) { case 1: _localctx = new LiteralPrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1114; + State = 1132; literal(); } break; @@ -7639,7 +7753,7 @@ public PrimaryContext primary() { _localctx = new CasePrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1115; + State = 1133; caseExpression(); } break; @@ -7647,7 +7761,7 @@ public PrimaryContext primary() { _localctx = new FunctionCallPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1116; + State = 1134; functionCall(); } break; @@ -7655,7 +7769,7 @@ public PrimaryContext primary() { _localctx = new ColumnPrimaryContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 1117; + State = 1135; columnRef(); } break; @@ -7663,7 +7777,7 @@ public PrimaryContext primary() { _localctx = new ParamPrimaryContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 1118; + State = 1136; Match(PARAM); } break; @@ -7671,7 +7785,7 @@ public PrimaryContext primary() { _localctx = new SystemVariablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 1119; + State = 1137; Match(SYSVAR); } break; @@ -7679,13 +7793,13 @@ public PrimaryContext primary() { _localctx = new ExistsPrimaryContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 1120; + State = 1138; Match(EXISTS); - State = 1121; + State = 1139; Match(LPAREN); - State = 1122; + State = 1140; queryExpression(); - State = 1123; + State = 1141; Match(RPAREN); } break; @@ -7693,11 +7807,11 @@ public PrimaryContext primary() { _localctx = new ScalarSubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 1125; + State = 1143; Match(LPAREN); - State = 1126; + State = 1144; queryExpression(); - State = 1127; + State = 1145; Match(RPAREN); } break; @@ -7705,11 +7819,11 @@ public PrimaryContext primary() { _localctx = new ParenPrimaryContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 1129; + State = 1147; Match(LPAREN); - State = 1130; + State = 1148; expression(0); - State = 1131; + State = 1149; Match(RPAREN); } break; @@ -7760,50 +7874,50 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public CaseExpressionContext caseExpression() { CaseExpressionContext _localctx = new CaseExpressionContext(Context, State); - EnterRule(_localctx, 116, RULE_caseExpression); + EnterRule(_localctx, 118, RULE_caseExpression); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1135; + State = 1153; Match(CASE); - State = 1137; + State = 1155; ErrorHandler.Sync(this); _la = TokenStream.LA(1); - if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775694237952L) != 0) || ((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & -283699838490247167L) != 0) || ((((_la - 149)) & ~0x3f) == 0 && ((1L << (_la - 149)) & 31L) != 0)) { + if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775694237952L) != 0) || ((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & -1134799353958629375L) != 0) || ((((_la - 149)) & ~0x3f) == 0 && ((1L << (_la - 149)) & 127L) != 0)) { { - State = 1136; + State = 1154; _localctx.operand = expression(0); } } - State = 1140; + State = 1158; ErrorHandler.Sync(this); _la = TokenStream.LA(1); do { { { - State = 1139; + State = 1157; caseWhen(); } } - State = 1142; + State = 1160; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } while ( _la==WHEN ); - State = 1146; + State = 1164; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ELSE) { { - State = 1144; + State = 1162; Match(ELSE); - State = 1145; + State = 1163; _localctx.elseResult = expression(0); } } - State = 1148; + State = 1166; Match(END); } } @@ -7845,17 +7959,17 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public CaseWhenContext caseWhen() { CaseWhenContext _localctx = new CaseWhenContext(Context, State); - EnterRule(_localctx, 118, RULE_caseWhen); + EnterRule(_localctx, 120, RULE_caseWhen); try { EnterOuterAlt(_localctx, 1); { - State = 1150; + State = 1168; Match(WHEN); - State = 1151; + State = 1169; _localctx.condition = expression(0); - State = 1152; + State = 1170; Match(THEN); - State = 1153; + State = 1171; _localctx.result = expression(0); } } @@ -7923,21 +8037,21 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public FunctionCallContext functionCall() { FunctionCallContext _localctx = new FunctionCallContext(Context, State); - EnterRule(_localctx, 120, RULE_functionCall); + EnterRule(_localctx, 122, RULE_functionCall); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1155; + State = 1173; _localctx.name = functionName(); - State = 1156; + State = 1174; Match(LPAREN); - State = 1169; + State = 1187; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: { - State = 1157; + State = 1175; _localctx.star = Match(STAR); } break; @@ -7972,6 +8086,8 @@ public FunctionCallContext functionCall() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case PLUS: case MINUS: case LPAREN: @@ -7988,31 +8104,31 @@ public FunctionCallContext functionCall() { case IDENTIFIER: { { - State = 1159; + State = 1177; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==DISTINCT) { { - State = 1158; + State = 1176; _localctx.distinct = Match(DISTINCT); } } - State = 1161; + State = 1179; expression(0); - State = 1166; + State = 1184; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1162; + State = 1180; Match(COMMA); - State = 1163; + State = 1181; expression(0); } } - State = 1168; + State = 1186; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -8024,56 +8140,56 @@ public FunctionCallContext functionCall() { default: break; } - State = 1171; + State = 1189; Match(RPAREN); - State = 1173; + State = 1191; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,145,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,149,Context) ) { case 1: { - State = 1172; + State = 1190; withinGroup(); } break; } - State = 1176; + State = 1194; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,146,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,150,Context) ) { case 1: { - State = 1175; + State = 1193; filterClause(); } break; } - State = 1186; + State = 1204; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,149,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,153,Context) ) { case 1: { - State = 1179; + State = 1197; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FROM) { { - State = 1178; + State = 1196; nthRowFrom(); } } - State = 1182; + State = 1200; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==IGNORE || _la==RESPECT) { { - State = 1181; + State = 1199; nullTreatment(); } } - State = 1184; + State = 1202; Match(OVER); - State = 1185; + State = 1203; windowSpecification(); } break; @@ -8116,9 +8232,9 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public FunctionNameContext functionName() { FunctionNameContext _localctx = new FunctionNameContext(Context, State); - EnterRule(_localctx, 122, RULE_functionName); + EnterRule(_localctx, 124, RULE_functionName); try { - State = 1194; + State = 1212; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -8137,47 +8253,49 @@ public FunctionNameContext functionName() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case BRACKET_ID: case BACKTICK_ID: case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1188; + State = 1206; identifier(); } break; case LEFT: EnterOuterAlt(_localctx, 2); { - State = 1189; + State = 1207; Match(LEFT); } break; case RIGHT: EnterOuterAlt(_localctx, 3); { - State = 1190; + State = 1208; Match(RIGHT); } break; case ASC: EnterOuterAlt(_localctx, 4); { - State = 1191; + State = 1209; Match(ASC); } break; case FIRST: EnterOuterAlt(_localctx, 5); { - State = 1192; + State = 1210; Match(FIRST); } break; case PARTITION: EnterOuterAlt(_localctx, 6); { - State = 1193; + State = 1211; Match(PARTITION); } break; @@ -8201,7 +8319,7 @@ public partial class ColumnRefContext : ParserRuleContext { public IToken _DOT; public IList _separators = new List(); public IToken _BANG; - public IToken _tset3292; + public IToken _tset3324; public MemberNameContext _memberName; public IList _rest = new List(); [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() { @@ -8237,41 +8355,41 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public ColumnRefContext columnRef() { ColumnRefContext _localctx = new ColumnRefContext(Context, State); - EnterRule(_localctx, 124, RULE_columnRef); + EnterRule(_localctx, 126, RULE_columnRef); int _la; try { int _alt; EnterOuterAlt(_localctx, 1); { - State = 1196; + State = 1214; _localctx.first = identifier(); - State = 1201; + State = 1219; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,151,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,155,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 1197; - _localctx._tset3292 = TokenStream.LT(1); + State = 1215; + _localctx._tset3324 = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==DOT || _la==BANG) ) { - _localctx._tset3292 = ErrorHandler.RecoverInline(this); + _localctx._tset3324 = ErrorHandler.RecoverInline(this); } else { ErrorHandler.ReportMatch(this); Consume(); } - _localctx._separators.Add(_localctx._tset3292); - State = 1198; + _localctx._separators.Add(_localctx._tset3324); + State = 1216; _localctx._memberName = memberName(); _localctx._rest.Add(_localctx._memberName); } } } - State = 1203; + State = 1221; ErrorHandler.Sync(this); - _alt = Interpreter.AdaptivePredict(TokenStream,151,Context); + _alt = Interpreter.AdaptivePredict(TokenStream,155,Context); } } } @@ -8309,9 +8427,9 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public MemberNameContext memberName() { MemberNameContext _localctx = new MemberNameContext(Context, State); - EnterRule(_localctx, 126, RULE_memberName); + EnterRule(_localctx, 128, RULE_memberName); try { - State = 1206; + State = 1224; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case YES: @@ -8330,12 +8448,14 @@ public MemberNameContext memberName() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case BRACKET_ID: case BACKTICK_ID: case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1204; + State = 1222; identifier(); } break; @@ -8448,7 +8568,7 @@ public MemberNameContext memberName() { case NULL: EnterOuterAlt(_localctx, 2); { - State = 1205; + State = 1223; reservedKeyword(); } break; @@ -8490,29 +8610,29 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public IdentifierContext identifier() { IdentifierContext _localctx = new IdentifierContext(Context, State); - EnterRule(_localctx, 128, RULE_identifier); + EnterRule(_localctx, 130, RULE_identifier); try { - State = 1212; + State = 1230; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1208; + State = 1226; Match(IDENTIFIER); } break; case BRACKET_ID: EnterOuterAlt(_localctx, 2); { - State = 1209; + State = 1227; Match(BRACKET_ID); } break; case BACKTICK_ID: EnterOuterAlt(_localctx, 3); { - State = 1210; + State = 1228; Match(BACKTICK_ID); } break; @@ -8532,9 +8652,11 @@ public IdentifierContext identifier() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: EnterOuterAlt(_localctx, 4); { - State = 1211; + State = 1229; nonReservedKeyword(); } break; @@ -8663,17 +8785,17 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public LiteralContext literal() { LiteralContext _localctx = new LiteralContext(Context, State); - EnterRule(_localctx, 130, RULE_literal); + EnterRule(_localctx, 132, RULE_literal); int _la; try { - State = 1223; + State = 1241; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: _localctx = new IntLiteralContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1214; + State = 1232; Match(INTEGER_LITERAL); } break; @@ -8681,7 +8803,7 @@ public LiteralContext literal() { _localctx = new NumberLiteralContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1215; + State = 1233; Match(NUMBER_LITERAL); } break; @@ -8689,7 +8811,7 @@ public LiteralContext literal() { _localctx = new HexLiteralContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1216; + State = 1234; Match(HEX_LITERAL); } break; @@ -8697,7 +8819,7 @@ public LiteralContext literal() { _localctx = new StringLiteralContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 1217; + State = 1235; Match(STRING_LITERAL); } break; @@ -8705,7 +8827,7 @@ public LiteralContext literal() { _localctx = new DateLiteralContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 1218; + State = 1236; Match(DATE_LITERAL); } break; @@ -8713,7 +8835,7 @@ public LiteralContext literal() { _localctx = new GuidLiteralContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 1219; + State = 1237; Match(GUID_LITERAL); } break; @@ -8723,7 +8845,7 @@ public LiteralContext literal() { _localctx = new TrueLiteralContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 1220; + State = 1238; _la = TokenStream.LA(1); if ( !(_la==ON || _la==TRUE || _la==YES) ) { ErrorHandler.RecoverInline(this); @@ -8740,7 +8862,7 @@ public LiteralContext literal() { _localctx = new FalseLiteralContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 1221; + State = 1239; _la = TokenStream.LA(1); if ( !(((((_la - 85)) & ~0x3f) == 0 && ((1L << (_la - 85)) & 10485761L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -8755,7 +8877,7 @@ public LiteralContext literal() { _localctx = new NullLiteralContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 1222; + State = 1240; Match(NULL); } break; @@ -8826,24 +8948,24 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public TransactionStatementContext transactionStatement() { TransactionStatementContext _localctx = new TransactionStatementContext(Context, State); - EnterRule(_localctx, 132, RULE_transactionStatement); + EnterRule(_localctx, 134, RULE_transactionStatement); int _la; try { - State = 1237; + State = 1255; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BEGIN: _localctx = new BeginTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1225; + State = 1243; Match(BEGIN); - State = 1227; + State = 1245; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1226; + State = 1244; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8861,14 +8983,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new CommitTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1229; + State = 1247; Match(COMMIT); - State = 1231; + State = 1249; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1230; + State = 1248; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8886,14 +9008,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new RollbackTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1233; + State = 1251; Match(ROLLBACK); - State = 1235; + State = 1253; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1234; + State = 1252; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8943,13 +9065,13 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public StandaloneExpressionContext standaloneExpression() { StandaloneExpressionContext _localctx = new StandaloneExpressionContext(Context, State); - EnterRule(_localctx, 134, RULE_standaloneExpression); + EnterRule(_localctx, 136, RULE_standaloneExpression); try { EnterOuterAlt(_localctx, 1); { - State = 1239; + State = 1257; expression(0); - State = 1240; + State = 1258; Match(Eof); } } @@ -9003,66 +9125,66 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public WindowSpecificationContext windowSpecification() { WindowSpecificationContext _localctx = new WindowSpecificationContext(Context, State); - EnterRule(_localctx, 136, RULE_windowSpecification); + EnterRule(_localctx, 138, RULE_windowSpecification); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1242; + State = 1260; Match(LPAREN); - State = 1253; + State = 1271; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PARTITION) { { - State = 1243; + State = 1261; Match(PARTITION); - State = 1244; + State = 1262; Match(BY); - State = 1245; + State = 1263; _localctx._expression = expression(0); _localctx._partition.Add(_localctx._expression); - State = 1250; + State = 1268; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1246; + State = 1264; Match(COMMA); - State = 1247; + State = 1265; _localctx._expression = expression(0); _localctx._partition.Add(_localctx._expression); } } - State = 1252; + State = 1270; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } } } - State = 1256; + State = 1274; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ORDER) { { - State = 1255; + State = 1273; orderByClause(); } } - State = 1259; + State = 1277; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 6917529027641081857L) != 0)) { { - State = 1258; + State = 1276; windowFrame(); } } - State = 1261; + State = 1279; Match(RPAREN); } } @@ -9096,12 +9218,12 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public ClusteredOptionContext clusteredOption() { ClusteredOptionContext _localctx = new ClusteredOptionContext(Context, State); - EnterRule(_localctx, 138, RULE_clusteredOption); + EnterRule(_localctx, 140, RULE_clusteredOption); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1263; + State = 1281; _la = TokenStream.LA(1); if ( !(_la==CLUSTERED || _la==NONCLUSTERED) ) { ErrorHandler.RecoverInline(this); @@ -9159,12 +9281,12 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public WindowFrameContext windowFrame() { WindowFrameContext _localctx = new WindowFrameContext(Context, State); - EnterRule(_localctx, 140, RULE_windowFrame); + EnterRule(_localctx, 142, RULE_windowFrame); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1265; + State = 1283; _localctx.unit = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 6917529027641081857L) != 0)) ) { @@ -9174,18 +9296,18 @@ public WindowFrameContext windowFrame() { ErrorHandler.ReportMatch(this); Consume(); } - State = 1272; + State = 1290; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BETWEEN: { - State = 1266; + State = 1284; Match(BETWEEN); - State = 1267; + State = 1285; _localctx.start = frameBound(); - State = 1268; + State = 1286; Match(AND); - State = 1269; + State = 1287; _localctx.end = frameBound(); } break; @@ -9219,6 +9341,8 @@ public WindowFrameContext windowFrame() { case RESPECT: case NULLS: case FILTER: + case OWNERACCESS: + case OPTION: case PLUS: case MINUS: case LPAREN: @@ -9234,21 +9358,21 @@ public WindowFrameContext windowFrame() { case BACKTICK_ID: case IDENTIFIER: { - State = 1271; + State = 1289; _localctx.start = frameBound(); } break; default: throw new NoViableAltException(this); } - State = 1276; + State = 1294; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==EXCLUDE) { { - State = 1274; + State = 1292; Match(EXCLUDE); - State = 1275; + State = 1293; _localctx.exclusion = frameExclusion(); } } @@ -9293,18 +9417,18 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public FrameBoundContext frameBound() { FrameBoundContext _localctx = new FrameBoundContext(Context, State); - EnterRule(_localctx, 142, RULE_frameBound); + EnterRule(_localctx, 144, RULE_frameBound); int _la; try { - State = 1285; + State = 1303; ErrorHandler.Sync(this); - switch ( Interpreter.AdaptivePredict(TokenStream,165,Context) ) { + switch ( Interpreter.AdaptivePredict(TokenStream,169,Context) ) { case 1: EnterOuterAlt(_localctx, 1); { - State = 1278; + State = 1296; Match(UNBOUNDED); - State = 1279; + State = 1297; _localctx.direction = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PRECEDING || _la==FOLLOWING) ) { @@ -9319,18 +9443,18 @@ public FrameBoundContext frameBound() { case 2: EnterOuterAlt(_localctx, 2); { - State = 1280; + State = 1298; Match(CURRENT); - State = 1281; + State = 1299; Match(ROW); } break; case 3: EnterOuterAlt(_localctx, 3); { - State = 1282; + State = 1300; _localctx.offset = expression(0); - State = 1283; + State = 1301; _localctx.direction = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PRECEDING || _la==FOLLOWING) ) { @@ -9378,40 +9502,40 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public FrameExclusionContext frameExclusion() { FrameExclusionContext _localctx = new FrameExclusionContext(Context, State); - EnterRule(_localctx, 144, RULE_frameExclusion); + EnterRule(_localctx, 146, RULE_frameExclusion); try { - State = 1293; + State = 1311; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case CURRENT: EnterOuterAlt(_localctx, 1); { - State = 1287; + State = 1305; Match(CURRENT); - State = 1288; + State = 1306; Match(ROW); } break; case GROUP: EnterOuterAlt(_localctx, 2); { - State = 1289; + State = 1307; Match(GROUP); } break; case TIES: EnterOuterAlt(_localctx, 3); { - State = 1290; + State = 1308; Match(TIES); } break; case NO: EnterOuterAlt(_localctx, 4); { - State = 1291; + State = 1309; Match(NO); - State = 1292; + State = 1310; Match(OTHERS); } break; @@ -9447,6 +9571,8 @@ public partial class NonReservedKeywordContext : ParserRuleContext { [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FILTER() { return GetToken(AccessSqlParser.FILTER, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode YES() { return GetToken(AccessSqlParser.YES, 0); } [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OFF() { return GetToken(AccessSqlParser.OFF, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OWNERACCESS() { return GetToken(AccessSqlParser.OWNERACCESS, 0); } + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OPTION() { return GetToken(AccessSqlParser.OPTION, 0); } public NonReservedKeywordContext(ParserRuleContext parent, int invokingState) : base(parent, invokingState) { @@ -9463,14 +9589,14 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public NonReservedKeywordContext nonReservedKeyword() { NonReservedKeywordContext _localctx = new NonReservedKeywordContext(Context, State); - EnterRule(_localctx, 146, RULE_nonReservedKeyword); + EnterRule(_localctx, 148, RULE_nonReservedKeyword); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1295; + State = 1313; _la = TokenStream.LA(1); - if ( !(((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 131067L) != 0)) ) { + if ( !(((((_la - 107)) & ~0x3f) == 0 && ((1L << (_la - 107)) & 524283L) != 0)) ) { ErrorHandler.RecoverInline(this); } else { @@ -9614,12 +9740,12 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public ReservedKeywordContext reservedKeyword() { ReservedKeywordContext _localctx = new ReservedKeywordContext(Context, State); - EnterRule(_localctx, 148, RULE_reservedKeyword); + EnterRule(_localctx, 150, RULE_reservedKeyword); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1297; + State = 1315; _la = TokenStream.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & 43980465111039L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -9666,19 +9792,19 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public FilterClauseContext filterClause() { FilterClauseContext _localctx = new FilterClauseContext(Context, State); - EnterRule(_localctx, 150, RULE_filterClause); + EnterRule(_localctx, 152, RULE_filterClause); try { EnterOuterAlt(_localctx, 1); { - State = 1299; + State = 1317; Match(FILTER); - State = 1300; + State = 1318; Match(LPAREN); - State = 1301; + State = 1319; Match(WHERE); - State = 1302; + State = 1320; _localctx.condition = expression(0); - State = 1303; + State = 1321; Match(RPAREN); } } @@ -9717,19 +9843,19 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public WithinGroupContext withinGroup() { WithinGroupContext _localctx = new WithinGroupContext(Context, State); - EnterRule(_localctx, 152, RULE_withinGroup); + EnterRule(_localctx, 154, RULE_withinGroup); try { EnterOuterAlt(_localctx, 1); { - State = 1305; + State = 1323; Match(WITHIN); - State = 1306; + State = 1324; Match(GROUP); - State = 1307; + State = 1325; Match(LPAREN); - State = 1308; + State = 1326; orderByClause(); - State = 1309; + State = 1327; Match(RPAREN); } } @@ -9765,14 +9891,14 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public NthRowFromContext nthRowFrom() { NthRowFromContext _localctx = new NthRowFromContext(Context, State); - EnterRule(_localctx, 154, RULE_nthRowFrom); + EnterRule(_localctx, 156, RULE_nthRowFrom); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1311; + State = 1329; Match(FROM); - State = 1312; + State = 1330; _localctx.edge = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==FIRST || _la==LAST) ) { @@ -9816,12 +9942,12 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public NullTreatmentContext nullTreatment() { NullTreatmentContext _localctx = new NullTreatmentContext(Context, State); - EnterRule(_localctx, 156, RULE_nullTreatment); + EnterRule(_localctx, 158, RULE_nullTreatment); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1314; + State = 1332; _localctx.treatment = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==IGNORE || _la==RESPECT) ) { @@ -9831,7 +9957,7 @@ public NullTreatmentContext nullTreatment() { ErrorHandler.ReportMatch(this); Consume(); } - State = 1315; + State = 1333; Match(NULLS); } } @@ -9877,34 +10003,34 @@ public override TResult Accept(IParseTreeVisitor visitor) { [RuleVersion(0)] public DerivedColumnsContext derivedColumns() { DerivedColumnsContext _localctx = new DerivedColumnsContext(Context, State); - EnterRule(_localctx, 158, RULE_derivedColumns); + EnterRule(_localctx, 160, RULE_derivedColumns); int _la; try { EnterOuterAlt(_localctx, 1); { - State = 1317; + State = 1335; Match(LPAREN); - State = 1318; + State = 1336; _localctx._identifier = identifier(); _localctx._names.Add(_localctx._identifier); - State = 1323; + State = 1341; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1319; + State = 1337; Match(COMMA); - State = 1320; + State = 1338; _localctx._identifier = identifier(); _localctx._names.Add(_localctx._identifier); } } - State = 1325; + State = 1343; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 1326; + State = 1344; Match(RPAREN); } } @@ -9921,7 +10047,7 @@ public DerivedColumnsContext derivedColumns() { public override bool Sempred(RuleContext _localctx, int ruleIndex, int predIndex) { switch (ruleIndex) { - case 56: return expression_sempred((ExpressionContext)_localctx, predIndex); + case 57: return expression_sempred((ExpressionContext)_localctx, predIndex); } return true; } @@ -9951,7 +10077,7 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { } private static int[] _serializedATN = { - 4,1,156,1329,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,158,1347,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, @@ -9962,488 +10088,496 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { 2,57,7,57,2,58,7,58,2,59,7,59,2,60,7,60,2,61,7,61,2,62,7,62,2,63,7,63, 2,64,7,64,2,65,7,65,2,66,7,66,2,67,7,67,2,68,7,68,2,69,7,69,2,70,7,70, 2,71,7,71,2,72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77, - 2,78,7,78,2,79,7,79,1,0,3,0,162,8,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1, - 0,1,0,1,0,1,0,1,0,1,0,3,0,178,8,0,1,0,3,0,181,8,0,1,0,1,0,1,1,1,1,3,1, - 187,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1, - 2,1,2,1,2,3,2,207,8,2,1,3,1,3,1,3,1,3,1,3,5,3,214,8,3,10,3,12,3,217,9, - 3,3,3,219,8,3,1,4,1,4,1,4,1,4,5,4,225,8,4,10,4,12,4,228,9,4,1,4,1,4,1, - 4,1,4,5,4,234,8,4,10,4,12,4,237,9,4,1,4,3,4,240,8,4,1,5,1,5,1,5,1,5,1, - 6,1,6,1,6,1,6,1,6,1,6,3,6,252,8,6,1,6,1,6,1,6,1,6,5,6,258,8,6,10,6,12, - 6,261,9,6,1,6,3,6,264,8,6,1,7,1,7,1,7,1,7,5,7,270,8,7,10,7,12,7,273,9, - 7,1,8,1,8,3,8,277,8,8,1,8,3,8,280,8,8,1,9,1,9,1,9,1,9,5,9,286,8,9,10,9, - 12,9,289,9,9,1,9,1,9,1,10,1,10,3,10,295,8,10,1,10,1,10,1,10,1,10,1,10, - 1,10,5,10,303,8,10,10,10,12,10,306,9,10,1,10,1,10,5,10,310,8,10,10,10, - 12,10,313,9,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,11,1,11,5,11,324,8, - 11,10,11,12,11,327,9,11,1,11,1,11,3,11,331,8,11,1,11,1,11,1,11,1,12,1, - 12,1,12,1,12,3,12,340,8,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,5,13,349, - 8,13,10,13,12,13,352,9,13,1,13,1,13,1,13,1,13,1,13,5,13,359,8,13,10,13, - 12,13,362,9,13,3,13,364,8,13,1,14,1,14,1,14,1,15,1,15,3,15,371,8,15,1, - 16,1,16,1,16,1,16,1,16,1,17,1,17,3,17,380,8,17,1,17,1,17,1,17,1,17,1,17, - 3,17,387,8,17,1,17,1,17,1,17,5,17,392,8,17,10,17,12,17,395,9,17,1,17,1, - 17,3,17,399,8,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,408,8,17,1,17, + 2,78,7,78,2,79,7,79,2,80,7,80,1,0,3,0,164,8,0,1,0,1,0,1,0,1,0,1,0,1,0, + 1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,3,0,180,8,0,1,0,3,0,183,8,0,1,0,1,0,1, + 1,1,1,3,1,189,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,2, + 1,2,1,2,1,2,1,2,1,2,3,2,209,8,2,1,3,1,3,1,3,1,3,1,3,5,3,216,8,3,10,3,12, + 3,219,9,3,3,3,221,8,3,1,4,1,4,1,4,1,4,5,4,227,8,4,10,4,12,4,230,9,4,1, + 4,1,4,1,4,1,4,5,4,236,8,4,10,4,12,4,239,9,4,1,4,3,4,242,8,4,1,4,3,4,245, + 8,4,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,3,6,257,8,6,1,6,1,6,1,6,1, + 6,5,6,263,8,6,10,6,12,6,266,9,6,1,6,3,6,269,8,6,1,6,3,6,272,8,6,1,7,1, + 7,1,7,1,7,5,7,278,8,7,10,7,12,7,281,9,7,1,8,1,8,3,8,285,8,8,1,8,3,8,288, + 8,8,1,9,1,9,1,9,1,9,5,9,294,8,9,10,9,12,9,297,9,9,1,9,1,9,1,10,1,10,3, + 10,303,8,10,1,10,1,10,1,10,1,10,1,10,1,10,5,10,311,8,10,10,10,12,10,314, + 9,10,1,10,1,10,5,10,318,8,10,10,10,12,10,321,9,10,1,10,1,10,1,11,1,11, + 1,11,1,11,1,11,1,11,1,11,5,11,332,8,11,10,11,12,11,335,9,11,1,11,1,11, + 3,11,339,8,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,3,12,348,8,12,1,12,1, + 12,1,12,1,13,1,13,1,13,1,13,5,13,357,8,13,10,13,12,13,360,9,13,1,13,1, + 13,1,13,1,13,1,13,5,13,367,8,13,10,13,12,13,370,9,13,3,13,372,8,13,1,14, + 1,14,1,14,1,15,1,15,3,15,379,8,15,1,16,1,16,1,16,1,16,1,16,1,17,1,17,3, + 17,388,8,17,1,17,1,17,1,17,1,17,1,17,3,17,395,8,17,1,17,1,17,1,17,5,17, + 400,8,17,10,17,12,17,403,9,17,1,17,1,17,3,17,407,8,17,1,17,1,17,1,17,1, + 17,1,17,1,17,1,17,3,17,416,8,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17, 1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17, - 1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,3,17,435,8,17,1,18,1, - 18,1,18,1,18,1,18,3,18,442,8,18,1,19,1,19,3,19,446,8,19,1,19,1,19,1,19, - 1,19,1,19,1,19,1,19,1,19,5,19,456,8,19,10,19,12,19,459,9,19,1,19,1,19, - 1,19,3,19,464,8,19,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1, - 20,1,20,1,20,1,20,1,20,3,20,481,8,20,1,21,1,21,3,21,485,8,21,1,22,1,22, - 1,22,1,22,1,22,3,22,492,8,22,1,23,1,23,1,23,3,23,497,8,23,1,23,5,23,500, - 8,23,10,23,12,23,503,9,23,1,24,1,24,1,24,1,24,1,24,1,25,1,25,3,25,512, - 8,25,1,25,3,25,515,8,25,1,25,3,25,518,8,25,1,25,1,25,1,25,1,25,3,25,524, - 8,25,1,25,1,25,3,25,528,8,25,1,26,3,26,531,8,26,1,26,1,26,1,27,1,27,1, - 27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,544,8,27,1,27,1,27,1,27,1,27,1,27, - 1,27,1,27,3,27,553,8,27,1,27,1,27,1,27,3,27,558,8,27,1,27,1,27,3,27,562, - 8,27,1,27,1,27,3,27,566,8,27,1,27,1,27,3,27,570,8,27,1,27,1,27,1,27,1, - 27,1,27,1,27,5,27,578,8,27,10,27,12,27,581,9,27,1,27,1,27,3,27,585,8,27, - 1,27,5,27,588,8,27,10,27,12,27,591,9,27,1,27,1,27,1,27,1,27,1,27,3,27, - 598,8,27,1,27,1,27,3,27,602,8,27,3,27,604,8,27,1,28,1,28,3,28,608,8,28, - 1,28,1,28,1,28,3,28,613,8,28,1,28,1,28,1,28,1,28,5,28,619,8,28,10,28,12, - 28,622,9,28,1,28,1,28,1,28,1,28,3,28,628,8,28,1,28,1,28,3,28,632,8,28, - 1,28,1,28,1,28,1,28,5,28,638,8,28,10,28,12,28,641,9,28,1,28,1,28,1,28, - 1,28,3,28,647,8,28,1,28,1,28,1,28,1,28,3,28,653,8,28,1,28,1,28,1,28,1, - 28,5,28,659,8,28,10,28,12,28,662,9,28,1,28,1,28,1,28,1,28,1,28,1,28,1, - 28,5,28,671,8,28,10,28,12,28,674,9,28,1,28,1,28,3,28,678,8,28,1,28,5,28, - 681,8,28,10,28,12,28,684,9,28,1,28,1,28,3,28,688,8,28,1,28,1,28,1,28,1, - 28,1,28,3,28,695,8,28,1,29,1,29,1,29,1,29,1,29,5,29,702,8,29,10,29,12, - 29,705,9,29,1,30,1,30,1,30,1,30,1,30,1,30,3,30,713,8,30,1,31,1,31,1,31, - 1,31,1,31,1,31,1,31,1,31,3,31,723,8,31,1,32,1,32,1,32,1,32,1,32,1,32,1, - 32,5,32,732,8,32,10,32,12,32,735,9,32,1,32,1,32,3,32,739,8,32,1,32,1,32, - 1,32,3,32,744,8,32,1,33,1,33,1,33,1,33,5,33,750,8,33,10,33,12,33,753,9, - 33,1,33,1,33,1,34,1,34,3,34,759,8,34,1,35,1,35,1,35,1,35,5,35,765,8,35, - 10,35,12,35,768,9,35,1,35,3,35,771,8,35,1,35,3,35,774,8,35,1,36,1,36,1, - 36,1,36,1,36,1,36,1,36,1,36,1,36,5,36,785,8,36,10,36,12,36,788,9,36,3, - 36,790,8,36,1,37,1,37,3,37,794,8,37,1,37,1,37,3,37,798,8,37,1,38,1,38, - 3,38,802,8,38,1,38,3,38,805,8,38,1,38,1,38,1,38,3,38,810,8,38,1,38,3,38, - 813,8,38,1,38,3,38,816,8,38,1,38,3,38,819,8,38,1,38,3,38,822,8,38,1,39, - 1,39,1,40,1,40,1,40,1,40,1,40,5,40,831,8,40,10,40,12,40,834,9,40,1,41, - 1,41,1,41,1,42,1,42,1,42,1,42,5,42,843,8,42,10,42,12,42,846,9,42,1,42, - 3,42,849,8,42,1,42,1,42,3,42,853,8,42,1,43,1,43,1,43,1,43,1,43,1,43,3, - 43,861,8,43,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,873, - 8,44,3,44,875,8,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,884,8,44,3, - 44,886,8,44,1,45,1,45,1,46,1,46,1,46,1,46,5,46,894,8,46,10,46,12,46,897, - 9,46,3,46,899,8,46,1,47,1,47,1,47,1,47,1,47,1,47,3,47,907,8,47,1,47,3, - 47,910,8,47,3,47,912,8,47,1,48,1,48,1,48,1,48,5,48,918,8,48,10,48,12,48, - 921,9,48,1,49,1,49,5,49,925,8,49,10,49,12,49,928,9,49,1,50,1,50,3,50,932, - 8,50,1,50,3,50,935,8,50,1,50,1,50,1,50,1,50,3,50,941,8,50,1,50,1,50,3, - 50,945,8,50,3,50,947,8,50,1,50,1,50,1,50,1,50,3,50,953,8,50,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,1,51,3,51, - 970,8,51,1,52,3,52,973,8,52,1,52,1,52,3,52,977,8,52,1,52,1,52,3,52,981, - 8,52,1,52,1,52,3,52,985,8,52,3,52,987,8,52,1,53,1,53,1,53,1,54,1,54,1, - 54,1,54,1,54,5,54,997,8,54,10,54,12,54,1000,9,54,1,55,1,55,3,55,1004,8, - 55,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1012,8,56,1,56,1,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,3,56,1037,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1, - 56,3,56,1046,8,56,1,56,1,56,1,56,1,56,1,56,3,56,1053,8,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, - 56,1,56,1,56,1,56,3,56,1075,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3, - 56,1084,8,56,1,56,1,56,1,56,1,56,1,56,5,56,1091,8,56,10,56,12,56,1094, - 9,56,1,56,1,56,1,56,1,56,1,56,3,56,1101,8,56,1,56,1,56,1,56,1,56,3,56, - 1107,8,56,1,56,5,56,1110,8,56,10,56,12,56,1113,9,56,1,57,1,57,1,57,1,57, - 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, - 1,57,3,57,1134,8,57,1,58,1,58,3,58,1138,8,58,1,58,4,58,1141,8,58,11,58, - 12,58,1142,1,58,1,58,3,58,1147,8,58,1,58,1,58,1,59,1,59,1,59,1,59,1,59, - 1,60,1,60,1,60,1,60,3,60,1160,8,60,1,60,1,60,1,60,5,60,1165,8,60,10,60, - 12,60,1168,9,60,3,60,1170,8,60,1,60,1,60,3,60,1174,8,60,1,60,3,60,1177, - 8,60,1,60,3,60,1180,8,60,1,60,3,60,1183,8,60,1,60,1,60,3,60,1187,8,60, - 1,61,1,61,1,61,1,61,1,61,1,61,3,61,1195,8,61,1,62,1,62,1,62,5,62,1200, - 8,62,10,62,12,62,1203,9,62,1,63,1,63,3,63,1207,8,63,1,64,1,64,1,64,1,64, - 3,64,1213,8,64,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,3,65,1224, - 8,65,1,66,1,66,3,66,1228,8,66,1,66,1,66,3,66,1232,8,66,1,66,1,66,3,66, - 1236,8,66,3,66,1238,8,66,1,67,1,67,1,67,1,68,1,68,1,68,1,68,1,68,1,68, - 5,68,1249,8,68,10,68,12,68,1252,9,68,3,68,1254,8,68,1,68,3,68,1257,8,68, - 1,68,3,68,1260,8,68,1,68,1,68,1,69,1,69,1,70,1,70,1,70,1,70,1,70,1,70, - 1,70,3,70,1273,8,70,1,70,1,70,3,70,1277,8,70,1,71,1,71,1,71,1,71,1,71, - 1,71,1,71,3,71,1286,8,71,1,72,1,72,1,72,1,72,1,72,1,72,3,72,1294,8,72, - 1,73,1,73,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,76,1,77,1,77,1,77,1,78,1,78,1,78,1,79,1,79,1,79,1,79,5,79,1322, - 8,79,10,79,12,79,1325,9,79,1,79,1,79,1,79,0,1,112,80,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,120,122,124,126,128,130,132,134,136,138,140,142, - 144,146,148,150,152,154,156,158,0,26,1,0,101,102,1,0,103,104,1,0,93,94, - 1,0,137,138,2,0,34,35,54,54,1,0,128,129,1,0,47,48,1,0,49,50,2,0,8,8,15, - 15,1,0,124,125,1,0,131,136,2,0,6,6,12,12,2,0,7,7,13,13,2,0,9,9,14,14,1, - 0,105,106,1,0,140,141,3,0,25,25,105,105,107,107,3,0,85,85,106,106,108, - 108,1,0,62,63,2,0,87,87,89,89,2,0,49,49,110,111,1,0,113,114,2,0,107,108, - 110,123,2,0,1,106,109,109,2,0,48,48,120,120,2,0,96,96,121,121,1515,0,161, - 1,0,0,0,2,184,1,0,0,0,4,206,1,0,0,0,6,208,1,0,0,0,8,220,1,0,0,0,10,241, - 1,0,0,0,12,245,1,0,0,0,14,265,1,0,0,0,16,274,1,0,0,0,18,281,1,0,0,0,20, - 292,1,0,0,0,22,316,1,0,0,0,24,335,1,0,0,0,26,363,1,0,0,0,28,365,1,0,0, - 0,30,370,1,0,0,0,32,372,1,0,0,0,34,434,1,0,0,0,36,441,1,0,0,0,38,443,1, - 0,0,0,40,480,1,0,0,0,42,482,1,0,0,0,44,491,1,0,0,0,46,493,1,0,0,0,48,504, - 1,0,0,0,50,511,1,0,0,0,52,530,1,0,0,0,54,603,1,0,0,0,56,694,1,0,0,0,58, - 703,1,0,0,0,60,712,1,0,0,0,62,722,1,0,0,0,64,724,1,0,0,0,66,745,1,0,0, - 0,68,758,1,0,0,0,70,760,1,0,0,0,72,789,1,0,0,0,74,797,1,0,0,0,76,799,1, - 0,0,0,78,823,1,0,0,0,80,825,1,0,0,0,82,835,1,0,0,0,84,838,1,0,0,0,86,860, - 1,0,0,0,88,885,1,0,0,0,90,887,1,0,0,0,92,898,1,0,0,0,94,911,1,0,0,0,96, - 913,1,0,0,0,98,922,1,0,0,0,100,952,1,0,0,0,102,969,1,0,0,0,104,986,1,0, - 0,0,106,988,1,0,0,0,108,991,1,0,0,0,110,1001,1,0,0,0,112,1011,1,0,0,0, - 114,1133,1,0,0,0,116,1135,1,0,0,0,118,1150,1,0,0,0,120,1155,1,0,0,0,122, - 1194,1,0,0,0,124,1196,1,0,0,0,126,1206,1,0,0,0,128,1212,1,0,0,0,130,1223, - 1,0,0,0,132,1237,1,0,0,0,134,1239,1,0,0,0,136,1242,1,0,0,0,138,1263,1, - 0,0,0,140,1265,1,0,0,0,142,1285,1,0,0,0,144,1293,1,0,0,0,146,1295,1,0, - 0,0,148,1297,1,0,0,0,150,1299,1,0,0,0,152,1305,1,0,0,0,154,1311,1,0,0, - 0,156,1314,1,0,0,0,158,1317,1,0,0,0,160,162,3,18,9,0,161,160,1,0,0,0,161, - 162,1,0,0,0,162,177,1,0,0,0,163,178,3,2,1,0,164,178,3,20,10,0,165,178, - 3,38,19,0,166,178,3,22,11,0,167,178,3,24,12,0,168,178,3,32,16,0,169,178, - 3,40,20,0,170,178,3,64,32,0,171,178,3,8,4,0,172,178,3,12,6,0,173,178,3, - 132,66,0,174,178,3,6,3,0,175,178,3,14,7,0,176,178,3,70,35,0,177,163,1, - 0,0,0,177,164,1,0,0,0,177,165,1,0,0,0,177,166,1,0,0,0,177,167,1,0,0,0, - 177,168,1,0,0,0,177,169,1,0,0,0,177,170,1,0,0,0,177,171,1,0,0,0,177,172, - 1,0,0,0,177,173,1,0,0,0,177,174,1,0,0,0,177,175,1,0,0,0,177,176,1,0,0, - 0,178,180,1,0,0,0,179,181,5,142,0,0,180,179,1,0,0,0,180,181,1,0,0,0,181, - 182,1,0,0,0,182,183,5,0,0,1,183,1,1,0,0,0,184,186,5,32,0,0,185,187,5,8, - 0,0,186,185,1,0,0,0,186,187,1,0,0,0,187,188,1,0,0,0,188,189,5,31,0,0,189, - 190,5,137,0,0,190,191,3,70,35,0,191,192,5,138,0,0,192,193,5,33,0,0,193, - 194,3,4,2,0,194,3,1,0,0,0,195,207,3,20,10,0,196,207,3,38,19,0,197,207, - 3,22,11,0,198,207,3,24,12,0,199,207,3,32,16,0,200,207,3,40,20,0,201,207, - 3,64,32,0,202,207,3,8,4,0,203,207,3,12,6,0,204,207,3,6,3,0,205,207,3,70, - 35,0,206,195,1,0,0,0,206,196,1,0,0,0,206,197,1,0,0,0,206,198,1,0,0,0,206, - 199,1,0,0,0,206,200,1,0,0,0,206,201,1,0,0,0,206,202,1,0,0,0,206,203,1, - 0,0,0,206,204,1,0,0,0,206,205,1,0,0,0,207,5,1,0,0,0,208,209,7,0,0,0,209, - 218,3,128,64,0,210,215,3,112,56,0,211,212,5,139,0,0,212,214,3,112,56,0, - 213,211,1,0,0,0,214,217,1,0,0,0,215,213,1,0,0,0,215,216,1,0,0,0,216,219, - 1,0,0,0,217,215,1,0,0,0,218,210,1,0,0,0,218,219,1,0,0,0,219,7,1,0,0,0, - 220,221,5,79,0,0,221,226,3,98,49,0,222,223,5,139,0,0,223,225,3,98,49,0, - 224,222,1,0,0,0,225,228,1,0,0,0,226,224,1,0,0,0,226,227,1,0,0,0,227,229, - 1,0,0,0,228,226,1,0,0,0,229,230,5,83,0,0,230,235,3,10,5,0,231,232,5,139, - 0,0,232,234,3,10,5,0,233,231,1,0,0,0,234,237,1,0,0,0,235,233,1,0,0,0,235, - 236,1,0,0,0,236,239,1,0,0,0,237,235,1,0,0,0,238,240,3,106,53,0,239,238, - 1,0,0,0,239,240,1,0,0,0,240,9,1,0,0,0,241,242,3,124,62,0,242,243,5,131, - 0,0,243,244,3,112,56,0,244,11,1,0,0,0,245,251,5,78,0,0,246,247,3,128,64, - 0,247,248,5,140,0,0,248,249,5,124,0,0,249,252,1,0,0,0,250,252,5,124,0, - 0,251,246,1,0,0,0,251,250,1,0,0,0,251,252,1,0,0,0,252,253,1,0,0,0,253, - 254,5,2,0,0,254,259,3,98,49,0,255,256,5,139,0,0,256,258,3,98,49,0,257, - 255,1,0,0,0,258,261,1,0,0,0,259,257,1,0,0,0,259,260,1,0,0,0,260,263,1, - 0,0,0,261,259,1,0,0,0,262,264,3,106,53,0,263,262,1,0,0,0,263,264,1,0,0, - 0,264,13,1,0,0,0,265,266,5,1,0,0,266,271,3,16,8,0,267,268,5,139,0,0,268, - 270,3,16,8,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,15,1,0,0,0,273,271,1,0,0,0,274,279,5,143,0,0,275,277,5,5,0,0, - 276,275,1,0,0,0,276,277,1,0,0,0,277,278,1,0,0,0,278,280,3,128,64,0,279, - 276,1,0,0,0,279,280,1,0,0,0,280,17,1,0,0,0,281,282,5,100,0,0,282,287,3, - 28,14,0,283,284,5,139,0,0,284,286,3,28,14,0,285,283,1,0,0,0,286,289,1, - 0,0,0,287,285,1,0,0,0,287,288,1,0,0,0,288,290,1,0,0,0,289,287,1,0,0,0, - 290,291,5,142,0,0,291,19,1,0,0,0,292,294,5,57,0,0,293,295,5,91,0,0,294, - 293,1,0,0,0,294,295,1,0,0,0,295,296,1,0,0,0,296,297,5,58,0,0,297,298,3, - 128,64,0,298,299,5,137,0,0,299,304,3,46,23,0,300,301,5,139,0,0,301,303, - 3,46,23,0,302,300,1,0,0,0,303,306,1,0,0,0,304,302,1,0,0,0,304,305,1,0, - 0,0,305,311,1,0,0,0,306,304,1,0,0,0,307,308,5,139,0,0,308,310,3,56,28, - 0,309,307,1,0,0,0,310,313,1,0,0,0,311,309,1,0,0,0,311,312,1,0,0,0,312, - 314,1,0,0,0,313,311,1,0,0,0,314,315,5,138,0,0,315,21,1,0,0,0,316,317,5, - 57,0,0,317,318,5,98,0,0,318,330,3,128,64,0,319,320,5,137,0,0,320,325,3, - 128,64,0,321,322,5,139,0,0,322,324,3,128,64,0,323,321,1,0,0,0,324,327, - 1,0,0,0,325,323,1,0,0,0,325,326,1,0,0,0,326,328,1,0,0,0,327,325,1,0,0, - 0,328,329,5,138,0,0,329,331,1,0,0,0,330,319,1,0,0,0,330,331,1,0,0,0,331, - 332,1,0,0,0,332,333,5,5,0,0,333,334,3,70,35,0,334,23,1,0,0,0,335,336,5, - 57,0,0,336,337,5,99,0,0,337,339,3,128,64,0,338,340,3,26,13,0,339,338,1, - 0,0,0,339,340,1,0,0,0,340,341,1,0,0,0,341,342,5,5,0,0,342,343,3,36,18, - 0,343,25,1,0,0,0,344,345,5,137,0,0,345,350,3,28,14,0,346,347,5,139,0,0, - 347,349,3,28,14,0,348,346,1,0,0,0,349,352,1,0,0,0,350,348,1,0,0,0,350, - 351,1,0,0,0,351,353,1,0,0,0,352,350,1,0,0,0,353,354,5,138,0,0,354,364, - 1,0,0,0,355,360,3,28,14,0,356,357,5,139,0,0,357,359,3,28,14,0,358,356, - 1,0,0,0,359,362,1,0,0,0,360,358,1,0,0,0,360,361,1,0,0,0,361,364,1,0,0, - 0,362,360,1,0,0,0,363,344,1,0,0,0,363,355,1,0,0,0,364,27,1,0,0,0,365,366, - 3,30,15,0,366,367,3,50,25,0,367,29,1,0,0,0,368,371,3,124,62,0,369,371, - 5,144,0,0,370,368,1,0,0,0,370,369,1,0,0,0,371,31,1,0,0,0,372,373,5,64, - 0,0,373,374,5,58,0,0,374,375,3,128,64,0,375,376,3,34,17,0,376,33,1,0,0, - 0,377,379,5,67,0,0,378,380,5,69,0,0,379,378,1,0,0,0,379,380,1,0,0,0,380, - 381,1,0,0,0,381,435,3,46,23,0,382,383,5,67,0,0,383,435,3,56,28,0,384,386, - 5,64,0,0,385,387,5,69,0,0,386,385,1,0,0,0,386,387,1,0,0,0,387,388,1,0, - 0,0,388,389,3,128,64,0,389,393,3,50,25,0,390,392,3,54,27,0,391,390,1,0, - 0,0,392,395,1,0,0,0,393,391,1,0,0,0,393,394,1,0,0,0,394,435,1,0,0,0,395, - 393,1,0,0,0,396,398,5,64,0,0,397,399,5,69,0,0,398,397,1,0,0,0,398,399, - 1,0,0,0,399,400,1,0,0,0,400,401,3,128,64,0,401,402,5,83,0,0,402,403,5, - 84,0,0,403,404,3,112,56,0,404,435,1,0,0,0,405,407,5,64,0,0,406,408,5,69, - 0,0,407,406,1,0,0,0,407,408,1,0,0,0,408,409,1,0,0,0,409,410,3,128,64,0, - 410,411,5,68,0,0,411,412,5,84,0,0,412,435,1,0,0,0,413,414,5,68,0,0,414, - 415,5,69,0,0,415,435,3,128,64,0,416,417,5,68,0,0,417,418,5,75,0,0,418, - 435,3,128,64,0,419,420,5,65,0,0,420,421,5,66,0,0,421,435,3,128,64,0,422, - 423,5,65,0,0,423,424,5,69,0,0,424,425,3,128,64,0,425,426,5,66,0,0,426, - 427,3,128,64,0,427,435,1,0,0,0,428,429,5,65,0,0,429,430,5,90,0,0,430,431, - 3,128,64,0,431,432,5,66,0,0,432,433,3,128,64,0,433,435,1,0,0,0,434,377, - 1,0,0,0,434,382,1,0,0,0,434,384,1,0,0,0,434,396,1,0,0,0,434,405,1,0,0, - 0,434,413,1,0,0,0,434,416,1,0,0,0,434,419,1,0,0,0,434,422,1,0,0,0,434, - 428,1,0,0,0,435,35,1,0,0,0,436,442,3,70,35,0,437,442,3,64,32,0,438,442, - 3,8,4,0,439,442,3,12,6,0,440,442,3,20,10,0,441,436,1,0,0,0,441,437,1,0, - 0,0,441,438,1,0,0,0,441,439,1,0,0,0,441,440,1,0,0,0,442,37,1,0,0,0,443, - 445,5,57,0,0,444,446,5,86,0,0,445,444,1,0,0,0,445,446,1,0,0,0,446,447, - 1,0,0,0,447,448,5,90,0,0,448,449,3,128,64,0,449,450,5,25,0,0,450,451,3, - 128,64,0,451,452,5,137,0,0,452,457,3,42,21,0,453,454,5,139,0,0,454,456, - 3,42,21,0,455,453,1,0,0,0,456,459,1,0,0,0,457,455,1,0,0,0,457,458,1,0, - 0,0,458,460,1,0,0,0,459,457,1,0,0,0,460,463,5,138,0,0,461,462,5,92,0,0, - 462,464,3,44,22,0,463,461,1,0,0,0,463,464,1,0,0,0,464,39,1,0,0,0,465,466, - 5,68,0,0,466,467,5,58,0,0,467,481,3,128,64,0,468,469,5,68,0,0,469,470, - 5,90,0,0,470,471,3,128,64,0,471,472,5,25,0,0,472,473,3,128,64,0,473,481, - 1,0,0,0,474,475,5,68,0,0,475,476,5,99,0,0,476,481,3,128,64,0,477,478,5, - 68,0,0,478,479,5,98,0,0,479,481,3,128,64,0,480,465,1,0,0,0,480,468,1,0, - 0,0,480,474,1,0,0,0,480,477,1,0,0,0,481,41,1,0,0,0,482,484,3,128,64,0, - 483,485,7,1,0,0,484,483,1,0,0,0,484,485,1,0,0,0,485,43,1,0,0,0,486,492, - 5,73,0,0,487,488,5,95,0,0,488,492,5,109,0,0,489,490,5,96,0,0,490,492,5, - 109,0,0,491,486,1,0,0,0,491,487,1,0,0,0,491,489,1,0,0,0,492,45,1,0,0,0, - 493,494,3,128,64,0,494,496,3,50,25,0,495,497,3,48,24,0,496,495,1,0,0,0, - 496,497,1,0,0,0,497,501,1,0,0,0,498,500,3,54,27,0,499,498,1,0,0,0,500, - 503,1,0,0,0,501,499,1,0,0,0,501,502,1,0,0,0,502,47,1,0,0,0,503,501,1,0, - 0,0,504,505,5,5,0,0,505,506,5,137,0,0,506,507,3,112,56,0,507,508,5,138, - 0,0,508,49,1,0,0,0,509,512,3,128,64,0,510,512,5,88,0,0,511,509,1,0,0,0, - 511,510,1,0,0,0,512,514,1,0,0,0,513,515,3,128,64,0,514,513,1,0,0,0,514, - 515,1,0,0,0,515,517,1,0,0,0,516,518,3,128,64,0,517,516,1,0,0,0,517,518, - 1,0,0,0,518,527,1,0,0,0,519,520,5,137,0,0,520,523,3,52,26,0,521,522,5, - 139,0,0,522,524,3,52,26,0,523,521,1,0,0,0,523,524,1,0,0,0,524,525,1,0, - 0,0,525,526,5,138,0,0,526,528,1,0,0,0,527,519,1,0,0,0,527,528,1,0,0,0, - 528,51,1,0,0,0,529,531,5,129,0,0,530,529,1,0,0,0,530,531,1,0,0,0,531,532, - 1,0,0,0,532,533,5,146,0,0,533,53,1,0,0,0,534,535,5,8,0,0,535,604,5,109, - 0,0,536,604,5,109,0,0,537,538,5,84,0,0,538,604,3,112,56,0,539,540,5,92, - 0,0,540,604,7,2,0,0,541,542,5,75,0,0,542,544,3,128,64,0,543,541,1,0,0, - 0,543,544,1,0,0,0,544,545,1,0,0,0,545,546,5,97,0,0,546,547,5,137,0,0,547, - 548,3,58,29,0,548,549,5,138,0,0,549,604,1,0,0,0,550,551,5,75,0,0,551,553, - 3,128,64,0,552,550,1,0,0,0,552,553,1,0,0,0,553,554,1,0,0,0,554,555,5,73, - 0,0,555,557,5,74,0,0,556,558,3,138,69,0,557,556,1,0,0,0,557,558,1,0,0, - 0,558,604,1,0,0,0,559,560,5,75,0,0,560,562,3,128,64,0,561,559,1,0,0,0, - 561,562,1,0,0,0,562,563,1,0,0,0,563,565,5,86,0,0,564,566,3,138,69,0,565, - 564,1,0,0,0,565,566,1,0,0,0,566,604,1,0,0,0,567,568,5,75,0,0,568,570,3, - 128,64,0,569,567,1,0,0,0,569,570,1,0,0,0,570,571,1,0,0,0,571,572,5,77, - 0,0,572,584,3,128,64,0,573,574,5,137,0,0,574,579,3,128,64,0,575,576,5, - 139,0,0,576,578,3,128,64,0,577,575,1,0,0,0,578,581,1,0,0,0,579,577,1,0, - 0,0,579,580,1,0,0,0,580,582,1,0,0,0,581,579,1,0,0,0,582,583,5,138,0,0, - 583,585,1,0,0,0,584,573,1,0,0,0,584,585,1,0,0,0,585,589,1,0,0,0,586,588, - 3,60,30,0,587,586,1,0,0,0,588,591,1,0,0,0,589,587,1,0,0,0,589,590,1,0, - 0,0,590,604,1,0,0,0,591,589,1,0,0,0,592,601,5,88,0,0,593,594,5,137,0,0, - 594,597,3,52,26,0,595,596,5,139,0,0,596,598,3,52,26,0,597,595,1,0,0,0, - 597,598,1,0,0,0,598,599,1,0,0,0,599,600,5,138,0,0,600,602,1,0,0,0,601, - 593,1,0,0,0,601,602,1,0,0,0,602,604,1,0,0,0,603,534,1,0,0,0,603,536,1, - 0,0,0,603,537,1,0,0,0,603,539,1,0,0,0,603,543,1,0,0,0,603,552,1,0,0,0, - 603,561,1,0,0,0,603,569,1,0,0,0,603,592,1,0,0,0,604,55,1,0,0,0,605,606, - 5,75,0,0,606,608,3,128,64,0,607,605,1,0,0,0,607,608,1,0,0,0,608,609,1, - 0,0,0,609,610,5,73,0,0,610,612,5,74,0,0,611,613,3,138,69,0,612,611,1,0, - 0,0,612,613,1,0,0,0,613,614,1,0,0,0,614,615,5,137,0,0,615,620,3,128,64, - 0,616,617,5,139,0,0,617,619,3,128,64,0,618,616,1,0,0,0,619,622,1,0,0,0, - 620,618,1,0,0,0,620,621,1,0,0,0,621,623,1,0,0,0,622,620,1,0,0,0,623,624, - 5,138,0,0,624,695,1,0,0,0,625,626,5,75,0,0,626,628,3,128,64,0,627,625, - 1,0,0,0,627,628,1,0,0,0,628,629,1,0,0,0,629,631,5,86,0,0,630,632,3,138, - 69,0,631,630,1,0,0,0,631,632,1,0,0,0,632,633,1,0,0,0,633,634,5,137,0,0, - 634,639,3,128,64,0,635,636,5,139,0,0,636,638,3,128,64,0,637,635,1,0,0, - 0,638,641,1,0,0,0,639,637,1,0,0,0,639,640,1,0,0,0,640,642,1,0,0,0,641, - 639,1,0,0,0,642,643,5,138,0,0,643,695,1,0,0,0,644,645,5,75,0,0,645,647, - 3,128,64,0,646,644,1,0,0,0,646,647,1,0,0,0,647,648,1,0,0,0,648,649,5,76, - 0,0,649,652,5,74,0,0,650,651,5,85,0,0,651,653,5,90,0,0,652,650,1,0,0,0, - 652,653,1,0,0,0,653,654,1,0,0,0,654,655,5,137,0,0,655,660,3,128,64,0,656, - 657,5,139,0,0,657,659,3,128,64,0,658,656,1,0,0,0,659,662,1,0,0,0,660,658, - 1,0,0,0,660,661,1,0,0,0,661,663,1,0,0,0,662,660,1,0,0,0,663,664,5,138, - 0,0,664,665,5,77,0,0,665,677,3,128,64,0,666,667,5,137,0,0,667,672,3,128, - 64,0,668,669,5,139,0,0,669,671,3,128,64,0,670,668,1,0,0,0,671,674,1,0, - 0,0,672,670,1,0,0,0,672,673,1,0,0,0,673,675,1,0,0,0,674,672,1,0,0,0,675, - 676,5,138,0,0,676,678,1,0,0,0,677,666,1,0,0,0,677,678,1,0,0,0,678,682, - 1,0,0,0,679,681,3,60,30,0,680,679,1,0,0,0,681,684,1,0,0,0,682,680,1,0, - 0,0,682,683,1,0,0,0,683,695,1,0,0,0,684,682,1,0,0,0,685,686,5,75,0,0,686, - 688,3,128,64,0,687,685,1,0,0,0,687,688,1,0,0,0,688,689,1,0,0,0,689,690, - 5,97,0,0,690,691,5,137,0,0,691,692,3,58,29,0,692,693,5,138,0,0,693,695, - 1,0,0,0,694,607,1,0,0,0,694,627,1,0,0,0,694,646,1,0,0,0,694,687,1,0,0, - 0,695,57,1,0,0,0,696,702,8,3,0,0,697,698,5,137,0,0,698,699,3,58,29,0,699, - 700,5,138,0,0,700,702,1,0,0,0,701,696,1,0,0,0,701,697,1,0,0,0,702,705, - 1,0,0,0,703,701,1,0,0,0,703,704,1,0,0,0,704,59,1,0,0,0,705,703,1,0,0,0, - 706,707,5,25,0,0,707,708,5,79,0,0,708,713,3,62,31,0,709,710,5,25,0,0,710, - 711,5,78,0,0,711,713,3,62,31,0,712,706,1,0,0,0,712,709,1,0,0,0,713,61, - 1,0,0,0,714,723,5,80,0,0,715,716,5,85,0,0,716,723,5,82,0,0,717,723,5,81, - 0,0,718,719,5,83,0,0,719,723,5,109,0,0,720,721,5,83,0,0,721,723,5,84,0, - 0,722,714,1,0,0,0,722,715,1,0,0,0,722,717,1,0,0,0,722,718,1,0,0,0,722, - 720,1,0,0,0,723,63,1,0,0,0,724,725,5,70,0,0,725,726,5,71,0,0,726,743,3, - 128,64,0,727,728,5,137,0,0,728,733,3,128,64,0,729,730,5,139,0,0,730,732, - 3,128,64,0,731,729,1,0,0,0,732,735,1,0,0,0,733,731,1,0,0,0,733,734,1,0, - 0,0,734,736,1,0,0,0,735,733,1,0,0,0,736,737,5,138,0,0,737,739,1,0,0,0, - 738,727,1,0,0,0,738,739,1,0,0,0,739,740,1,0,0,0,740,744,3,70,35,0,741, - 742,5,84,0,0,742,744,5,72,0,0,743,738,1,0,0,0,743,741,1,0,0,0,744,65,1, - 0,0,0,745,746,5,137,0,0,746,751,3,68,34,0,747,748,5,139,0,0,748,750,3, - 68,34,0,749,747,1,0,0,0,750,753,1,0,0,0,751,749,1,0,0,0,751,752,1,0,0, - 0,752,754,1,0,0,0,753,751,1,0,0,0,754,755,5,138,0,0,755,67,1,0,0,0,756, - 759,5,84,0,0,757,759,3,112,56,0,758,756,1,0,0,0,758,757,1,0,0,0,759,69, - 1,0,0,0,760,766,3,72,36,0,761,762,3,74,37,0,762,763,3,72,36,0,763,765, - 1,0,0,0,764,761,1,0,0,0,765,768,1,0,0,0,766,764,1,0,0,0,766,767,1,0,0, - 0,767,770,1,0,0,0,768,766,1,0,0,0,769,771,3,108,54,0,770,769,1,0,0,0,770, - 771,1,0,0,0,771,773,1,0,0,0,772,774,3,88,44,0,773,772,1,0,0,0,773,774, - 1,0,0,0,774,71,1,0,0,0,775,790,3,76,38,0,776,777,5,137,0,0,777,778,3,70, - 35,0,778,779,5,138,0,0,779,790,1,0,0,0,780,781,5,72,0,0,781,786,3,66,33, - 0,782,783,5,139,0,0,783,785,3,66,33,0,784,782,1,0,0,0,785,788,1,0,0,0, - 786,784,1,0,0,0,786,787,1,0,0,0,787,790,1,0,0,0,788,786,1,0,0,0,789,775, - 1,0,0,0,789,776,1,0,0,0,789,780,1,0,0,0,790,73,1,0,0,0,791,793,5,53,0, - 0,792,794,5,54,0,0,793,792,1,0,0,0,793,794,1,0,0,0,794,798,1,0,0,0,795, - 798,5,55,0,0,796,798,5,56,0,0,797,791,1,0,0,0,797,795,1,0,0,0,797,796, - 1,0,0,0,798,75,1,0,0,0,799,801,5,1,0,0,800,802,3,78,39,0,801,800,1,0,0, - 0,801,802,1,0,0,0,802,804,1,0,0,0,803,805,3,84,42,0,804,803,1,0,0,0,804, - 805,1,0,0,0,805,806,1,0,0,0,806,809,3,92,46,0,807,808,5,71,0,0,808,810, - 3,128,64,0,809,807,1,0,0,0,809,810,1,0,0,0,810,812,1,0,0,0,811,813,3,96, - 48,0,812,811,1,0,0,0,812,813,1,0,0,0,813,815,1,0,0,0,814,816,3,106,53, - 0,815,814,1,0,0,0,815,816,1,0,0,0,816,818,1,0,0,0,817,819,3,80,40,0,818, - 817,1,0,0,0,818,819,1,0,0,0,819,821,1,0,0,0,820,822,3,82,41,0,821,820, - 1,0,0,0,821,822,1,0,0,0,822,77,1,0,0,0,823,824,7,4,0,0,824,79,1,0,0,0, - 825,826,5,27,0,0,826,827,5,29,0,0,827,832,3,112,56,0,828,829,5,139,0,0, - 829,831,3,112,56,0,830,828,1,0,0,0,831,834,1,0,0,0,832,830,1,0,0,0,832, - 833,1,0,0,0,833,81,1,0,0,0,834,832,1,0,0,0,835,836,5,30,0,0,836,837,3, - 112,56,0,837,83,1,0,0,0,838,839,5,4,0,0,839,844,3,86,43,0,840,841,7,5, - 0,0,841,843,3,86,43,0,842,840,1,0,0,0,843,846,1,0,0,0,844,842,1,0,0,0, - 844,845,1,0,0,0,845,848,1,0,0,0,846,844,1,0,0,0,847,849,5,36,0,0,848,847, - 1,0,0,0,848,849,1,0,0,0,849,852,1,0,0,0,850,851,5,92,0,0,851,853,5,117, - 0,0,852,850,1,0,0,0,852,853,1,0,0,0,853,85,1,0,0,0,854,861,5,146,0,0,855, - 861,5,144,0,0,856,857,5,137,0,0,857,858,3,112,56,0,858,859,5,138,0,0,859, - 861,1,0,0,0,860,854,1,0,0,0,860,855,1,0,0,0,860,856,1,0,0,0,861,87,1,0, - 0,0,862,863,5,45,0,0,863,864,3,112,56,0,864,874,3,90,45,0,865,866,5,46, - 0,0,866,867,7,6,0,0,867,868,3,112,56,0,868,872,3,90,45,0,869,873,5,51, - 0,0,870,871,5,92,0,0,871,873,5,117,0,0,872,869,1,0,0,0,872,870,1,0,0,0, - 873,875,1,0,0,0,874,865,1,0,0,0,874,875,1,0,0,0,875,886,1,0,0,0,876,877, - 5,46,0,0,877,878,7,6,0,0,878,879,3,112,56,0,879,883,3,90,45,0,880,884, - 5,51,0,0,881,882,5,92,0,0,882,884,5,117,0,0,883,880,1,0,0,0,883,881,1, - 0,0,0,884,886,1,0,0,0,885,862,1,0,0,0,885,876,1,0,0,0,886,89,1,0,0,0,887, - 888,7,7,0,0,888,91,1,0,0,0,889,899,5,124,0,0,890,895,3,94,47,0,891,892, - 5,139,0,0,892,894,3,94,47,0,893,891,1,0,0,0,894,897,1,0,0,0,895,893,1, - 0,0,0,895,896,1,0,0,0,896,899,1,0,0,0,897,895,1,0,0,0,898,889,1,0,0,0, - 898,890,1,0,0,0,899,93,1,0,0,0,900,901,3,128,64,0,901,902,5,140,0,0,902, - 903,5,124,0,0,903,912,1,0,0,0,904,909,3,112,56,0,905,907,5,5,0,0,906,905, - 1,0,0,0,906,907,1,0,0,0,907,908,1,0,0,0,908,910,3,128,64,0,909,906,1,0, - 0,0,909,910,1,0,0,0,910,912,1,0,0,0,911,900,1,0,0,0,911,904,1,0,0,0,912, - 95,1,0,0,0,913,914,5,2,0,0,914,919,3,98,49,0,915,916,5,139,0,0,916,918, - 3,98,49,0,917,915,1,0,0,0,918,921,1,0,0,0,919,917,1,0,0,0,919,920,1,0, - 0,0,920,97,1,0,0,0,921,919,1,0,0,0,922,926,3,100,50,0,923,925,3,102,51, - 0,924,923,1,0,0,0,925,928,1,0,0,0,926,924,1,0,0,0,926,927,1,0,0,0,927, - 99,1,0,0,0,928,926,1,0,0,0,929,934,3,128,64,0,930,932,5,5,0,0,931,930, - 1,0,0,0,931,932,1,0,0,0,932,933,1,0,0,0,933,935,3,128,64,0,934,931,1,0, - 0,0,934,935,1,0,0,0,935,953,1,0,0,0,936,937,5,137,0,0,937,938,3,70,35, - 0,938,946,5,138,0,0,939,941,5,5,0,0,940,939,1,0,0,0,940,941,1,0,0,0,941, - 942,1,0,0,0,942,944,3,128,64,0,943,945,3,158,79,0,944,943,1,0,0,0,944, - 945,1,0,0,0,945,947,1,0,0,0,946,940,1,0,0,0,946,947,1,0,0,0,947,953,1, - 0,0,0,948,949,5,137,0,0,949,950,3,98,49,0,950,951,5,138,0,0,951,953,1, - 0,0,0,952,929,1,0,0,0,952,936,1,0,0,0,952,948,1,0,0,0,953,101,1,0,0,0, - 954,955,3,104,52,0,955,956,5,23,0,0,956,957,3,100,50,0,957,958,5,25,0, - 0,958,959,3,112,56,0,959,970,1,0,0,0,960,961,5,37,0,0,961,962,5,23,0,0, - 962,970,3,100,50,0,963,964,5,37,0,0,964,965,5,38,0,0,965,970,3,100,50, - 0,966,967,5,22,0,0,967,968,5,38,0,0,968,970,3,100,50,0,969,954,1,0,0,0, - 969,960,1,0,0,0,969,963,1,0,0,0,969,966,1,0,0,0,970,103,1,0,0,0,971,973, - 5,18,0,0,972,971,1,0,0,0,972,973,1,0,0,0,973,987,1,0,0,0,974,976,5,19, - 0,0,975,977,5,22,0,0,976,975,1,0,0,0,976,977,1,0,0,0,977,987,1,0,0,0,978, - 980,5,20,0,0,979,981,5,22,0,0,980,979,1,0,0,0,980,981,1,0,0,0,981,987, - 1,0,0,0,982,984,5,21,0,0,983,985,5,22,0,0,984,983,1,0,0,0,984,985,1,0, - 0,0,985,987,1,0,0,0,986,972,1,0,0,0,986,974,1,0,0,0,986,978,1,0,0,0,986, - 982,1,0,0,0,987,105,1,0,0,0,988,989,5,3,0,0,989,990,3,112,56,0,990,107, - 1,0,0,0,991,992,5,26,0,0,992,993,5,29,0,0,993,998,3,110,55,0,994,995,5, - 139,0,0,995,997,3,110,55,0,996,994,1,0,0,0,997,1000,1,0,0,0,998,996,1, - 0,0,0,998,999,1,0,0,0,999,109,1,0,0,0,1000,998,1,0,0,0,1001,1003,3,112, - 56,0,1002,1004,7,1,0,0,1003,1002,1,0,0,0,1003,1004,1,0,0,0,1004,111,1, - 0,0,0,1005,1006,6,56,-1,0,1006,1007,7,5,0,0,1007,1012,3,112,56,21,1008, - 1009,7,8,0,0,1009,1012,3,112,56,7,1010,1012,3,114,57,0,1011,1005,1,0,0, - 0,1011,1008,1,0,0,0,1011,1010,1,0,0,0,1012,1111,1,0,0,0,1013,1014,10,22, - 0,0,1014,1015,5,127,0,0,1015,1110,3,112,56,23,1016,1017,10,20,0,0,1017, - 1018,7,9,0,0,1018,1110,3,112,56,21,1019,1020,10,19,0,0,1020,1021,5,126, - 0,0,1021,1110,3,112,56,20,1022,1023,10,18,0,0,1023,1024,5,17,0,0,1024, - 1110,3,112,56,19,1025,1026,10,17,0,0,1026,1027,7,5,0,0,1027,1110,3,112, - 56,18,1028,1029,10,16,0,0,1029,1030,5,130,0,0,1030,1110,3,112,56,17,1031, - 1032,10,15,0,0,1032,1033,7,10,0,0,1033,1110,3,112,56,16,1034,1036,10,14, - 0,0,1035,1037,5,8,0,0,1036,1035,1,0,0,0,1036,1037,1,0,0,0,1037,1038,1, - 0,0,0,1038,1039,5,52,0,0,1039,1040,3,112,56,0,1040,1041,5,6,0,0,1041,1042, - 3,112,56,15,1042,1110,1,0,0,0,1043,1045,10,13,0,0,1044,1046,5,8,0,0,1045, - 1044,1,0,0,0,1045,1046,1,0,0,0,1046,1047,1,0,0,0,1047,1048,5,16,0,0,1048, - 1110,3,112,56,14,1049,1050,10,8,0,0,1050,1052,5,28,0,0,1051,1053,5,8,0, - 0,1052,1051,1,0,0,0,1052,1053,1,0,0,0,1053,1054,1,0,0,0,1054,1055,5,35, - 0,0,1055,1056,5,2,0,0,1056,1110,3,112,56,9,1057,1058,10,6,0,0,1058,1059, - 7,11,0,0,1059,1110,3,112,56,7,1060,1061,10,5,0,0,1061,1062,7,12,0,0,1062, - 1110,3,112,56,6,1063,1064,10,4,0,0,1064,1065,7,13,0,0,1065,1110,3,112, - 56,5,1066,1067,10,3,0,0,1067,1068,5,10,0,0,1068,1110,3,112,56,4,1069,1070, - 10,2,0,0,1070,1071,5,11,0,0,1071,1110,3,112,56,3,1072,1074,10,12,0,0,1073, - 1075,5,8,0,0,1074,1073,1,0,0,0,1074,1075,1,0,0,0,1075,1076,1,0,0,0,1076, - 1077,5,24,0,0,1077,1078,5,137,0,0,1078,1079,3,70,35,0,1079,1080,5,138, - 0,0,1080,1110,1,0,0,0,1081,1083,10,11,0,0,1082,1084,5,8,0,0,1083,1082, - 1,0,0,0,1083,1084,1,0,0,0,1084,1085,1,0,0,0,1085,1086,5,24,0,0,1086,1087, - 5,137,0,0,1087,1092,3,112,56,0,1088,1089,5,139,0,0,1089,1091,3,112,56, - 0,1090,1088,1,0,0,0,1091,1094,1,0,0,0,1092,1090,1,0,0,0,1092,1093,1,0, - 0,0,1093,1095,1,0,0,0,1094,1092,1,0,0,0,1095,1096,5,138,0,0,1096,1110, - 1,0,0,0,1097,1098,10,10,0,0,1098,1100,5,28,0,0,1099,1101,5,8,0,0,1100, - 1099,1,0,0,0,1100,1101,1,0,0,0,1101,1102,1,0,0,0,1102,1110,5,109,0,0,1103, - 1104,10,9,0,0,1104,1106,5,28,0,0,1105,1107,5,8,0,0,1106,1105,1,0,0,0,1106, - 1107,1,0,0,0,1107,1108,1,0,0,0,1108,1110,7,14,0,0,1109,1013,1,0,0,0,1109, - 1016,1,0,0,0,1109,1019,1,0,0,0,1109,1022,1,0,0,0,1109,1025,1,0,0,0,1109, - 1028,1,0,0,0,1109,1031,1,0,0,0,1109,1034,1,0,0,0,1109,1043,1,0,0,0,1109, - 1049,1,0,0,0,1109,1057,1,0,0,0,1109,1060,1,0,0,0,1109,1063,1,0,0,0,1109, - 1066,1,0,0,0,1109,1069,1,0,0,0,1109,1072,1,0,0,0,1109,1081,1,0,0,0,1109, - 1097,1,0,0,0,1109,1103,1,0,0,0,1110,1113,1,0,0,0,1111,1109,1,0,0,0,1111, - 1112,1,0,0,0,1112,113,1,0,0,0,1113,1111,1,0,0,0,1114,1134,3,130,65,0,1115, - 1134,3,116,58,0,1116,1134,3,120,60,0,1117,1134,3,124,62,0,1118,1134,5, - 144,0,0,1119,1134,5,143,0,0,1120,1121,5,31,0,0,1121,1122,5,137,0,0,1122, - 1123,3,70,35,0,1123,1124,5,138,0,0,1124,1134,1,0,0,0,1125,1126,5,137,0, - 0,1126,1127,3,70,35,0,1127,1128,5,138,0,0,1128,1134,1,0,0,0,1129,1130, - 5,137,0,0,1130,1131,3,112,56,0,1131,1132,5,138,0,0,1132,1134,1,0,0,0,1133, - 1114,1,0,0,0,1133,1115,1,0,0,0,1133,1116,1,0,0,0,1133,1117,1,0,0,0,1133, - 1118,1,0,0,0,1133,1119,1,0,0,0,1133,1120,1,0,0,0,1133,1125,1,0,0,0,1133, - 1129,1,0,0,0,1134,115,1,0,0,0,1135,1137,5,41,0,0,1136,1138,3,112,56,0, - 1137,1136,1,0,0,0,1137,1138,1,0,0,0,1138,1140,1,0,0,0,1139,1141,3,118, - 59,0,1140,1139,1,0,0,0,1141,1142,1,0,0,0,1142,1140,1,0,0,0,1142,1143,1, - 0,0,0,1143,1146,1,0,0,0,1144,1145,5,43,0,0,1145,1147,3,112,56,0,1146,1144, - 1,0,0,0,1146,1147,1,0,0,0,1147,1148,1,0,0,0,1148,1149,5,44,0,0,1149,117, - 1,0,0,0,1150,1151,5,42,0,0,1151,1152,3,112,56,0,1152,1153,5,33,0,0,1153, - 1154,3,112,56,0,1154,119,1,0,0,0,1155,1156,3,122,61,0,1156,1169,5,137, - 0,0,1157,1170,5,124,0,0,1158,1160,5,35,0,0,1159,1158,1,0,0,0,1159,1160, - 1,0,0,0,1160,1161,1,0,0,0,1161,1166,3,112,56,0,1162,1163,5,139,0,0,1163, - 1165,3,112,56,0,1164,1162,1,0,0,0,1165,1168,1,0,0,0,1166,1164,1,0,0,0, - 1166,1167,1,0,0,0,1167,1170,1,0,0,0,1168,1166,1,0,0,0,1169,1157,1,0,0, - 0,1169,1159,1,0,0,0,1169,1170,1,0,0,0,1170,1171,1,0,0,0,1171,1173,5,138, - 0,0,1172,1174,3,152,76,0,1173,1172,1,0,0,0,1173,1174,1,0,0,0,1174,1176, - 1,0,0,0,1175,1177,3,150,75,0,1176,1175,1,0,0,0,1176,1177,1,0,0,0,1177, - 1186,1,0,0,0,1178,1180,3,154,77,0,1179,1178,1,0,0,0,1179,1180,1,0,0,0, - 1180,1182,1,0,0,0,1181,1183,3,156,78,0,1182,1181,1,0,0,0,1182,1183,1,0, - 0,0,1183,1184,1,0,0,0,1184,1185,5,39,0,0,1185,1187,3,136,68,0,1186,1179, - 1,0,0,0,1186,1187,1,0,0,0,1187,121,1,0,0,0,1188,1195,3,128,64,0,1189,1195, - 5,19,0,0,1190,1195,5,20,0,0,1191,1195,5,103,0,0,1192,1195,5,48,0,0,1193, - 1195,5,40,0,0,1194,1188,1,0,0,0,1194,1189,1,0,0,0,1194,1190,1,0,0,0,1194, - 1191,1,0,0,0,1194,1192,1,0,0,0,1194,1193,1,0,0,0,1195,123,1,0,0,0,1196, - 1201,3,128,64,0,1197,1198,7,15,0,0,1198,1200,3,126,63,0,1199,1197,1,0, - 0,0,1200,1203,1,0,0,0,1201,1199,1,0,0,0,1201,1202,1,0,0,0,1202,125,1,0, - 0,0,1203,1201,1,0,0,0,1204,1207,3,128,64,0,1205,1207,3,148,74,0,1206,1204, - 1,0,0,0,1206,1205,1,0,0,0,1207,127,1,0,0,0,1208,1213,5,153,0,0,1209,1213, - 5,151,0,0,1210,1213,5,152,0,0,1211,1213,3,146,73,0,1212,1208,1,0,0,0,1212, - 1209,1,0,0,0,1212,1210,1,0,0,0,1212,1211,1,0,0,0,1213,129,1,0,0,0,1214, - 1224,5,146,0,0,1215,1224,5,147,0,0,1216,1224,5,145,0,0,1217,1224,5,148, - 0,0,1218,1224,5,149,0,0,1219,1224,5,150,0,0,1220,1224,7,16,0,0,1221,1224, - 7,17,0,0,1222,1224,5,109,0,0,1223,1214,1,0,0,0,1223,1215,1,0,0,0,1223, - 1216,1,0,0,0,1223,1217,1,0,0,0,1223,1218,1,0,0,0,1223,1219,1,0,0,0,1223, - 1220,1,0,0,0,1223,1221,1,0,0,0,1223,1222,1,0,0,0,1224,131,1,0,0,0,1225, - 1227,5,59,0,0,1226,1228,7,18,0,0,1227,1226,1,0,0,0,1227,1228,1,0,0,0,1228, - 1238,1,0,0,0,1229,1231,5,60,0,0,1230,1232,7,18,0,0,1231,1230,1,0,0,0,1231, - 1232,1,0,0,0,1232,1238,1,0,0,0,1233,1235,5,61,0,0,1234,1236,7,18,0,0,1235, - 1234,1,0,0,0,1235,1236,1,0,0,0,1236,1238,1,0,0,0,1237,1225,1,0,0,0,1237, - 1229,1,0,0,0,1237,1233,1,0,0,0,1238,133,1,0,0,0,1239,1240,3,112,56,0,1240, - 1241,5,0,0,1,1241,135,1,0,0,0,1242,1253,5,137,0,0,1243,1244,5,40,0,0,1244, - 1245,5,29,0,0,1245,1250,3,112,56,0,1246,1247,5,139,0,0,1247,1249,3,112, - 56,0,1248,1246,1,0,0,0,1249,1252,1,0,0,0,1250,1248,1,0,0,0,1250,1251,1, - 0,0,0,1251,1254,1,0,0,0,1252,1250,1,0,0,0,1253,1243,1,0,0,0,1253,1254, - 1,0,0,0,1254,1256,1,0,0,0,1255,1257,3,108,54,0,1256,1255,1,0,0,0,1256, - 1257,1,0,0,0,1257,1259,1,0,0,0,1258,1260,3,140,70,0,1259,1258,1,0,0,0, - 1259,1260,1,0,0,0,1260,1261,1,0,0,0,1261,1262,5,138,0,0,1262,137,1,0,0, - 0,1263,1264,7,19,0,0,1264,139,1,0,0,0,1265,1272,7,20,0,0,1266,1267,5,52, - 0,0,1267,1268,3,142,71,0,1268,1269,5,6,0,0,1269,1270,3,142,71,0,1270,1273, - 1,0,0,0,1271,1273,3,142,71,0,1272,1266,1,0,0,0,1272,1271,1,0,0,0,1273, - 1276,1,0,0,0,1274,1275,5,116,0,0,1275,1277,3,144,72,0,1276,1274,1,0,0, - 0,1276,1277,1,0,0,0,1277,141,1,0,0,0,1278,1279,5,112,0,0,1279,1286,7,21, - 0,0,1280,1281,5,115,0,0,1281,1286,5,50,0,0,1282,1283,3,112,56,0,1283,1284, - 7,21,0,0,1284,1286,1,0,0,0,1285,1278,1,0,0,0,1285,1280,1,0,0,0,1285,1282, - 1,0,0,0,1286,143,1,0,0,0,1287,1288,5,115,0,0,1288,1294,5,50,0,0,1289,1294, - 5,27,0,0,1290,1294,5,117,0,0,1291,1292,5,85,0,0,1292,1294,5,118,0,0,1293, - 1287,1,0,0,0,1293,1289,1,0,0,0,1293,1290,1,0,0,0,1293,1291,1,0,0,0,1294, - 145,1,0,0,0,1295,1296,7,22,0,0,1296,147,1,0,0,0,1297,1298,7,23,0,0,1298, - 149,1,0,0,0,1299,1300,5,123,0,0,1300,1301,5,137,0,0,1301,1302,5,3,0,0, - 1302,1303,3,112,56,0,1303,1304,5,138,0,0,1304,151,1,0,0,0,1305,1306,5, - 119,0,0,1306,1307,5,27,0,0,1307,1308,5,137,0,0,1308,1309,3,108,54,0,1309, - 1310,5,138,0,0,1310,153,1,0,0,0,1311,1312,5,2,0,0,1312,1313,7,24,0,0,1313, - 155,1,0,0,0,1314,1315,7,25,0,0,1315,1316,5,122,0,0,1316,157,1,0,0,0,1317, - 1318,5,137,0,0,1318,1323,3,128,64,0,1319,1320,5,139,0,0,1320,1322,3,128, - 64,0,1321,1319,1,0,0,0,1322,1325,1,0,0,0,1323,1321,1,0,0,0,1323,1324,1, - 0,0,0,1324,1326,1,0,0,0,1325,1323,1,0,0,0,1326,1327,5,138,0,0,1327,159, - 1,0,0,0,168,161,177,180,186,206,215,218,226,235,239,251,259,263,271,276, - 279,287,294,304,311,325,330,339,350,360,363,370,379,386,393,398,407,434, - 441,445,457,463,480,484,491,496,501,511,514,517,523,527,530,543,552,557, - 561,565,569,579,584,589,597,601,603,607,612,620,627,631,639,646,652,660, - 672,677,682,687,694,701,703,712,722,733,738,743,751,758,766,770,773,786, - 789,793,797,801,804,809,812,815,818,821,832,844,848,852,860,872,874,883, - 885,895,898,906,909,911,919,926,931,934,940,944,946,952,969,972,976,980, - 984,986,998,1003,1011,1036,1045,1052,1074,1083,1092,1100,1106,1109,1111, - 1133,1137,1142,1146,1159,1166,1169,1173,1176,1179,1182,1186,1194,1201, - 1206,1212,1223,1227,1231,1235,1237,1250,1253,1256,1259,1272,1276,1285, - 1293,1323 + 1,17,1,17,1,17,3,17,443,8,17,1,18,1,18,1,18,1,18,1,18,3,18,450,8,18,1, + 19,1,19,3,19,454,8,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,5,19,464, + 8,19,10,19,12,19,467,9,19,1,19,1,19,1,19,3,19,472,8,19,1,20,1,20,1,20, + 1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,3,20,489,8, + 20,1,21,1,21,3,21,493,8,21,1,22,1,22,1,22,1,22,1,22,3,22,500,8,22,1,23, + 1,23,1,23,3,23,505,8,23,1,23,5,23,508,8,23,10,23,12,23,511,9,23,1,24,1, + 24,1,24,1,24,1,24,1,25,1,25,3,25,520,8,25,1,25,3,25,523,8,25,1,25,3,25, + 526,8,25,1,25,1,25,1,25,1,25,3,25,532,8,25,1,25,1,25,3,25,536,8,25,1,26, + 3,26,539,8,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3, + 27,552,8,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,561,8,27,1,27,1,27, + 1,27,3,27,566,8,27,1,27,1,27,3,27,570,8,27,1,27,1,27,3,27,574,8,27,1,27, + 1,27,3,27,578,8,27,1,27,1,27,1,27,1,27,1,27,1,27,5,27,586,8,27,10,27,12, + 27,589,9,27,1,27,1,27,3,27,593,8,27,1,27,5,27,596,8,27,10,27,12,27,599, + 9,27,1,27,1,27,1,27,1,27,1,27,3,27,606,8,27,1,27,1,27,3,27,610,8,27,3, + 27,612,8,27,1,28,1,28,3,28,616,8,28,1,28,1,28,1,28,3,28,621,8,28,1,28, + 1,28,1,28,1,28,5,28,627,8,28,10,28,12,28,630,9,28,1,28,1,28,1,28,1,28, + 3,28,636,8,28,1,28,1,28,3,28,640,8,28,1,28,1,28,1,28,1,28,5,28,646,8,28, + 10,28,12,28,649,9,28,1,28,1,28,1,28,1,28,3,28,655,8,28,1,28,1,28,1,28, + 1,28,3,28,661,8,28,1,28,1,28,1,28,1,28,5,28,667,8,28,10,28,12,28,670,9, + 28,1,28,1,28,1,28,1,28,1,28,1,28,1,28,5,28,679,8,28,10,28,12,28,682,9, + 28,1,28,1,28,3,28,686,8,28,1,28,5,28,689,8,28,10,28,12,28,692,9,28,1,28, + 1,28,3,28,696,8,28,1,28,1,28,1,28,1,28,1,28,3,28,703,8,28,1,29,1,29,1, + 29,1,29,1,29,5,29,710,8,29,10,29,12,29,713,9,29,1,30,1,30,1,30,1,30,1, + 30,1,30,3,30,721,8,30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,3,31,731, + 8,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32,5,32,740,8,32,10,32,12,32,743, + 9,32,1,32,1,32,3,32,747,8,32,1,32,1,32,1,32,3,32,752,8,32,1,33,1,33,1, + 33,1,33,5,33,758,8,33,10,33,12,33,761,9,33,1,33,1,33,1,34,1,34,3,34,767, + 8,34,1,35,1,35,1,35,1,35,5,35,773,8,35,10,35,12,35,776,9,35,1,35,3,35, + 779,8,35,1,35,3,35,782,8,35,1,35,3,35,785,8,35,1,36,1,36,1,36,1,36,1,37, + 1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,5,37,800,8,37,10,37,12,37,803, + 9,37,3,37,805,8,37,1,38,1,38,3,38,809,8,38,1,38,1,38,3,38,813,8,38,1,39, + 1,39,3,39,817,8,39,1,39,3,39,820,8,39,1,39,1,39,1,39,3,39,825,8,39,1,39, + 3,39,828,8,39,1,39,3,39,831,8,39,1,39,3,39,834,8,39,1,39,3,39,837,8,39, + 1,39,3,39,840,8,39,1,40,1,40,1,41,1,41,1,41,1,41,1,41,5,41,849,8,41,10, + 41,12,41,852,9,41,1,42,1,42,1,42,1,43,1,43,1,43,1,43,5,43,861,8,43,10, + 43,12,43,864,9,43,1,43,3,43,867,8,43,1,43,1,43,3,43,871,8,43,1,44,1,44, + 1,44,1,44,1,44,1,44,3,44,879,8,44,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1, + 45,1,45,1,45,3,45,891,8,45,3,45,893,8,45,1,45,1,45,1,45,1,45,1,45,1,45, + 1,45,3,45,902,8,45,3,45,904,8,45,1,46,1,46,1,47,1,47,1,47,1,47,5,47,912, + 8,47,10,47,12,47,915,9,47,3,47,917,8,47,1,48,1,48,1,48,1,48,1,48,1,48, + 3,48,925,8,48,1,48,3,48,928,8,48,3,48,930,8,48,1,49,1,49,1,49,1,49,5,49, + 936,8,49,10,49,12,49,939,9,49,1,50,1,50,5,50,943,8,50,10,50,12,50,946, + 9,50,1,51,1,51,3,51,950,8,51,1,51,3,51,953,8,51,1,51,1,51,1,51,1,51,3, + 51,959,8,51,1,51,1,51,3,51,963,8,51,3,51,965,8,51,1,51,1,51,1,51,1,51, + 3,51,971,8,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,3,52,988,8,52,1,53,3,53,991,8,53,1,53,1,53,3,53,995, + 8,53,1,53,1,53,3,53,999,8,53,1,53,1,53,3,53,1003,8,53,3,53,1005,8,53,1, + 54,1,54,1,54,1,55,1,55,1,55,1,55,1,55,5,55,1015,8,55,10,55,12,55,1018, + 9,55,1,56,1,56,3,56,1022,8,56,1,57,1,57,1,57,1,57,1,57,1,57,3,57,1030, + 8,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, + 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,3,57,1055,8,57,1,57, + 1,57,1,57,1,57,1,57,1,57,1,57,3,57,1064,8,57,1,57,1,57,1,57,1,57,1,57, + 3,57,1071,8,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, + 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,3,57,1093,8,57,1,57,1,57, + 1,57,1,57,1,57,1,57,1,57,3,57,1102,8,57,1,57,1,57,1,57,1,57,1,57,5,57, + 1109,8,57,10,57,12,57,1112,9,57,1,57,1,57,1,57,1,57,1,57,3,57,1119,8,57, + 1,57,1,57,1,57,1,57,3,57,1125,8,57,1,57,5,57,1128,8,57,10,57,12,57,1131, + 9,57,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58, + 1,58,1,58,1,58,1,58,1,58,1,58,3,58,1152,8,58,1,59,1,59,3,59,1156,8,59, + 1,59,4,59,1159,8,59,11,59,12,59,1160,1,59,1,59,3,59,1165,8,59,1,59,1,59, + 1,60,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,61,3,61,1178,8,61,1,61,1,61, + 1,61,5,61,1183,8,61,10,61,12,61,1186,9,61,3,61,1188,8,61,1,61,1,61,3,61, + 1192,8,61,1,61,3,61,1195,8,61,1,61,3,61,1198,8,61,1,61,3,61,1201,8,61, + 1,61,1,61,3,61,1205,8,61,1,62,1,62,1,62,1,62,1,62,1,62,3,62,1213,8,62, + 1,63,1,63,1,63,5,63,1218,8,63,10,63,12,63,1221,9,63,1,64,1,64,3,64,1225, + 8,64,1,65,1,65,1,65,1,65,3,65,1231,8,65,1,66,1,66,1,66,1,66,1,66,1,66, + 1,66,1,66,1,66,3,66,1242,8,66,1,67,1,67,3,67,1246,8,67,1,67,1,67,3,67, + 1250,8,67,1,67,1,67,3,67,1254,8,67,3,67,1256,8,67,1,68,1,68,1,68,1,69, + 1,69,1,69,1,69,1,69,1,69,5,69,1267,8,69,10,69,12,69,1270,9,69,3,69,1272, + 8,69,1,69,3,69,1275,8,69,1,69,3,69,1278,8,69,1,69,1,69,1,70,1,70,1,71, + 1,71,1,71,1,71,1,71,1,71,1,71,3,71,1291,8,71,1,71,1,71,3,71,1295,8,71, + 1,72,1,72,1,72,1,72,1,72,1,72,1,72,3,72,1304,8,72,1,73,1,73,1,73,1,73, + 1,73,1,73,3,73,1312,8,73,1,74,1,74,1,75,1,75,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,78,1,78,1,78,1,79,1,79,1,79,1,80, + 1,80,1,80,1,80,5,80,1340,8,80,10,80,12,80,1343,9,80,1,80,1,80,1,80,0,1, + 114,81,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,120,122,124,126,128,130, + 132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,0,26,1,0,101, + 102,1,0,103,104,1,0,93,94,1,0,139,140,2,0,34,35,54,54,1,0,130,131,1,0, + 47,48,1,0,49,50,2,0,8,8,15,15,1,0,126,127,1,0,133,138,2,0,6,6,12,12,2, + 0,7,7,13,13,2,0,9,9,14,14,1,0,105,106,1,0,142,143,3,0,25,25,105,105,107, + 107,3,0,85,85,106,106,108,108,1,0,62,63,2,0,87,87,89,89,2,0,49,49,110, + 111,1,0,113,114,2,0,107,108,110,125,2,0,1,106,109,109,2,0,48,48,120,120, + 2,0,96,96,121,121,1536,0,163,1,0,0,0,2,186,1,0,0,0,4,208,1,0,0,0,6,210, + 1,0,0,0,8,222,1,0,0,0,10,246,1,0,0,0,12,250,1,0,0,0,14,273,1,0,0,0,16, + 282,1,0,0,0,18,289,1,0,0,0,20,300,1,0,0,0,22,324,1,0,0,0,24,343,1,0,0, + 0,26,371,1,0,0,0,28,373,1,0,0,0,30,378,1,0,0,0,32,380,1,0,0,0,34,442,1, + 0,0,0,36,449,1,0,0,0,38,451,1,0,0,0,40,488,1,0,0,0,42,490,1,0,0,0,44,499, + 1,0,0,0,46,501,1,0,0,0,48,512,1,0,0,0,50,519,1,0,0,0,52,538,1,0,0,0,54, + 611,1,0,0,0,56,702,1,0,0,0,58,711,1,0,0,0,60,720,1,0,0,0,62,730,1,0,0, + 0,64,732,1,0,0,0,66,753,1,0,0,0,68,766,1,0,0,0,70,768,1,0,0,0,72,786,1, + 0,0,0,74,804,1,0,0,0,76,812,1,0,0,0,78,814,1,0,0,0,80,841,1,0,0,0,82,843, + 1,0,0,0,84,853,1,0,0,0,86,856,1,0,0,0,88,878,1,0,0,0,90,903,1,0,0,0,92, + 905,1,0,0,0,94,916,1,0,0,0,96,929,1,0,0,0,98,931,1,0,0,0,100,940,1,0,0, + 0,102,970,1,0,0,0,104,987,1,0,0,0,106,1004,1,0,0,0,108,1006,1,0,0,0,110, + 1009,1,0,0,0,112,1019,1,0,0,0,114,1029,1,0,0,0,116,1151,1,0,0,0,118,1153, + 1,0,0,0,120,1168,1,0,0,0,122,1173,1,0,0,0,124,1212,1,0,0,0,126,1214,1, + 0,0,0,128,1224,1,0,0,0,130,1230,1,0,0,0,132,1241,1,0,0,0,134,1255,1,0, + 0,0,136,1257,1,0,0,0,138,1260,1,0,0,0,140,1281,1,0,0,0,142,1283,1,0,0, + 0,144,1303,1,0,0,0,146,1311,1,0,0,0,148,1313,1,0,0,0,150,1315,1,0,0,0, + 152,1317,1,0,0,0,154,1323,1,0,0,0,156,1329,1,0,0,0,158,1332,1,0,0,0,160, + 1335,1,0,0,0,162,164,3,18,9,0,163,162,1,0,0,0,163,164,1,0,0,0,164,179, + 1,0,0,0,165,180,3,2,1,0,166,180,3,20,10,0,167,180,3,38,19,0,168,180,3, + 22,11,0,169,180,3,24,12,0,170,180,3,32,16,0,171,180,3,40,20,0,172,180, + 3,64,32,0,173,180,3,8,4,0,174,180,3,12,6,0,175,180,3,134,67,0,176,180, + 3,6,3,0,177,180,3,14,7,0,178,180,3,70,35,0,179,165,1,0,0,0,179,166,1,0, + 0,0,179,167,1,0,0,0,179,168,1,0,0,0,179,169,1,0,0,0,179,170,1,0,0,0,179, + 171,1,0,0,0,179,172,1,0,0,0,179,173,1,0,0,0,179,174,1,0,0,0,179,175,1, + 0,0,0,179,176,1,0,0,0,179,177,1,0,0,0,179,178,1,0,0,0,180,182,1,0,0,0, + 181,183,5,144,0,0,182,181,1,0,0,0,182,183,1,0,0,0,183,184,1,0,0,0,184, + 185,5,0,0,1,185,1,1,0,0,0,186,188,5,32,0,0,187,189,5,8,0,0,188,187,1,0, + 0,0,188,189,1,0,0,0,189,190,1,0,0,0,190,191,5,31,0,0,191,192,5,139,0,0, + 192,193,3,70,35,0,193,194,5,140,0,0,194,195,5,33,0,0,195,196,3,4,2,0,196, + 3,1,0,0,0,197,209,3,20,10,0,198,209,3,38,19,0,199,209,3,22,11,0,200,209, + 3,24,12,0,201,209,3,32,16,0,202,209,3,40,20,0,203,209,3,64,32,0,204,209, + 3,8,4,0,205,209,3,12,6,0,206,209,3,6,3,0,207,209,3,70,35,0,208,197,1,0, + 0,0,208,198,1,0,0,0,208,199,1,0,0,0,208,200,1,0,0,0,208,201,1,0,0,0,208, + 202,1,0,0,0,208,203,1,0,0,0,208,204,1,0,0,0,208,205,1,0,0,0,208,206,1, + 0,0,0,208,207,1,0,0,0,209,5,1,0,0,0,210,211,7,0,0,0,211,220,3,130,65,0, + 212,217,3,114,57,0,213,214,5,141,0,0,214,216,3,114,57,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,221,1,0,0,0,219, + 217,1,0,0,0,220,212,1,0,0,0,220,221,1,0,0,0,221,7,1,0,0,0,222,223,5,79, + 0,0,223,228,3,100,50,0,224,225,5,141,0,0,225,227,3,100,50,0,226,224,1, + 0,0,0,227,230,1,0,0,0,228,226,1,0,0,0,228,229,1,0,0,0,229,231,1,0,0,0, + 230,228,1,0,0,0,231,232,5,83,0,0,232,237,3,10,5,0,233,234,5,141,0,0,234, + 236,3,10,5,0,235,233,1,0,0,0,236,239,1,0,0,0,237,235,1,0,0,0,237,238,1, + 0,0,0,238,241,1,0,0,0,239,237,1,0,0,0,240,242,3,108,54,0,241,240,1,0,0, + 0,241,242,1,0,0,0,242,244,1,0,0,0,243,245,3,72,36,0,244,243,1,0,0,0,244, + 245,1,0,0,0,245,9,1,0,0,0,246,247,3,126,63,0,247,248,5,133,0,0,248,249, + 3,114,57,0,249,11,1,0,0,0,250,256,5,78,0,0,251,252,3,130,65,0,252,253, + 5,142,0,0,253,254,5,126,0,0,254,257,1,0,0,0,255,257,5,126,0,0,256,251, + 1,0,0,0,256,255,1,0,0,0,256,257,1,0,0,0,257,258,1,0,0,0,258,259,5,2,0, + 0,259,264,3,100,50,0,260,261,5,141,0,0,261,263,3,100,50,0,262,260,1,0, + 0,0,263,266,1,0,0,0,264,262,1,0,0,0,264,265,1,0,0,0,265,268,1,0,0,0,266, + 264,1,0,0,0,267,269,3,108,54,0,268,267,1,0,0,0,268,269,1,0,0,0,269,271, + 1,0,0,0,270,272,3,72,36,0,271,270,1,0,0,0,271,272,1,0,0,0,272,13,1,0,0, + 0,273,274,5,1,0,0,274,279,3,16,8,0,275,276,5,141,0,0,276,278,3,16,8,0, + 277,275,1,0,0,0,278,281,1,0,0,0,279,277,1,0,0,0,279,280,1,0,0,0,280,15, + 1,0,0,0,281,279,1,0,0,0,282,287,5,145,0,0,283,285,5,5,0,0,284,283,1,0, + 0,0,284,285,1,0,0,0,285,286,1,0,0,0,286,288,3,130,65,0,287,284,1,0,0,0, + 287,288,1,0,0,0,288,17,1,0,0,0,289,290,5,100,0,0,290,295,3,28,14,0,291, + 292,5,141,0,0,292,294,3,28,14,0,293,291,1,0,0,0,294,297,1,0,0,0,295,293, + 1,0,0,0,295,296,1,0,0,0,296,298,1,0,0,0,297,295,1,0,0,0,298,299,5,144, + 0,0,299,19,1,0,0,0,300,302,5,57,0,0,301,303,5,91,0,0,302,301,1,0,0,0,302, + 303,1,0,0,0,303,304,1,0,0,0,304,305,5,58,0,0,305,306,3,130,65,0,306,307, + 5,139,0,0,307,312,3,46,23,0,308,309,5,141,0,0,309,311,3,46,23,0,310,308, + 1,0,0,0,311,314,1,0,0,0,312,310,1,0,0,0,312,313,1,0,0,0,313,319,1,0,0, + 0,314,312,1,0,0,0,315,316,5,141,0,0,316,318,3,56,28,0,317,315,1,0,0,0, + 318,321,1,0,0,0,319,317,1,0,0,0,319,320,1,0,0,0,320,322,1,0,0,0,321,319, + 1,0,0,0,322,323,5,140,0,0,323,21,1,0,0,0,324,325,5,57,0,0,325,326,5,98, + 0,0,326,338,3,130,65,0,327,328,5,139,0,0,328,333,3,130,65,0,329,330,5, + 141,0,0,330,332,3,130,65,0,331,329,1,0,0,0,332,335,1,0,0,0,333,331,1,0, + 0,0,333,334,1,0,0,0,334,336,1,0,0,0,335,333,1,0,0,0,336,337,5,140,0,0, + 337,339,1,0,0,0,338,327,1,0,0,0,338,339,1,0,0,0,339,340,1,0,0,0,340,341, + 5,5,0,0,341,342,3,70,35,0,342,23,1,0,0,0,343,344,5,57,0,0,344,345,5,99, + 0,0,345,347,3,130,65,0,346,348,3,26,13,0,347,346,1,0,0,0,347,348,1,0,0, + 0,348,349,1,0,0,0,349,350,5,5,0,0,350,351,3,36,18,0,351,25,1,0,0,0,352, + 353,5,139,0,0,353,358,3,28,14,0,354,355,5,141,0,0,355,357,3,28,14,0,356, + 354,1,0,0,0,357,360,1,0,0,0,358,356,1,0,0,0,358,359,1,0,0,0,359,361,1, + 0,0,0,360,358,1,0,0,0,361,362,5,140,0,0,362,372,1,0,0,0,363,368,3,28,14, + 0,364,365,5,141,0,0,365,367,3,28,14,0,366,364,1,0,0,0,367,370,1,0,0,0, + 368,366,1,0,0,0,368,369,1,0,0,0,369,372,1,0,0,0,370,368,1,0,0,0,371,352, + 1,0,0,0,371,363,1,0,0,0,372,27,1,0,0,0,373,374,3,30,15,0,374,375,3,50, + 25,0,375,29,1,0,0,0,376,379,3,126,63,0,377,379,5,146,0,0,378,376,1,0,0, + 0,378,377,1,0,0,0,379,31,1,0,0,0,380,381,5,64,0,0,381,382,5,58,0,0,382, + 383,3,130,65,0,383,384,3,34,17,0,384,33,1,0,0,0,385,387,5,67,0,0,386,388, + 5,69,0,0,387,386,1,0,0,0,387,388,1,0,0,0,388,389,1,0,0,0,389,443,3,46, + 23,0,390,391,5,67,0,0,391,443,3,56,28,0,392,394,5,64,0,0,393,395,5,69, + 0,0,394,393,1,0,0,0,394,395,1,0,0,0,395,396,1,0,0,0,396,397,3,130,65,0, + 397,401,3,50,25,0,398,400,3,54,27,0,399,398,1,0,0,0,400,403,1,0,0,0,401, + 399,1,0,0,0,401,402,1,0,0,0,402,443,1,0,0,0,403,401,1,0,0,0,404,406,5, + 64,0,0,405,407,5,69,0,0,406,405,1,0,0,0,406,407,1,0,0,0,407,408,1,0,0, + 0,408,409,3,130,65,0,409,410,5,83,0,0,410,411,5,84,0,0,411,412,3,114,57, + 0,412,443,1,0,0,0,413,415,5,64,0,0,414,416,5,69,0,0,415,414,1,0,0,0,415, + 416,1,0,0,0,416,417,1,0,0,0,417,418,3,130,65,0,418,419,5,68,0,0,419,420, + 5,84,0,0,420,443,1,0,0,0,421,422,5,68,0,0,422,423,5,69,0,0,423,443,3,130, + 65,0,424,425,5,68,0,0,425,426,5,75,0,0,426,443,3,130,65,0,427,428,5,65, + 0,0,428,429,5,66,0,0,429,443,3,130,65,0,430,431,5,65,0,0,431,432,5,69, + 0,0,432,433,3,130,65,0,433,434,5,66,0,0,434,435,3,130,65,0,435,443,1,0, + 0,0,436,437,5,65,0,0,437,438,5,90,0,0,438,439,3,130,65,0,439,440,5,66, + 0,0,440,441,3,130,65,0,441,443,1,0,0,0,442,385,1,0,0,0,442,390,1,0,0,0, + 442,392,1,0,0,0,442,404,1,0,0,0,442,413,1,0,0,0,442,421,1,0,0,0,442,424, + 1,0,0,0,442,427,1,0,0,0,442,430,1,0,0,0,442,436,1,0,0,0,443,35,1,0,0,0, + 444,450,3,70,35,0,445,450,3,64,32,0,446,450,3,8,4,0,447,450,3,12,6,0,448, + 450,3,20,10,0,449,444,1,0,0,0,449,445,1,0,0,0,449,446,1,0,0,0,449,447, + 1,0,0,0,449,448,1,0,0,0,450,37,1,0,0,0,451,453,5,57,0,0,452,454,5,86,0, + 0,453,452,1,0,0,0,453,454,1,0,0,0,454,455,1,0,0,0,455,456,5,90,0,0,456, + 457,3,130,65,0,457,458,5,25,0,0,458,459,3,130,65,0,459,460,5,139,0,0,460, + 465,3,42,21,0,461,462,5,141,0,0,462,464,3,42,21,0,463,461,1,0,0,0,464, + 467,1,0,0,0,465,463,1,0,0,0,465,466,1,0,0,0,466,468,1,0,0,0,467,465,1, + 0,0,0,468,471,5,140,0,0,469,470,5,92,0,0,470,472,3,44,22,0,471,469,1,0, + 0,0,471,472,1,0,0,0,472,39,1,0,0,0,473,474,5,68,0,0,474,475,5,58,0,0,475, + 489,3,130,65,0,476,477,5,68,0,0,477,478,5,90,0,0,478,479,3,130,65,0,479, + 480,5,25,0,0,480,481,3,130,65,0,481,489,1,0,0,0,482,483,5,68,0,0,483,484, + 5,99,0,0,484,489,3,130,65,0,485,486,5,68,0,0,486,487,5,98,0,0,487,489, + 3,130,65,0,488,473,1,0,0,0,488,476,1,0,0,0,488,482,1,0,0,0,488,485,1,0, + 0,0,489,41,1,0,0,0,490,492,3,130,65,0,491,493,7,1,0,0,492,491,1,0,0,0, + 492,493,1,0,0,0,493,43,1,0,0,0,494,500,5,73,0,0,495,496,5,95,0,0,496,500, + 5,109,0,0,497,498,5,96,0,0,498,500,5,109,0,0,499,494,1,0,0,0,499,495,1, + 0,0,0,499,497,1,0,0,0,500,45,1,0,0,0,501,502,3,130,65,0,502,504,3,50,25, + 0,503,505,3,48,24,0,504,503,1,0,0,0,504,505,1,0,0,0,505,509,1,0,0,0,506, + 508,3,54,27,0,507,506,1,0,0,0,508,511,1,0,0,0,509,507,1,0,0,0,509,510, + 1,0,0,0,510,47,1,0,0,0,511,509,1,0,0,0,512,513,5,5,0,0,513,514,5,139,0, + 0,514,515,3,114,57,0,515,516,5,140,0,0,516,49,1,0,0,0,517,520,3,130,65, + 0,518,520,5,88,0,0,519,517,1,0,0,0,519,518,1,0,0,0,520,522,1,0,0,0,521, + 523,3,130,65,0,522,521,1,0,0,0,522,523,1,0,0,0,523,525,1,0,0,0,524,526, + 3,130,65,0,525,524,1,0,0,0,525,526,1,0,0,0,526,535,1,0,0,0,527,528,5,139, + 0,0,528,531,3,52,26,0,529,530,5,141,0,0,530,532,3,52,26,0,531,529,1,0, + 0,0,531,532,1,0,0,0,532,533,1,0,0,0,533,534,5,140,0,0,534,536,1,0,0,0, + 535,527,1,0,0,0,535,536,1,0,0,0,536,51,1,0,0,0,537,539,5,131,0,0,538,537, + 1,0,0,0,538,539,1,0,0,0,539,540,1,0,0,0,540,541,5,148,0,0,541,53,1,0,0, + 0,542,543,5,8,0,0,543,612,5,109,0,0,544,612,5,109,0,0,545,546,5,84,0,0, + 546,612,3,114,57,0,547,548,5,92,0,0,548,612,7,2,0,0,549,550,5,75,0,0,550, + 552,3,130,65,0,551,549,1,0,0,0,551,552,1,0,0,0,552,553,1,0,0,0,553,554, + 5,97,0,0,554,555,5,139,0,0,555,556,3,58,29,0,556,557,5,140,0,0,557,612, + 1,0,0,0,558,559,5,75,0,0,559,561,3,130,65,0,560,558,1,0,0,0,560,561,1, + 0,0,0,561,562,1,0,0,0,562,563,5,73,0,0,563,565,5,74,0,0,564,566,3,140, + 70,0,565,564,1,0,0,0,565,566,1,0,0,0,566,612,1,0,0,0,567,568,5,75,0,0, + 568,570,3,130,65,0,569,567,1,0,0,0,569,570,1,0,0,0,570,571,1,0,0,0,571, + 573,5,86,0,0,572,574,3,140,70,0,573,572,1,0,0,0,573,574,1,0,0,0,574,612, + 1,0,0,0,575,576,5,75,0,0,576,578,3,130,65,0,577,575,1,0,0,0,577,578,1, + 0,0,0,578,579,1,0,0,0,579,580,5,77,0,0,580,592,3,130,65,0,581,582,5,139, + 0,0,582,587,3,130,65,0,583,584,5,141,0,0,584,586,3,130,65,0,585,583,1, + 0,0,0,586,589,1,0,0,0,587,585,1,0,0,0,587,588,1,0,0,0,588,590,1,0,0,0, + 589,587,1,0,0,0,590,591,5,140,0,0,591,593,1,0,0,0,592,581,1,0,0,0,592, + 593,1,0,0,0,593,597,1,0,0,0,594,596,3,60,30,0,595,594,1,0,0,0,596,599, + 1,0,0,0,597,595,1,0,0,0,597,598,1,0,0,0,598,612,1,0,0,0,599,597,1,0,0, + 0,600,609,5,88,0,0,601,602,5,139,0,0,602,605,3,52,26,0,603,604,5,141,0, + 0,604,606,3,52,26,0,605,603,1,0,0,0,605,606,1,0,0,0,606,607,1,0,0,0,607, + 608,5,140,0,0,608,610,1,0,0,0,609,601,1,0,0,0,609,610,1,0,0,0,610,612, + 1,0,0,0,611,542,1,0,0,0,611,544,1,0,0,0,611,545,1,0,0,0,611,547,1,0,0, + 0,611,551,1,0,0,0,611,560,1,0,0,0,611,569,1,0,0,0,611,577,1,0,0,0,611, + 600,1,0,0,0,612,55,1,0,0,0,613,614,5,75,0,0,614,616,3,130,65,0,615,613, + 1,0,0,0,615,616,1,0,0,0,616,617,1,0,0,0,617,618,5,73,0,0,618,620,5,74, + 0,0,619,621,3,140,70,0,620,619,1,0,0,0,620,621,1,0,0,0,621,622,1,0,0,0, + 622,623,5,139,0,0,623,628,3,130,65,0,624,625,5,141,0,0,625,627,3,130,65, + 0,626,624,1,0,0,0,627,630,1,0,0,0,628,626,1,0,0,0,628,629,1,0,0,0,629, + 631,1,0,0,0,630,628,1,0,0,0,631,632,5,140,0,0,632,703,1,0,0,0,633,634, + 5,75,0,0,634,636,3,130,65,0,635,633,1,0,0,0,635,636,1,0,0,0,636,637,1, + 0,0,0,637,639,5,86,0,0,638,640,3,140,70,0,639,638,1,0,0,0,639,640,1,0, + 0,0,640,641,1,0,0,0,641,642,5,139,0,0,642,647,3,130,65,0,643,644,5,141, + 0,0,644,646,3,130,65,0,645,643,1,0,0,0,646,649,1,0,0,0,647,645,1,0,0,0, + 647,648,1,0,0,0,648,650,1,0,0,0,649,647,1,0,0,0,650,651,5,140,0,0,651, + 703,1,0,0,0,652,653,5,75,0,0,653,655,3,130,65,0,654,652,1,0,0,0,654,655, + 1,0,0,0,655,656,1,0,0,0,656,657,5,76,0,0,657,660,5,74,0,0,658,659,5,85, + 0,0,659,661,5,90,0,0,660,658,1,0,0,0,660,661,1,0,0,0,661,662,1,0,0,0,662, + 663,5,139,0,0,663,668,3,130,65,0,664,665,5,141,0,0,665,667,3,130,65,0, + 666,664,1,0,0,0,667,670,1,0,0,0,668,666,1,0,0,0,668,669,1,0,0,0,669,671, + 1,0,0,0,670,668,1,0,0,0,671,672,5,140,0,0,672,673,5,77,0,0,673,685,3,130, + 65,0,674,675,5,139,0,0,675,680,3,130,65,0,676,677,5,141,0,0,677,679,3, + 130,65,0,678,676,1,0,0,0,679,682,1,0,0,0,680,678,1,0,0,0,680,681,1,0,0, + 0,681,683,1,0,0,0,682,680,1,0,0,0,683,684,5,140,0,0,684,686,1,0,0,0,685, + 674,1,0,0,0,685,686,1,0,0,0,686,690,1,0,0,0,687,689,3,60,30,0,688,687, + 1,0,0,0,689,692,1,0,0,0,690,688,1,0,0,0,690,691,1,0,0,0,691,703,1,0,0, + 0,692,690,1,0,0,0,693,694,5,75,0,0,694,696,3,130,65,0,695,693,1,0,0,0, + 695,696,1,0,0,0,696,697,1,0,0,0,697,698,5,97,0,0,698,699,5,139,0,0,699, + 700,3,58,29,0,700,701,5,140,0,0,701,703,1,0,0,0,702,615,1,0,0,0,702,635, + 1,0,0,0,702,654,1,0,0,0,702,695,1,0,0,0,703,57,1,0,0,0,704,710,8,3,0,0, + 705,706,5,139,0,0,706,707,3,58,29,0,707,708,5,140,0,0,708,710,1,0,0,0, + 709,704,1,0,0,0,709,705,1,0,0,0,710,713,1,0,0,0,711,709,1,0,0,0,711,712, + 1,0,0,0,712,59,1,0,0,0,713,711,1,0,0,0,714,715,5,25,0,0,715,716,5,79,0, + 0,716,721,3,62,31,0,717,718,5,25,0,0,718,719,5,78,0,0,719,721,3,62,31, + 0,720,714,1,0,0,0,720,717,1,0,0,0,721,61,1,0,0,0,722,731,5,80,0,0,723, + 724,5,85,0,0,724,731,5,82,0,0,725,731,5,81,0,0,726,727,5,83,0,0,727,731, + 5,109,0,0,728,729,5,83,0,0,729,731,5,84,0,0,730,722,1,0,0,0,730,723,1, + 0,0,0,730,725,1,0,0,0,730,726,1,0,0,0,730,728,1,0,0,0,731,63,1,0,0,0,732, + 733,5,70,0,0,733,734,5,71,0,0,734,751,3,130,65,0,735,736,5,139,0,0,736, + 741,3,130,65,0,737,738,5,141,0,0,738,740,3,130,65,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,744,1,0,0,0,743,741,1, + 0,0,0,744,745,5,140,0,0,745,747,1,0,0,0,746,735,1,0,0,0,746,747,1,0,0, + 0,747,748,1,0,0,0,748,752,3,70,35,0,749,750,5,84,0,0,750,752,5,72,0,0, + 751,746,1,0,0,0,751,749,1,0,0,0,752,65,1,0,0,0,753,754,5,139,0,0,754,759, + 3,68,34,0,755,756,5,141,0,0,756,758,3,68,34,0,757,755,1,0,0,0,758,761, + 1,0,0,0,759,757,1,0,0,0,759,760,1,0,0,0,760,762,1,0,0,0,761,759,1,0,0, + 0,762,763,5,140,0,0,763,67,1,0,0,0,764,767,5,84,0,0,765,767,3,114,57,0, + 766,764,1,0,0,0,766,765,1,0,0,0,767,69,1,0,0,0,768,774,3,74,37,0,769,770, + 3,76,38,0,770,771,3,74,37,0,771,773,1,0,0,0,772,769,1,0,0,0,773,776,1, + 0,0,0,774,772,1,0,0,0,774,775,1,0,0,0,775,778,1,0,0,0,776,774,1,0,0,0, + 777,779,3,110,55,0,778,777,1,0,0,0,778,779,1,0,0,0,779,781,1,0,0,0,780, + 782,3,90,45,0,781,780,1,0,0,0,781,782,1,0,0,0,782,784,1,0,0,0,783,785, + 3,72,36,0,784,783,1,0,0,0,784,785,1,0,0,0,785,71,1,0,0,0,786,787,5,92, + 0,0,787,788,5,124,0,0,788,789,5,125,0,0,789,73,1,0,0,0,790,805,3,78,39, + 0,791,792,5,139,0,0,792,793,3,70,35,0,793,794,5,140,0,0,794,805,1,0,0, + 0,795,796,5,72,0,0,796,801,3,66,33,0,797,798,5,141,0,0,798,800,3,66,33, + 0,799,797,1,0,0,0,800,803,1,0,0,0,801,799,1,0,0,0,801,802,1,0,0,0,802, + 805,1,0,0,0,803,801,1,0,0,0,804,790,1,0,0,0,804,791,1,0,0,0,804,795,1, + 0,0,0,805,75,1,0,0,0,806,808,5,53,0,0,807,809,5,54,0,0,808,807,1,0,0,0, + 808,809,1,0,0,0,809,813,1,0,0,0,810,813,5,55,0,0,811,813,5,56,0,0,812, + 806,1,0,0,0,812,810,1,0,0,0,812,811,1,0,0,0,813,77,1,0,0,0,814,816,5,1, + 0,0,815,817,3,80,40,0,816,815,1,0,0,0,816,817,1,0,0,0,817,819,1,0,0,0, + 818,820,3,86,43,0,819,818,1,0,0,0,819,820,1,0,0,0,820,821,1,0,0,0,821, + 824,3,94,47,0,822,823,5,71,0,0,823,825,3,130,65,0,824,822,1,0,0,0,824, + 825,1,0,0,0,825,827,1,0,0,0,826,828,3,98,49,0,827,826,1,0,0,0,827,828, + 1,0,0,0,828,830,1,0,0,0,829,831,3,108,54,0,830,829,1,0,0,0,830,831,1,0, + 0,0,831,833,1,0,0,0,832,834,3,82,41,0,833,832,1,0,0,0,833,834,1,0,0,0, + 834,836,1,0,0,0,835,837,3,84,42,0,836,835,1,0,0,0,836,837,1,0,0,0,837, + 839,1,0,0,0,838,840,3,72,36,0,839,838,1,0,0,0,839,840,1,0,0,0,840,79,1, + 0,0,0,841,842,7,4,0,0,842,81,1,0,0,0,843,844,5,27,0,0,844,845,5,29,0,0, + 845,850,3,114,57,0,846,847,5,141,0,0,847,849,3,114,57,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,83,1,0,0,0,852,850, + 1,0,0,0,853,854,5,30,0,0,854,855,3,114,57,0,855,85,1,0,0,0,856,857,5,4, + 0,0,857,862,3,88,44,0,858,859,7,5,0,0,859,861,3,88,44,0,860,858,1,0,0, + 0,861,864,1,0,0,0,862,860,1,0,0,0,862,863,1,0,0,0,863,866,1,0,0,0,864, + 862,1,0,0,0,865,867,5,36,0,0,866,865,1,0,0,0,866,867,1,0,0,0,867,870,1, + 0,0,0,868,869,5,92,0,0,869,871,5,117,0,0,870,868,1,0,0,0,870,871,1,0,0, + 0,871,87,1,0,0,0,872,879,5,148,0,0,873,879,5,146,0,0,874,875,5,139,0,0, + 875,876,3,114,57,0,876,877,5,140,0,0,877,879,1,0,0,0,878,872,1,0,0,0,878, + 873,1,0,0,0,878,874,1,0,0,0,879,89,1,0,0,0,880,881,5,45,0,0,881,882,3, + 114,57,0,882,892,3,92,46,0,883,884,5,46,0,0,884,885,7,6,0,0,885,886,3, + 114,57,0,886,890,3,92,46,0,887,891,5,51,0,0,888,889,5,92,0,0,889,891,5, + 117,0,0,890,887,1,0,0,0,890,888,1,0,0,0,891,893,1,0,0,0,892,883,1,0,0, + 0,892,893,1,0,0,0,893,904,1,0,0,0,894,895,5,46,0,0,895,896,7,6,0,0,896, + 897,3,114,57,0,897,901,3,92,46,0,898,902,5,51,0,0,899,900,5,92,0,0,900, + 902,5,117,0,0,901,898,1,0,0,0,901,899,1,0,0,0,902,904,1,0,0,0,903,880, + 1,0,0,0,903,894,1,0,0,0,904,91,1,0,0,0,905,906,7,7,0,0,906,93,1,0,0,0, + 907,917,5,126,0,0,908,913,3,96,48,0,909,910,5,141,0,0,910,912,3,96,48, + 0,911,909,1,0,0,0,912,915,1,0,0,0,913,911,1,0,0,0,913,914,1,0,0,0,914, + 917,1,0,0,0,915,913,1,0,0,0,916,907,1,0,0,0,916,908,1,0,0,0,917,95,1,0, + 0,0,918,919,3,130,65,0,919,920,5,142,0,0,920,921,5,126,0,0,921,930,1,0, + 0,0,922,927,3,114,57,0,923,925,5,5,0,0,924,923,1,0,0,0,924,925,1,0,0,0, + 925,926,1,0,0,0,926,928,3,130,65,0,927,924,1,0,0,0,927,928,1,0,0,0,928, + 930,1,0,0,0,929,918,1,0,0,0,929,922,1,0,0,0,930,97,1,0,0,0,931,932,5,2, + 0,0,932,937,3,100,50,0,933,934,5,141,0,0,934,936,3,100,50,0,935,933,1, + 0,0,0,936,939,1,0,0,0,937,935,1,0,0,0,937,938,1,0,0,0,938,99,1,0,0,0,939, + 937,1,0,0,0,940,944,3,102,51,0,941,943,3,104,52,0,942,941,1,0,0,0,943, + 946,1,0,0,0,944,942,1,0,0,0,944,945,1,0,0,0,945,101,1,0,0,0,946,944,1, + 0,0,0,947,952,3,130,65,0,948,950,5,5,0,0,949,948,1,0,0,0,949,950,1,0,0, + 0,950,951,1,0,0,0,951,953,3,130,65,0,952,949,1,0,0,0,952,953,1,0,0,0,953, + 971,1,0,0,0,954,955,5,139,0,0,955,956,3,70,35,0,956,964,5,140,0,0,957, + 959,5,5,0,0,958,957,1,0,0,0,958,959,1,0,0,0,959,960,1,0,0,0,960,962,3, + 130,65,0,961,963,3,160,80,0,962,961,1,0,0,0,962,963,1,0,0,0,963,965,1, + 0,0,0,964,958,1,0,0,0,964,965,1,0,0,0,965,971,1,0,0,0,966,967,5,139,0, + 0,967,968,3,100,50,0,968,969,5,140,0,0,969,971,1,0,0,0,970,947,1,0,0,0, + 970,954,1,0,0,0,970,966,1,0,0,0,971,103,1,0,0,0,972,973,3,106,53,0,973, + 974,5,23,0,0,974,975,3,102,51,0,975,976,5,25,0,0,976,977,3,114,57,0,977, + 988,1,0,0,0,978,979,5,37,0,0,979,980,5,23,0,0,980,988,3,102,51,0,981,982, + 5,37,0,0,982,983,5,38,0,0,983,988,3,102,51,0,984,985,5,22,0,0,985,986, + 5,38,0,0,986,988,3,102,51,0,987,972,1,0,0,0,987,978,1,0,0,0,987,981,1, + 0,0,0,987,984,1,0,0,0,988,105,1,0,0,0,989,991,5,18,0,0,990,989,1,0,0,0, + 990,991,1,0,0,0,991,1005,1,0,0,0,992,994,5,19,0,0,993,995,5,22,0,0,994, + 993,1,0,0,0,994,995,1,0,0,0,995,1005,1,0,0,0,996,998,5,20,0,0,997,999, + 5,22,0,0,998,997,1,0,0,0,998,999,1,0,0,0,999,1005,1,0,0,0,1000,1002,5, + 21,0,0,1001,1003,5,22,0,0,1002,1001,1,0,0,0,1002,1003,1,0,0,0,1003,1005, + 1,0,0,0,1004,990,1,0,0,0,1004,992,1,0,0,0,1004,996,1,0,0,0,1004,1000,1, + 0,0,0,1005,107,1,0,0,0,1006,1007,5,3,0,0,1007,1008,3,114,57,0,1008,109, + 1,0,0,0,1009,1010,5,26,0,0,1010,1011,5,29,0,0,1011,1016,3,112,56,0,1012, + 1013,5,141,0,0,1013,1015,3,112,56,0,1014,1012,1,0,0,0,1015,1018,1,0,0, + 0,1016,1014,1,0,0,0,1016,1017,1,0,0,0,1017,111,1,0,0,0,1018,1016,1,0,0, + 0,1019,1021,3,114,57,0,1020,1022,7,1,0,0,1021,1020,1,0,0,0,1021,1022,1, + 0,0,0,1022,113,1,0,0,0,1023,1024,6,57,-1,0,1024,1025,7,5,0,0,1025,1030, + 3,114,57,21,1026,1027,7,8,0,0,1027,1030,3,114,57,7,1028,1030,3,116,58, + 0,1029,1023,1,0,0,0,1029,1026,1,0,0,0,1029,1028,1,0,0,0,1030,1129,1,0, + 0,0,1031,1032,10,22,0,0,1032,1033,5,129,0,0,1033,1128,3,114,57,23,1034, + 1035,10,20,0,0,1035,1036,7,9,0,0,1036,1128,3,114,57,21,1037,1038,10,19, + 0,0,1038,1039,5,128,0,0,1039,1128,3,114,57,20,1040,1041,10,18,0,0,1041, + 1042,5,17,0,0,1042,1128,3,114,57,19,1043,1044,10,17,0,0,1044,1045,7,5, + 0,0,1045,1128,3,114,57,18,1046,1047,10,16,0,0,1047,1048,5,132,0,0,1048, + 1128,3,114,57,17,1049,1050,10,15,0,0,1050,1051,7,10,0,0,1051,1128,3,114, + 57,16,1052,1054,10,14,0,0,1053,1055,5,8,0,0,1054,1053,1,0,0,0,1054,1055, + 1,0,0,0,1055,1056,1,0,0,0,1056,1057,5,52,0,0,1057,1058,3,114,57,0,1058, + 1059,5,6,0,0,1059,1060,3,114,57,15,1060,1128,1,0,0,0,1061,1063,10,13,0, + 0,1062,1064,5,8,0,0,1063,1062,1,0,0,0,1063,1064,1,0,0,0,1064,1065,1,0, + 0,0,1065,1066,5,16,0,0,1066,1128,3,114,57,14,1067,1068,10,8,0,0,1068,1070, + 5,28,0,0,1069,1071,5,8,0,0,1070,1069,1,0,0,0,1070,1071,1,0,0,0,1071,1072, + 1,0,0,0,1072,1073,5,35,0,0,1073,1074,5,2,0,0,1074,1128,3,114,57,9,1075, + 1076,10,6,0,0,1076,1077,7,11,0,0,1077,1128,3,114,57,7,1078,1079,10,5,0, + 0,1079,1080,7,12,0,0,1080,1128,3,114,57,6,1081,1082,10,4,0,0,1082,1083, + 7,13,0,0,1083,1128,3,114,57,5,1084,1085,10,3,0,0,1085,1086,5,10,0,0,1086, + 1128,3,114,57,4,1087,1088,10,2,0,0,1088,1089,5,11,0,0,1089,1128,3,114, + 57,3,1090,1092,10,12,0,0,1091,1093,5,8,0,0,1092,1091,1,0,0,0,1092,1093, + 1,0,0,0,1093,1094,1,0,0,0,1094,1095,5,24,0,0,1095,1096,5,139,0,0,1096, + 1097,3,70,35,0,1097,1098,5,140,0,0,1098,1128,1,0,0,0,1099,1101,10,11,0, + 0,1100,1102,5,8,0,0,1101,1100,1,0,0,0,1101,1102,1,0,0,0,1102,1103,1,0, + 0,0,1103,1104,5,24,0,0,1104,1105,5,139,0,0,1105,1110,3,114,57,0,1106,1107, + 5,141,0,0,1107,1109,3,114,57,0,1108,1106,1,0,0,0,1109,1112,1,0,0,0,1110, + 1108,1,0,0,0,1110,1111,1,0,0,0,1111,1113,1,0,0,0,1112,1110,1,0,0,0,1113, + 1114,5,140,0,0,1114,1128,1,0,0,0,1115,1116,10,10,0,0,1116,1118,5,28,0, + 0,1117,1119,5,8,0,0,1118,1117,1,0,0,0,1118,1119,1,0,0,0,1119,1120,1,0, + 0,0,1120,1128,5,109,0,0,1121,1122,10,9,0,0,1122,1124,5,28,0,0,1123,1125, + 5,8,0,0,1124,1123,1,0,0,0,1124,1125,1,0,0,0,1125,1126,1,0,0,0,1126,1128, + 7,14,0,0,1127,1031,1,0,0,0,1127,1034,1,0,0,0,1127,1037,1,0,0,0,1127,1040, + 1,0,0,0,1127,1043,1,0,0,0,1127,1046,1,0,0,0,1127,1049,1,0,0,0,1127,1052, + 1,0,0,0,1127,1061,1,0,0,0,1127,1067,1,0,0,0,1127,1075,1,0,0,0,1127,1078, + 1,0,0,0,1127,1081,1,0,0,0,1127,1084,1,0,0,0,1127,1087,1,0,0,0,1127,1090, + 1,0,0,0,1127,1099,1,0,0,0,1127,1115,1,0,0,0,1127,1121,1,0,0,0,1128,1131, + 1,0,0,0,1129,1127,1,0,0,0,1129,1130,1,0,0,0,1130,115,1,0,0,0,1131,1129, + 1,0,0,0,1132,1152,3,132,66,0,1133,1152,3,118,59,0,1134,1152,3,122,61,0, + 1135,1152,3,126,63,0,1136,1152,5,146,0,0,1137,1152,5,145,0,0,1138,1139, + 5,31,0,0,1139,1140,5,139,0,0,1140,1141,3,70,35,0,1141,1142,5,140,0,0,1142, + 1152,1,0,0,0,1143,1144,5,139,0,0,1144,1145,3,70,35,0,1145,1146,5,140,0, + 0,1146,1152,1,0,0,0,1147,1148,5,139,0,0,1148,1149,3,114,57,0,1149,1150, + 5,140,0,0,1150,1152,1,0,0,0,1151,1132,1,0,0,0,1151,1133,1,0,0,0,1151,1134, + 1,0,0,0,1151,1135,1,0,0,0,1151,1136,1,0,0,0,1151,1137,1,0,0,0,1151,1138, + 1,0,0,0,1151,1143,1,0,0,0,1151,1147,1,0,0,0,1152,117,1,0,0,0,1153,1155, + 5,41,0,0,1154,1156,3,114,57,0,1155,1154,1,0,0,0,1155,1156,1,0,0,0,1156, + 1158,1,0,0,0,1157,1159,3,120,60,0,1158,1157,1,0,0,0,1159,1160,1,0,0,0, + 1160,1158,1,0,0,0,1160,1161,1,0,0,0,1161,1164,1,0,0,0,1162,1163,5,43,0, + 0,1163,1165,3,114,57,0,1164,1162,1,0,0,0,1164,1165,1,0,0,0,1165,1166,1, + 0,0,0,1166,1167,5,44,0,0,1167,119,1,0,0,0,1168,1169,5,42,0,0,1169,1170, + 3,114,57,0,1170,1171,5,33,0,0,1171,1172,3,114,57,0,1172,121,1,0,0,0,1173, + 1174,3,124,62,0,1174,1187,5,139,0,0,1175,1188,5,126,0,0,1176,1178,5,35, + 0,0,1177,1176,1,0,0,0,1177,1178,1,0,0,0,1178,1179,1,0,0,0,1179,1184,3, + 114,57,0,1180,1181,5,141,0,0,1181,1183,3,114,57,0,1182,1180,1,0,0,0,1183, + 1186,1,0,0,0,1184,1182,1,0,0,0,1184,1185,1,0,0,0,1185,1188,1,0,0,0,1186, + 1184,1,0,0,0,1187,1175,1,0,0,0,1187,1177,1,0,0,0,1187,1188,1,0,0,0,1188, + 1189,1,0,0,0,1189,1191,5,140,0,0,1190,1192,3,154,77,0,1191,1190,1,0,0, + 0,1191,1192,1,0,0,0,1192,1194,1,0,0,0,1193,1195,3,152,76,0,1194,1193,1, + 0,0,0,1194,1195,1,0,0,0,1195,1204,1,0,0,0,1196,1198,3,156,78,0,1197,1196, + 1,0,0,0,1197,1198,1,0,0,0,1198,1200,1,0,0,0,1199,1201,3,158,79,0,1200, + 1199,1,0,0,0,1200,1201,1,0,0,0,1201,1202,1,0,0,0,1202,1203,5,39,0,0,1203, + 1205,3,138,69,0,1204,1197,1,0,0,0,1204,1205,1,0,0,0,1205,123,1,0,0,0,1206, + 1213,3,130,65,0,1207,1213,5,19,0,0,1208,1213,5,20,0,0,1209,1213,5,103, + 0,0,1210,1213,5,48,0,0,1211,1213,5,40,0,0,1212,1206,1,0,0,0,1212,1207, + 1,0,0,0,1212,1208,1,0,0,0,1212,1209,1,0,0,0,1212,1210,1,0,0,0,1212,1211, + 1,0,0,0,1213,125,1,0,0,0,1214,1219,3,130,65,0,1215,1216,7,15,0,0,1216, + 1218,3,128,64,0,1217,1215,1,0,0,0,1218,1221,1,0,0,0,1219,1217,1,0,0,0, + 1219,1220,1,0,0,0,1220,127,1,0,0,0,1221,1219,1,0,0,0,1222,1225,3,130,65, + 0,1223,1225,3,150,75,0,1224,1222,1,0,0,0,1224,1223,1,0,0,0,1225,129,1, + 0,0,0,1226,1231,5,155,0,0,1227,1231,5,153,0,0,1228,1231,5,154,0,0,1229, + 1231,3,148,74,0,1230,1226,1,0,0,0,1230,1227,1,0,0,0,1230,1228,1,0,0,0, + 1230,1229,1,0,0,0,1231,131,1,0,0,0,1232,1242,5,148,0,0,1233,1242,5,149, + 0,0,1234,1242,5,147,0,0,1235,1242,5,150,0,0,1236,1242,5,151,0,0,1237,1242, + 5,152,0,0,1238,1242,7,16,0,0,1239,1242,7,17,0,0,1240,1242,5,109,0,0,1241, + 1232,1,0,0,0,1241,1233,1,0,0,0,1241,1234,1,0,0,0,1241,1235,1,0,0,0,1241, + 1236,1,0,0,0,1241,1237,1,0,0,0,1241,1238,1,0,0,0,1241,1239,1,0,0,0,1241, + 1240,1,0,0,0,1242,133,1,0,0,0,1243,1245,5,59,0,0,1244,1246,7,18,0,0,1245, + 1244,1,0,0,0,1245,1246,1,0,0,0,1246,1256,1,0,0,0,1247,1249,5,60,0,0,1248, + 1250,7,18,0,0,1249,1248,1,0,0,0,1249,1250,1,0,0,0,1250,1256,1,0,0,0,1251, + 1253,5,61,0,0,1252,1254,7,18,0,0,1253,1252,1,0,0,0,1253,1254,1,0,0,0,1254, + 1256,1,0,0,0,1255,1243,1,0,0,0,1255,1247,1,0,0,0,1255,1251,1,0,0,0,1256, + 135,1,0,0,0,1257,1258,3,114,57,0,1258,1259,5,0,0,1,1259,137,1,0,0,0,1260, + 1271,5,139,0,0,1261,1262,5,40,0,0,1262,1263,5,29,0,0,1263,1268,3,114,57, + 0,1264,1265,5,141,0,0,1265,1267,3,114,57,0,1266,1264,1,0,0,0,1267,1270, + 1,0,0,0,1268,1266,1,0,0,0,1268,1269,1,0,0,0,1269,1272,1,0,0,0,1270,1268, + 1,0,0,0,1271,1261,1,0,0,0,1271,1272,1,0,0,0,1272,1274,1,0,0,0,1273,1275, + 3,110,55,0,1274,1273,1,0,0,0,1274,1275,1,0,0,0,1275,1277,1,0,0,0,1276, + 1278,3,142,71,0,1277,1276,1,0,0,0,1277,1278,1,0,0,0,1278,1279,1,0,0,0, + 1279,1280,5,140,0,0,1280,139,1,0,0,0,1281,1282,7,19,0,0,1282,141,1,0,0, + 0,1283,1290,7,20,0,0,1284,1285,5,52,0,0,1285,1286,3,144,72,0,1286,1287, + 5,6,0,0,1287,1288,3,144,72,0,1288,1291,1,0,0,0,1289,1291,3,144,72,0,1290, + 1284,1,0,0,0,1290,1289,1,0,0,0,1291,1294,1,0,0,0,1292,1293,5,116,0,0,1293, + 1295,3,146,73,0,1294,1292,1,0,0,0,1294,1295,1,0,0,0,1295,143,1,0,0,0,1296, + 1297,5,112,0,0,1297,1304,7,21,0,0,1298,1299,5,115,0,0,1299,1304,5,50,0, + 0,1300,1301,3,114,57,0,1301,1302,7,21,0,0,1302,1304,1,0,0,0,1303,1296, + 1,0,0,0,1303,1298,1,0,0,0,1303,1300,1,0,0,0,1304,145,1,0,0,0,1305,1306, + 5,115,0,0,1306,1312,5,50,0,0,1307,1312,5,27,0,0,1308,1312,5,117,0,0,1309, + 1310,5,85,0,0,1310,1312,5,118,0,0,1311,1305,1,0,0,0,1311,1307,1,0,0,0, + 1311,1308,1,0,0,0,1311,1309,1,0,0,0,1312,147,1,0,0,0,1313,1314,7,22,0, + 0,1314,149,1,0,0,0,1315,1316,7,23,0,0,1316,151,1,0,0,0,1317,1318,5,123, + 0,0,1318,1319,5,139,0,0,1319,1320,5,3,0,0,1320,1321,3,114,57,0,1321,1322, + 5,140,0,0,1322,153,1,0,0,0,1323,1324,5,119,0,0,1324,1325,5,27,0,0,1325, + 1326,5,139,0,0,1326,1327,3,110,55,0,1327,1328,5,140,0,0,1328,155,1,0,0, + 0,1329,1330,5,2,0,0,1330,1331,7,24,0,0,1331,157,1,0,0,0,1332,1333,7,25, + 0,0,1333,1334,5,122,0,0,1334,159,1,0,0,0,1335,1336,5,139,0,0,1336,1341, + 3,130,65,0,1337,1338,5,141,0,0,1338,1340,3,130,65,0,1339,1337,1,0,0,0, + 1340,1343,1,0,0,0,1341,1339,1,0,0,0,1341,1342,1,0,0,0,1342,1344,1,0,0, + 0,1343,1341,1,0,0,0,1344,1345,5,140,0,0,1345,161,1,0,0,0,172,163,179,182, + 188,208,217,220,228,237,241,244,256,264,268,271,279,284,287,295,302,312, + 319,333,338,347,358,368,371,378,387,394,401,406,415,442,449,453,465,471, + 488,492,499,504,509,519,522,525,531,535,538,551,560,565,569,573,577,587, + 592,597,605,609,611,615,620,628,635,639,647,654,660,668,680,685,690,695, + 702,709,711,720,730,741,746,751,759,766,774,778,781,784,801,804,808,812, + 816,819,824,827,830,833,836,839,850,862,866,870,878,890,892,901,903,913, + 916,924,927,929,937,944,949,952,958,962,964,970,987,990,994,998,1002,1004, + 1016,1021,1029,1054,1063,1070,1092,1101,1110,1118,1124,1127,1129,1151, + 1155,1160,1164,1177,1184,1187,1191,1194,1197,1200,1204,1212,1219,1224, + 1230,1241,1245,1249,1253,1255,1268,1271,1274,1277,1290,1294,1303,1311, + 1341 }; 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 550b614c..25fa5cf5 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs @@ -466,6 +466,12 @@ public interface IAccessSqlVisitor : IParseTreeVisitor { /// The visitor result. Result VisitQueryExpression([NotNull] AccessSqlParser.QueryExpressionContext context); /// + /// Visit a parse tree produced by . + /// + /// The parse tree. + /// The visitor result. + Result VisitOwnerAccessOption([NotNull] AccessSqlParser.OwnerAccessOptionContext context); + /// /// Visit a parse tree produced by the SelectTerm /// labeled alternative in . /// diff --git a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs index 2e02c9b0..c33edbbd 100644 --- a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs +++ b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs @@ -453,7 +453,7 @@ private static SqlStatement BuildCreateProcedure(CreateProcedureStatementContext if (MakeTableTarget(query) is { } target) return new CreateActionProcedureStatement( name, ProcedureActionKind.MakeTable, null, target, null, - BuildViewDefinition(query), null, parameters); + BuildViewDefinition(query), null, parameters, OwnerAccess(query)); ViewDefinition definition = BuildViewDefinition(query); return new CreateProcedureStatement(name, parameters, definition, OriginalText(query)); @@ -487,7 +487,8 @@ private static CreateActionProcedureStatement BuildAppendProcedure( .Select((col, i) => new AppendColumn(Identifier(col), body.Columns[i].Expression)) .ToList(); return new CreateActionProcedureStatement( - name, ProcedureActionKind.Append, null, Identifier(insert.table), sourced, body, null, parameters); + name, ProcedureActionKind.Append, null, Identifier(insert.table), sourced, body, null, parameters, + OwnerAccess(source)); } // A stored append query keeps its columns and values as text pairs, which has room for exactly one @@ -514,7 +515,7 @@ private static CreateActionProcedureStatement BuildAppendProcedure( .ToList(); return new CreateActionProcedureStatement( name, ProcedureActionKind.Append, null, Identifier(insert.table), appendColumns, - null, null, parameters); + null, null, parameters, insert.source is { } query && OwnerAccess(query)); } /// An UPDATE body: its sources and WHERE are stored exactly as a view's are, and each SET @@ -528,7 +529,8 @@ private static CreateActionProcedureStatement BuildUpdateProcedure( .ToList(); return new CreateActionProcedureStatement( name, ProcedureActionKind.Update, null, null, assignments, - ActionBody(update.tableSource(), update.whereClause()), null, parameters); + ActionBody(update.tableSource(), update.whereClause()), null, parameters, + update.ownerAccessOption() is not null); } /// A DELETE body: its sources and WHERE, plus the table.* target when the statement names @@ -539,7 +541,8 @@ private static CreateActionProcedureStatement BuildDeleteProcedure( string? target = delete.target is { } t ? $"{Identifier(t)}.*" : null; return new CreateActionProcedureStatement( name, ProcedureActionKind.Delete, null, null, null, - ActionBody(delete.tableSource(), delete.whereClause()), target, parameters); + ActionBody(delete.tableSource(), delete.whereClause()), target, parameters, + delete.ownerAccessOption() is not null); } /// The sources, joins and WHERE of an UPDATE or DELETE body, in the shape a view stores them — @@ -716,7 +719,20 @@ private static ViewDefinition BuildViewDefinition(QueryExpressionContext ctx) string? where = select.whereClause() is { } w ? OriginalText(w.expression()) : null; return new ViewDefinition(select.predicate?.DISTINCT() is not null, columns, tables, joins, where, groupBy, having, orderBy, top, - TopPercent: select.topClause()?.percent is not null); + TopPercent: select.topClause()?.percent is not null, OwnerAccess: OwnerAccess(ctx)); + } + + /// Whether a query declares WITH OWNERACCESS OPTION — after its ORDER BY, or at the end of one of its + /// SELECTs, as ACE takes it either way. ACE reads a query's ORDER BY as part of its last SELECT, so the option + /// ending that SELECT cannot come before the ORDER BY, nor be written again after it (verified); both are + /// refused here. + private static bool OwnerAccess(QueryExpressionContext ctx) + { + if (ctx.queryTerm()[^1] is SelectTermContext last && last.querySpecification().ownerAccessOption() is not null + && (ctx.orderByClause() is not null || ctx.offsetFetchClause() is not null || ctx.ownerAccessOption() is not null)) + throw new SqlParseException("WITH OWNERACCESS OPTION ends a query: it comes after the query's ORDER BY, and once."); + return ctx.ownerAccessOption() is not null + || ctx.queryTerm().Any(t => t is SelectTermContext s && s.querySpecification().ownerAccessOption() is not null); } private static void CollectSources(TableSourceContext ts, List tables, List joins) @@ -857,6 +873,7 @@ private static SqlStatement BuildQueryExpression(QueryExpressionContext ctx) { QueryTermContext[] terms = ctx.queryTerm(); SetOperatorContext[] operators = ctx.setOperator(); + _ = OwnerAccess(ctx); // accepted and acted on by nothing, but refused where ACE refuses it // The ordering and paging of the WHOLE expression — the grammar admits them here and nowhere else, so // there is nothing to disentangle: a leading TOP sits on its operand's own querySpecification, a FETCH diff --git a/src/LibRed/README.md b/src/LibRed/README.md index 9576b66a..b7771ba9 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -88,7 +88,8 @@ Treat the number as of its date — an EF Core version bump moves it. view referenced in `FROM` (or inside an expression subquery) to a derived table — so they run through LibRed too. Reading is not limited to what LibRed writes: the `Attribute=1` operation row is read as the query **kind** (Access writes it on plain SELECTs as well as on action queries), a query with no column - rows is Access's `SELECT *`, and `DISTINCTROW` and `TOP … PERCENT` are decoded from their own option bits. + rows is Access's `SELECT *`, and `DISTINCTROW`, `TOP … PERCENT` and `WITH OWNERACCESS OPTION` are decoded + from their own option bits. Measured over a corpus of 19 real-world databases: 479 of 592 stored queries come back as views, and 337 of those reproduce ACE's own SQL exactly once bracketing and whitespace are normalised. - **Write** — row insert with order-preserving index-key encoding and **full B-tree maintenance** @@ -180,6 +181,8 @@ Treat the number as of its date — an EF Core version bump moves it. (`INSERT INTO … SELECT`, the append query) — `SELECT … INTO` (the make-table query), `UPDATE`, `DELETE`, `EXECUTE`, `IF … THEN`, and `SELECT` with `WHERE`, joins, `GROUP BY`/aggregates, `HAVING`, `ORDER BY`, `TOP [PERCENT]`, `ALL`/`DISTINCT`/`DISTINCTROW`, `UNION`/`INTERSECT`/`EXCEPT`, subqueries, and parameters. + `WITH OWNERACCESS OPTION` is taken wherever ACE takes it and changes nothing — LibRed has no users to act for — + but a stored query keeps it. Access's bang notation: `[Table]![Column]` is `Table.Column`, and an unaliased one is named as written, as ACE names it. A form control such as `[Forms]![frmMenu]![txtCity]` is reachable only as a parameter the query declares; unlike ACE, LibRed does not make an undeclared one an implicit parameter. `Yes`/`On` and diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index 6a6f81fd..d2b73798 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -414,6 +414,14 @@ > A query with none of them has no `0x03` row. A make-table or append query's own SELECT keeps its > `DISTINCT` and `TOP` on the same row (measured: `SELECT DISTINCT … INTO`, `SELECT TOP 5 … INTO`, > `INSERT INTO … SELECT DISTINCT`). LibRed writes the same single row, and reads it back into the statement. + > + > **`WITH OWNERACCESS OPTION` (`0x04`) is stored by a view and by every kind of action query**, on that same row: + > with it, ACE writes exactly the rows it writes without, but for the bit — `DISTINCT` with it is `6`, `TOP` `20`, + > `DISTINCT TOP` `22`, `DISTINCT TOP … PERCENT` `54`, and an update, delete or append query, which has no other + > option, gets a `0x03` row of its own, `Flag 4`, `Order 1`. Measured for plain, `DISTINCT`, `TOP`, + > `DISTINCT TOP` and `DISTINCT TOP … PERCENT` SELECTs and for append (from `VALUES` and from a SELECT), update, + > delete and make-table queries. LibRed writes it so, reads it back to the clause at the end of the rebuilt + > statement, and acts on nothing it says. > **A query with no `0x05` rows at all has no FROM clause.** `SELECT 1 AS n` is a query Access stores (as > a view or a procedure) and stores exactly as any other, minus the table rows: the type row, one `0x06` diff --git a/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs b/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs index 04a53e77..cd2771f5 100644 --- a/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs @@ -178,6 +178,70 @@ public void A_from_less_body_is_stored_as_ace_stores_it(string sql) } } + /// WITH OWNERACCESS OPTION is one more option row in a stored query, which ACE writes for a view and for + /// every kind of action query alike. LibRed acts on nothing it says, but keeps it: the rows are ACE's, ACE runs the + /// query LibRed stored, and LibRed reads the declaration back and runs the query itself. + [Theory] + [InlineData("CREATE VIEW [Q] AS SELECT CompanyName FROM Shippers WITH OWNERACCESS OPTION")] + // ACE's CREATE VIEW takes no ORDER BY ("Only simple SELECT queries are allowed in VIEWS"); a procedure does. + [InlineData("CREATE PROCEDURE [Q] AS SELECT DISTINCT CompanyName FROM Shippers ORDER BY CompanyName WITH OWNERACCESS OPTION")] + [InlineData("CREATE PROCEDURE [Q] AS SELECT TOP 2 CompanyName FROM Shippers ORDER BY CompanyName WITH OWNERACCESS OPTION")] + [InlineData("CREATE PROCEDURE [Q] AS SELECT DISTINCT TOP 2 CompanyName FROM Shippers ORDER BY CompanyName WITH OWNERACCESS OPTION")] + [InlineData("CREATE PROCEDURE [Q] AS SELECT DISTINCT TOP 50 PERCENT CompanyName FROM Shippers ORDER BY CompanyName " + + "WITH OWNERACCESS OPTION")] + [InlineData("CREATE PROCEDURE [Q] AS SELECT CompanyName FROM Shippers WITH OWNERACCESS OPTION")] + [InlineData("CREATE PROCEDURE [Q] AS UPDATE Customers SET ContactTitle = 'Owner' WHERE Country = 'UK' WITH OWNERACCESS OPTION")] + [InlineData("CREATE PROCEDURE [Q] AS DELETE FROM Shippers WHERE ShipperID > 900 WITH OWNERACCESS OPTION")] + [InlineData("CREATE PROCEDURE [Q] AS SELECT ShipperID, CompanyName INTO ShipperCopy FROM Shippers WITH OWNERACCESS OPTION")] + [InlineData("CREATE PROCEDURE [Q] AS INSERT INTO Shippers (CompanyName, Phone) SELECT CompanyName, Phone FROM Customers " + + "WHERE Country = 'UK' WITH OWNERACCESS OPTION")] + [InlineData("CREATE PROCEDURE [Q] AS INSERT INTO Shippers (CompanyName) VALUES ('Owner') WITH OWNERACCESS OPTION")] + public void An_owneraccess_query_is_stored_as_ace_stores_it(string sql) + { + string ourPath = Copy(), acePath = Copy(), readPath = Copy(); + bool action = !sql.Contains(" AS SELECT CompanyName", StringComparison.Ordinal) + && !sql.Contains(" AS SELECT DISTINCT", StringComparison.Ordinal) && !sql.Contains(" AS SELECT TOP", StringComparison.Ordinal); + try + { + using (var db = TemporaryDatabase.OpenTracked(ourPath, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery(sql); + + using (var connection = AceTestDatabase.Open(acePath)) + { + using var create = connection.CreateCommand(); + create.CommandText = sql; + create.ExecuteNonQuery(); + } + + Assert.Equal(QueryRows(acePath, "Q"), QueryRows(ourPath, "Q")); + Assert.Equal(ObjectFlags(acePath, "Q"), ObjectFlags(ourPath, "Q")); + + // ACE runs the query LibRed stored — on a copy, so an action query leaves the file LibRed reads next alone. + File.Copy(ourPath, readPath, overwrite: true); + using (var connection = AceTestDatabase.Open(readPath)) + { + using var run = connection.CreateCommand(); + run.CommandText = "Q"; + run.CommandType = CommandType.StoredProcedure; + run.ExecuteNonQuery(); + } + + // LibRed reads the declaration back with the query, and runs it. + using var ours = TemporaryDatabase.OpenTracked(ourPath, readOnly: false); + string? stored = action ? ours.Catalog.ActionQueries["Q"].Sql : ours.Catalog.Views["Q"]; + Assert.EndsWith(" WITH OWNERACCESS OPTION", stored); + var engine = new QueryEngine(ours); + if (action) engine.ExecuteStoredActionQuery("Q"); + else Assert.NotEmpty(engine.ExecuteQuery("SELECT * FROM [Q]").Rows); + } + finally + { + TemporaryDatabase.Delete(ourPath); + TemporaryDatabase.Delete(acePath); + TemporaryDatabase.Delete(readPath); + } + } + /// A stored SELECT's DISTINCT, TOP and PERCENT are bits of one option row, as ACE writes them — and PERCENT /// survives the round trip: ACE and LibRed return the same rows from the query LibRed stored. [Theory] diff --git a/test/LibRed.Engine.Tests/CreateProcedureTests.cs b/test/LibRed.Engine.Tests/CreateProcedureTests.cs index 9a5cfa85..9f700e1f 100644 --- a/test/LibRed.Engine.Tests/CreateProcedureTests.cs +++ b/test/LibRed.Engine.Tests/CreateProcedureTests.cs @@ -240,6 +240,8 @@ public void Unsupported_procedure_body_is_rejected() "SELECT TOP 10 PERCENT CompanyName INTO [CustomerCopy] FROM [Customers]", 10)] [InlineData("INSERT INTO Shippers (CompanyName) SELECT DISTINCT Country FROM Customers WHERE Country = 'UK'", "INSERT INTO [Shippers] ([CompanyName]) SELECT DISTINCT Country FROM [Customers] WHERE Country = 'UK'", 1)] + [InlineData("UPDATE Customers SET City = 'X' WHERE Country = 'UK' WITH OWNERACCESS OPTION", + "UPDATE [Customers] SET [City] = 'X' WHERE Country = 'UK' WITH OWNERACCESS OPTION", 7)] public void Action_query_body_round_trips_through_the_file(string body, string expected, int affected) { string path = Fresh(); diff --git a/test/LibRed.Engine.Tests/OwnerAccessTests.cs b/test/LibRed.Engine.Tests/OwnerAccessTests.cs new file mode 100644 index 00000000..0adbf9ac --- /dev/null +++ b/test/LibRed.Engine.Tests/OwnerAccessTests.cs @@ -0,0 +1,105 @@ +using System.Globalization; +using LibRed.Sql.Parsing; +using Xunit; + +namespace LibRed.Engine.Tests; + +/// +/// WITH OWNERACCESS OPTION: run a query with its owner's permissions. LibRed has no users, so a statement with it +/// does exactly what it does without it — it is accepted wherever ACE accepts it and refused wherever ACE refuses it +/// (verified). A stored query keeps it; StoredActionQueryWriteAccessTests checks that against ACE. +/// +public class OwnerAccessTests : TempDatabaseTest +{ + private static readonly string[] Setup = + [ + "CREATE TABLE T (Id LONG, N TEXT(10))", + "INSERT INTO T VALUES (1, 'a')", + "INSERT INTO T VALUES (2, 'b')", + "INSERT INTO T VALUES (3, 'b')", + ]; + + private static QueryEngine Fresh() => SharedDatabase.Fresh("owneraccess-", Setup); + + private static string Rows(QueryEngine engine, string sql, IReadOnlyDictionary? parameters = null) => + string.Join(" | ", engine.ExecuteQuery(sql, parameters).Rows.Select(row => + string.Join(",", row.Select(v => Convert.ToString(v, CultureInfo.InvariantCulture))))); + + // Each with the declaration returns what it returns without: it ends a SELECT, a query after its ORDER BY, each + // arm of a UNION and a subquery. + [Theory] + [InlineData("SELECT Id FROM T WITH OWNERACCESS OPTION", "SELECT Id FROM T")] + [InlineData("SELECT Id FROM T WITH OWNERACCESS OPTION;", "SELECT Id FROM T")] + [InlineData("SELECT Id FROM T ORDER BY Id DESC WITH OWNERACCESS OPTION", "SELECT Id FROM T ORDER BY Id DESC")] + [InlineData("SELECT Id FROM T WHERE Id > 1 WITH OWNERACCESS OPTION", "SELECT Id FROM T WHERE Id > 1")] + [InlineData("SELECT N, COUNT(*) FROM T GROUP BY N HAVING COUNT(*) > 1 WITH OWNERACCESS OPTION", + "SELECT N, COUNT(*) FROM T GROUP BY N HAVING COUNT(*) > 1")] + [InlineData("SELECT N FROM T UNION SELECT N FROM T ORDER BY N WITH OWNERACCESS OPTION", + "SELECT N FROM T UNION SELECT N FROM T ORDER BY N")] + [InlineData("SELECT N FROM T WITH OWNERACCESS OPTION UNION SELECT N FROM T ORDER BY N", + "SELECT N FROM T UNION SELECT N FROM T ORDER BY N")] + [InlineData("SELECT N FROM T WITH OWNERACCESS OPTION UNION SELECT N FROM T WITH OWNERACCESS OPTION", + "SELECT N FROM T UNION SELECT N FROM T")] + [InlineData("SELECT N FROM T WITH OWNERACCESS OPTION UNION SELECT N FROM T ORDER BY N WITH OWNERACCESS OPTION", + "SELECT N FROM T UNION SELECT N FROM T ORDER BY N")] + [InlineData("SELECT * FROM (SELECT Id FROM T WITH OWNERACCESS OPTION) WITH OWNERACCESS OPTION", "SELECT * FROM (SELECT Id FROM T)")] + [InlineData("SELECT * FROM (SELECT Id FROM T WITH OWNERACCESS OPTION) ORDER BY Id", "SELECT * FROM (SELECT Id FROM T) ORDER BY Id")] + [InlineData("SELECT Id FROM T WHERE Id IN (SELECT Id FROM T WHERE N = 'b' WITH OWNERACCESS OPTION)", + "SELECT Id FROM T WHERE Id IN (SELECT Id FROM T WHERE N = 'b')")] + [InlineData("SELECT Id FROM T with owneraccess\r\n option", "SELECT Id FROM T")] + public void A_query_with_it_returns_what_it_returns_without(string with, string without) + { + QueryEngine engine = Fresh(); + Assert.Equal(Rows(engine, without), Rows(engine, with)); + } + + [Fact] + public void It_follows_a_parameters_clause() => + Assert.Equal("2 | 3", Rows(Fresh(), "PARAMETERS p Long; SELECT Id FROM T WHERE Id > p ORDER BY Id WITH OWNERACCESS OPTION", + new Dictionary { ["p"] = 1 })); + + // An INSERT, UPDATE or DELETE with it changes the rows it changes without. + [Theory] + [InlineData("INSERT INTO T (Id, N) VALUES (4, 'c') WITH OWNERACCESS OPTION", 1, "1,a | 2,b | 3,b | 4,c")] + [InlineData("INSERT INTO T (Id, N) SELECT Id + 10, N FROM T WHERE N = 'b' WITH OWNERACCESS OPTION", 2, + "1,a | 2,b | 3,b | 12,b | 13,b")] + [InlineData("UPDATE T SET N = 'z' WHERE Id = 2 WITH OWNERACCESS OPTION", 1, "1,a | 2,z | 3,b")] + [InlineData("DELETE FROM T WHERE N = 'b' WITH OWNERACCESS OPTION", 2, "1,a")] + public void A_change_with_it_changes_what_it_changes_without(string sql, int affected, string after) + { + QueryEngine engine = Fresh(); + Assert.Equal(affected, engine.ExecuteNonQuery(sql)); + Assert.Equal(after, Rows(engine, "SELECT Id, N FROM T ORDER BY Id")); + } + + [Fact] + public void A_make_table_query_takes_it() + { + QueryEngine engine = Fresh(); + Assert.Equal(3, engine.ExecuteNonQuery("SELECT Id, N INTO T2 FROM T WITH OWNERACCESS OPTION")); + Assert.Equal("1,a | 2,b | 3,b", Rows(engine, "SELECT Id, N FROM T2 ORDER BY Id")); + } + + // ACE refuses each of these: a query's ORDER BY belongs to its last SELECT, so the option ending that SELECT cannot + // come before it; it ends a query once; it needs all three words; and it is not part of DDL. + [Theory] + [InlineData("SELECT Id FROM T WITH OWNERACCESS OPTION ORDER BY Id")] + [InlineData("SELECT Id FROM T UNION SELECT Id FROM T WITH OWNERACCESS OPTION ORDER BY Id")] + [InlineData("SELECT Id FROM T WITH OWNERACCESS OPTION WITH OWNERACCESS OPTION")] + [InlineData("SELECT Id FROM T WITH OWNERACCESS")] + [InlineData("SELECT Id FROM T WITH OPTION")] + [InlineData("SELECT Id FROM T OWNERACCESS OPTION")] + [InlineData("CREATE TABLE T3 (Id LONG) WITH OWNERACCESS OPTION")] + public void Where_ace_refuses_it_so_does_libred(string sql) => + Assert.Throws(() => Fresh().ExecuteNonQuery(sql)); + + // Neither word is reserved: a column may still be named either, unbracketed. + [Fact] + public void Its_words_still_name_columns() + { + QueryEngine engine = Fresh(); + engine.ExecuteNonQuery("CREATE TABLE W (Option LONG, OwnerAccess LONG)"); + engine.ExecuteNonQuery("INSERT INTO W (Option, OwnerAccess) VALUES (1, 2)"); + Assert.Equal("1,2", Rows(engine, "SELECT Option, OwnerAccess FROM W WITH OWNERACCESS OPTION")); + } +} From 791c3a4ad0a7b55051b1abd973c86a0f29f8685c Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 30 Sep 2026 01:57:49 +0800 Subject: [PATCH 79/88] Count OWNERACCESS and OPTION among the ReservedWords The ReservedWords collection lists every keyword the grammar lexes, reserved or not, and WITH OWNERACCESS OPTION added two. Co-Authored-By: Claude Opus 5.5 --- test/LibRed.Ado.Tests/LibRedSchemaTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/LibRed.Ado.Tests/LibRedSchemaTests.cs b/test/LibRed.Ado.Tests/LibRedSchemaTests.cs index d38d5ef4..149917bb 100644 --- a/test/LibRed.Ado.Tests/LibRedSchemaTests.cs +++ b/test/LibRed.Ado.Tests/LibRedSchemaTests.cs @@ -48,7 +48,7 @@ public void MetaDataCollections_lists_the_framework_collections_first() [InlineData("DataSourceInformation", 1)] [InlineData("DataTypes", 19)] [InlineData("Restrictions", 67)] - [InlineData("ReservedWords", 124)] + [InlineData("ReservedWords", 126)] [InlineData("Tables", 41)] [InlineData("Columns", 228)] // every table's columns and every view's output columns [InlineData("Indexes", 69)] From 642f5d119609bf8017910acb1ca4d6ac9ab229b5 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 30 Sep 2026 02:57:52 +0800 Subject: [PATCH 80/88] Keep the LibRedAccess job's blame output when a step fails LibRed.Core.AccessTests has been ending partway through on CI with an xUnit TestPipelineException, reporting only the tests that finished before it, and it never does locally. The job kept nothing to say which test was running. Its four test steps now pass --blame-crash as well as --blame-hang-timeout, and a failed run uploads their TestResults folders: the Sequence_*.xml naming the test in progress, and any dump. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pull_request.yml | 22 ++++++++++++++++++---- .github/workflows/push.yml | 22 ++++++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5f70f03b..f07d3d71 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -731,21 +731,35 @@ jobs: - name: 'Run Tests: LibRed.Core.AccessTests' if: env.skipTests != 'true' shell: pwsh - run: dotnet test .\test\LibRed.Core.AccessTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-hang-timeout 5m + run: dotnet test .\test\LibRed.Core.AccessTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-crash --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 + run: dotnet test .\test\LibRed.Engine.AccessTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-crash --blame-hang-timeout 5m - name: 'Run Tests: LibRed.Ado.Tests' if: always() && env.skipTests != 'true' shell: pwsh - run: dotnet test .\test\LibRed.Ado.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-hang-timeout 5m + run: dotnet test .\test\LibRed.Ado.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-crash --blame-hang-timeout 5m - name: 'Run Tests: LibRed.EFCore.Tests' if: always() && env.skipTests != 'true' shell: pwsh - run: dotnet test .\test\LibRed.EFCore.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-hang-timeout 5m + run: dotnet test .\test\LibRed.EFCore.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-crash --blame-hang-timeout 5m + # A run that dies partway reports only the tests that finished. The blame output names the test that + # was running (Sequence_*.xml) and holds any crash or hang dump, so keep it when a step fails. + - name: 'Upload Blame Results' + if: failure() + uses: actions/upload-artifact@v6 + with: + name: libredaccess-blame_ace_${{ matrix.aceVersion }} + path: | + test\LibRed.Core.AccessTests\TestResults\** + test\LibRed.Engine.AccessTests\TestResults\** + test\LibRed.Ado.Tests\TestResults\** + test\LibRed.EFCore.Tests\TestResults\** + if-no-files-found: ignore + retention-days: 7 LibRedFunctional: needs: diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index e860207f..467dbf45 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -739,21 +739,35 @@ jobs: - name: 'Run Tests: LibRed.Core.AccessTests' if: env.skipTests != 'true' shell: pwsh - run: dotnet test .\test\LibRed.Core.AccessTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-hang-timeout 5m + run: dotnet test .\test\LibRed.Core.AccessTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-crash --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 + run: dotnet test .\test\LibRed.Engine.AccessTests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-crash --blame-hang-timeout 5m - name: 'Run Tests: LibRed.Ado.Tests' if: always() && env.skipTests != 'true' shell: pwsh - run: dotnet test .\test\LibRed.Ado.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-hang-timeout 5m + run: dotnet test .\test\LibRed.Ado.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-crash --blame-hang-timeout 5m - name: 'Run Tests: LibRed.EFCore.Tests' if: always() && env.skipTests != 'true' shell: pwsh - run: dotnet test .\test\LibRed.EFCore.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-hang-timeout 5m + run: dotnet test .\test\LibRed.EFCore.Tests --configuration '${{ env.buildConfiguration }}' -p:FixedTestOrder=${{ env.deterministicTests }} --blame-crash --blame-hang-timeout 5m + # A run that dies partway reports only the tests that finished. The blame output names the test that + # was running (Sequence_*.xml) and holds any crash or hang dump, so keep it when a step fails. + - name: 'Upload Blame Results' + if: failure() + uses: actions/upload-artifact@v6 + with: + name: libredaccess-blame_ace_${{ matrix.aceVersion }} + path: | + test\LibRed.Core.AccessTests\TestResults\** + test\LibRed.Engine.AccessTests\TestResults\** + test\LibRed.Ado.Tests\TestResults\** + test\LibRed.EFCore.Tests\TestResults\** + if-no-files-found: ignore + retention-days: 7 LibRedFunctional: needs: From e8eb5771676feb5e783cd1a213ea1fe0b0068a58 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 30 Sep 2026 19:29:26 +0800 Subject: [PATCH 81/88] Cast a character above U+7FFF to its unsigned code EF translates a cast from char to an integer type to ASCW, which returns Access's signed 16-bit Integer, so a character from U+8000 up came back negative: cast to uint, U+8000 and U+FFFF read 4294934528 and 4294967295, in both SQL modes. The code is now widened to a Long and masked to 16 bits, (CLNG(ASCW(x)) BAND 65535), which is the character's UTF-16 value. The widening comes first because ACE sign-extends a 16-bit operand of BAND. Co-Authored-By: Claude Opus 5.5 --- .../Sql/Internal/JetQuerySqlGenerator.cs | 7 +-- .../Sql/Internal/LibRedQuerySqlGenerator.cs | 7 +-- .../CharacterConversionTests.cs | 44 +++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 test/LibRed.EFCore.Tests/CharacterConversionTests.cs diff --git a/src/EFCore.Jet.Common/Query/Sql/Internal/JetQuerySqlGenerator.cs b/src/EFCore.Jet.Common/Query/Sql/Internal/JetQuerySqlGenerator.cs index f67a762f..8073b388 100644 --- a/src/EFCore.Jet.Common/Query/Sql/Internal/JetQuerySqlGenerator.cs +++ b/src/EFCore.Jet.Common/Query/Sql/Internal/JetQuerySqlGenerator.cs @@ -925,9 +925,10 @@ static bool IsNullable(SqlExpression? e) => // through leaves a one-character string, which Jet then coerces by parsing it: 1, not 49. if (typeMapping.ClrType.IsInteger() && typeMapping.ClrType != typeof(char) && convertExpression.Operand.Type == typeof(char)) { - Sql.Append("ASCW("); + // Widen ASCW's signed Int16 before masking: ACE otherwise sign-extends a 16-bit left operand. + Sql.Append("(CLNG(ASCW("); Visit(convertExpression.Operand); - Sql.Append(")"); + Sql.Append(")) BAND 65535)"); return convertExpression; } @@ -1446,4 +1447,4 @@ protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql) } } } -} \ No newline at end of file +} diff --git a/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs b/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs index 55c06a1b..5ee3a563 100644 --- a/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs +++ b/src/LibRed/LibRed.EFCore/Query/Sql/Internal/LibRedQuerySqlGenerator.cs @@ -284,9 +284,10 @@ SqlFunctionExpression WrapConvert(SqlExpression inner) => // through leaves a one-character string, which Jet then coerces by parsing it: 1, not 49. if (typeMapping.ClrType.IsInteger() && typeMapping.ClrType != typeof(char) && convertExpression.Operand.Type == typeof(char)) { - Sql.Append("ASCW("); + // Widen ASCW's signed Int16 before masking: ACE otherwise sign-extends a 16-bit left operand. + Sql.Append("(CLNG(ASCW("); Visit(convertExpression.Operand); - Sql.Append(")"); + Sql.Append(")) BAND 65535)"); return convertExpression; } @@ -478,4 +479,4 @@ protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql) } } } -} \ No newline at end of file +} diff --git a/test/LibRed.EFCore.Tests/CharacterConversionTests.cs b/test/LibRed.EFCore.Tests/CharacterConversionTests.cs new file mode 100644 index 00000000..474cbc3f --- /dev/null +++ b/test/LibRed.EFCore.Tests/CharacterConversionTests.cs @@ -0,0 +1,44 @@ +using EntityFrameworkCore.LibRed.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace LibRed.EFCore.Tests; + +public class CharacterConversionTests +{ + [Theory] + [InlineData(LibRedSqlMode.Compatible)] + [InlineData(LibRedSqlMode.Extended)] + public void Character_to_uint_preserves_unsigned_utf16_values(LibRedSqlMode mode) + { + string path = Path.Combine(Path.GetTempPath(), $"libred-char-{Guid.NewGuid():N}.accdb"); + File.Copy(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), path); + try + { + var options = new DbContextOptionsBuilder().UseLibRed(path, mode).Options; + using var context = new CharacterContext(options); + context.Database.ExecuteSqlRaw("CREATE TABLE CharRows (Id LONG PRIMARY KEY, Value TEXT(1))"); + char[] values = ['1', '\u7FFF', '\u8000', '\uFFFF']; + for (int i = 0; i < values.Length; i++) + context.Database.ExecuteSqlRaw("INSERT INTO CharRows (Id, Value) VALUES ({0}, {1})", i, values[i].ToString()); + + Assert.Equal(values.Select(c => (uint)c), + context.Rows.OrderBy(r => r.Id).Select(r => (uint)r.Value).ToArray()); + } + finally { File.Delete(path); } + } + + private sealed class CharacterRow + { + public int Id { get; set; } + public char Value { get; set; } + } + + private sealed class CharacterContext(DbContextOptions options) : DbContext(options) + { + public DbSet Rows => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) => + modelBuilder.Entity().ToTable("CharRows"); + } +} From 5ecc78df58155959741cf85aba029c4a9ac2b091 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 30 Sep 2026 19:29:34 +0800 Subject: [PATCH 82/88] Keep the columns duplicate elimination and positional renames depend on Column pruning lets a table read decode only the columns its statement names, which is wrong under anything that looks at a whole row. DISTINCT compares every column, so SELECT Grp FROM (SELECT DISTINCT * FROM T) collapsed twelve rows to three; UNION, INTERSECT and EXCEPT did the same. A set operation also matches its inputs' columns by position, so a SELECT * FROM U under a UNION ALL read none of U's columns when the outer query named them by the other arm's names. The inputs of both now read every column. A derived table with a column list, AS t(x, y), renames its input's columns by position, and the names the outer query uses say nothing about the names inside it. It is left unpruned. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Engine/Planning/ColumnPruning.cs | 15 +++++++++++---- test/LibRed.Engine.Tests/ColumnPruningTests.cs | 14 ++++++++++++++ .../LibRed.Engine.Tests/DerivedColumnListTests.cs | 4 ++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs b/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs index ec37e67e..d006a4be 100644 --- a/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs +++ b/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs @@ -43,8 +43,10 @@ public static PlanNode Apply(PlanNode plan) ScanNode or IndexSeekNode or IndexRangeSeekNode => !visible, ProjectNode p => HasHiddenRead(p.Input, HasStar(p.Projection)), AggregateNode a => HasHiddenRead(a.Input, HasStar(a.Projection)), - FilterNode or SortNode or LimitNode or DistinctNode or DerivedTableNode or WindowNode - or JoinNode or HashJoinNode or SetOperationNode => node.Children.Any(c => HasHiddenRead(c, visible)), + DistinctNode or SetOperationNode => node.Children.Any(c => HasHiddenRead(c, visible: true)), + DerivedTableNode { Columns: not null } => false, + FilterNode or SortNode or LimitNode or DerivedTableNode or WindowNode + or JoinNode or HashJoinNode => node.Children.Any(c => HasHiddenRead(c, visible)), _ => false, }; @@ -65,7 +67,11 @@ FilterNode or SortNode or LimitNode or DistinctNode or DerivedTableNode or Windo FilterNode f => f with { Input = Rewrite(f.Input, visible, names) }, SortNode s => s with { Input = Rewrite(s.Input, visible, names) }, LimitNode l => l with { Input = Rewrite(l.Input, visible, names) }, - DistinctNode d => d with { Input = Rewrite(d.Input, visible, names) }, + // Duplicate elimination observes every input value, even under a narrow outer projection. + DistinctNode d => d with { Input = Rewrite(d.Input, visible: true, names) }, + // A column list renames by position. Physical names cannot be inferred from outer references, so + // keep this subtree intact, including any joins that would otherwise drop unnamed columns. + DerivedTableNode { Columns: not null } dt => dt, DerivedTableNode dt => dt with { Input = Rewrite(dt.Input, visible, names) }, WindowNode w => w with { Input = Rewrite(w.Input, visible, names) }, // A join builds each output row anew, so where its rows cannot surface whole it builds them from the read @@ -83,7 +89,8 @@ FilterNode or SortNode or LimitNode or DistinctNode or DerivedTableNode or Windo Right = Rewrite(h.Right, visible, names), Keep = visible ? null : names, }, - SetOperationNode so => so with { Left = Rewrite(so.Left, visible, names), Right = Rewrite(so.Right, visible, names) }, + // Set operations compare whole rows and match columns positionally across differently named inputs. + SetOperationNode so => so with { Left = Rewrite(so.Left, visible: true, names), Right = Rewrite(so.Right, visible: true, names) }, // DistinctRowNode compares whole underlying rows; anything else is unknown. Neither is pruned below. _ => node, diff --git a/test/LibRed.Engine.Tests/ColumnPruningTests.cs b/test/LibRed.Engine.Tests/ColumnPruningTests.cs index c3c1f7dd..1df53003 100644 --- a/test/LibRed.Engine.Tests/ColumnPruningTests.cs +++ b/test/LibRed.Engine.Tests/ColumnPruningTests.cs @@ -163,6 +163,20 @@ public void A_bare_count_reads_no_column_and_still_counts_every_row() // --- shapes that pass whole rows to the output, which must not be pruned --------------------------- + [Theory] + [InlineData("SELECT Grp FROM (SELECT DISTINCT * FROM T) AS d")] + [InlineData("SELECT Grp FROM (SELECT * FROM T UNION SELECT * FROM T) AS d")] + [InlineData("SELECT Grp FROM (SELECT * FROM T INTERSECT SELECT * FROM T) AS d")] + [InlineData("SELECT Grp FROM (SELECT * FROM T EXCEPT SELECT * FROM T WHERE Grp < 0) AS d")] + public void An_outer_projection_preserves_columns_used_for_inner_duplicate_elimination(string sql) + => Assert.Equal([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2], Sorted(Seeded(), sql)); + + // U's columns reach the outer Id by position, under names the statement never writes. + [Fact] + public void A_union_all_preserves_positionally_matched_columns_with_different_names() + => Assert.Equal([0, 1, 1, 2], Sorted(Seeded(), + "SELECT Id FROM (SELECT Id, V, Grp FROM T WHERE Id = 1 UNION ALL SELECT * FROM U) AS d")); + [Fact] public void Select_star_returns_every_column() => Assert.Equal([5, 2, 50, "L5", "N5"], RowOfT(Seeded(), 5).Select(Normalise)); diff --git a/test/LibRed.Engine.Tests/DerivedColumnListTests.cs b/test/LibRed.Engine.Tests/DerivedColumnListTests.cs index c77c12cc..e3e3bba8 100644 --- a/test/LibRed.Engine.Tests/DerivedColumnListTests.cs +++ b/test/LibRed.Engine.Tests/DerivedColumnListTests.cs @@ -34,6 +34,10 @@ private string Run(string sql) [InlineData("SELECT v.s FROM (VALUES (1, 'a'), (2, 'b')) AS v(n, s) WHERE v.n = 2", "[s] b")] [InlineData("SELECT x FROM (VALUES (1), (2)) v(x) ORDER BY x DESC", "[x] 2 | 1")] [InlineData("SELECT t.x FROM (SELECT Id, K FROM T) AS t(x, y) WHERE t.y = 70", "[x] 7")] + [InlineData("SELECT x FROM (SELECT * FROM T) AS t(x, y) ORDER BY x", "[x] 1 | 2 | 7")] + [InlineData("SELECT t.x FROM (SELECT * FROM T) AS t(x, y) WHERE t.y = 70", "[x] 7")] + [InlineData("SELECT x FROM (SELECT * FROM (SELECT * FROM T) AS t(x, y)) AS d ORDER BY x", "[x] 1 | 2 | 7")] + [InlineData("SELECT x FROM (SELECT a.* FROM T AS a INNER JOIN T AS b ON a.Id = b.Id) AS t(x, y) ORDER BY x", "[x] 1 | 2 | 7")] [InlineData("SELECT * FROM (SELECT Id AS a, K AS b FROM T WHERE Id = 1) AS t(c, d)", "[c,d] 1,10")] [InlineData("SELECT * FROM (SELECT Id FROM T WHERE Id = 1 UNION ALL VALUES (5)) AS u(n)", "[n] 1 | 5")] [InlineData("SELECT a.Id, v.label FROM T AS a INNER JOIN (VALUES (1, 'one'), (7, 'seven')) AS v(id, label) ON a.Id = v.id ORDER BY a.Id", From 86325bfd04d1d9e0384ad1f93418cdf64085e36b Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 30 Sep 2026 19:29:38 +0800 Subject: [PATCH 83/88] Check a foreign key's parent again at commit, and existing rows on ADD A child INSERT or UPDATE inside a transaction checks that its parent exists, and reading the parent writes no page, so the commit's page conflict check could not see it: another connection could delete the parent and commit, and both transactions would succeed, leaving a child referencing nothing. The check is now registered with the transaction and made again when it commits. It only holds the commit to the parent while the parent is still needed: while the relationship survives and a child row still holds the key, so deleting or moving the child, or dropping the relationship, releases it. The relationship is identified by its tables' definition pages and its columns' ids, so renaming a table or column in the same transaction does not lose it. The transaction holds one check per relationship and key however many rows relied on it, and a savepoint rollback frees the keys of the checks it discards. ALTER TABLE ... ADD FOREIGN KEY never looked at the rows already in the table. It is now refused where ACE refuses it, with ACE's message: a child with no parent, or a composite key partly null, whatever the ON DELETE action. ACE then holds both tables exclusively until the transaction ends; LibRed takes no table locks, so the same check is made again at commit. A violation raised by an UPDATE now says so, not "INSERT into". Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/IO/PageChannel.cs | 23 ++- src/LibRed/LibRed.Core/JetDatabase.cs | 3 +- .../Execution/StatementExecutor.cs | 136 ++++++++++++++++-- src/LibRed/docs/design/transactions.md | 12 +- test/LibRed.Core.Tests/PageChannelTests.cs | 32 +++++ .../AlterTableAddForeignKeyTests.cs | 73 ++++++++++ .../LibRed.Engine.Tests/ForeignKeyDdlTests.cs | 5 + .../ReaderWriterIsolationTests.cs | 101 +++++++++++++ .../SqlTransactionControlTests.cs | 35 +++++ 9 files changed, 403 insertions(+), 17 deletions(-) diff --git a/src/LibRed/LibRed.Core/IO/PageChannel.cs b/src/LibRed/LibRed.Core/IO/PageChannel.cs index 498d1dae..aa174332 100644 --- a/src/LibRed/LibRed.Core/IO/PageChannel.cs +++ b/src/LibRed/LibRed.Core/IO/PageChannel.cs @@ -67,8 +67,10 @@ public sealed class PageChannel : IDisposable // check above, and two transactions can each check the other's precondition and commit to a state neither // would have allowed (write skew). Each condition is re-evaluated at commit, under the publication lock, // so a commit that would leave a dangling reference is refused instead. A savepoint rollback truncates - // them with the writes that needed them. - private readonly List<(Func StillHolds, string Violation)> _dependencies = []; + // them with the writes that needed them. A condition registered under a key is held once: `_dependencyKeys` + // holds the keys of the conditions `_dependencies` still has. + private readonly List<(Func StillHolds, string Violation, object? Key)> _dependencies = []; + private readonly HashSet _dependencyKeys = []; // Whether anything this channel wrote has been published, so a close knows the session changed the file. private bool _published; @@ -108,12 +110,16 @@ internal void ReleaseAtClose(int page) /// again at commit, and a commit whose condition has since stopped holding is refused with /// . Outside a transaction there is nothing to record: the statement's check /// and its write are one publication, which no other connection can interleave with. + /// A names the condition, by its own equality: one already held under an equal + /// key is not registered again, so a transaction inserting a thousand children of one parent checks that + /// parent once at commit, not a thousand times. /// - public void DependOn(Func stillHolds, string violation) + public void DependOn(Func stillHolds, string violation, object? key = null) { ArgumentNullException.ThrowIfNull(stillHolds); if (_active is null) return; - _dependencies.Add((stillHolds, violation)); + if (key is not null && !_dependencyKeys.Add(key)) return; + _dependencies.Add((stillHolds, violation, key)); } /// The committed pages held for release at close, in the order they were freed. @@ -486,7 +492,7 @@ public void CommitTransaction(bool flush = true) // And what the transaction merely READ and relied on. Evaluated here, inside the publication lock, // so nothing can change between the check and the publish it guards. - foreach ((Func stillHolds, string violation) in _dependencies) + foreach ((Func stillHolds, string violation, _) in _dependencies) if (!stillHolds()) throw new InvalidOperationException(violation); @@ -576,6 +582,7 @@ private void ClearTransactionState() _overlayParsed.Clear(); _commitBaselines.Clear(); _dependencies.Clear(); + _dependencyKeys.Clear(); _releasing.Clear(); } @@ -657,8 +664,12 @@ public void RollbackToSavepoint(Savepoint savepoint) var (before, pageCount) = _active.TakeForRollbackTo(savepoint); RestoreOverlay(before, pageCount); _releasing.RemoveRange(releaseCount, _releasing.Count - releaseCount); - // The writes that needed them are undone, so the conditions are no longer anything to hold the commit to. + // The writes that needed them are undone, so the conditions are no longer anything to hold the commit to — + // and a key they held is free to be registered again by a write made after the rollback. _dependencies.RemoveRange(dependencyCount, _dependencies.Count - dependencyCount); + _dependencyKeys.Clear(); + foreach ((_, _, object? key) in _dependencies) + if (key is not null) _dependencyKeys.Add(key); } /// Releases , merging its changes into the enclosing scope. Only the diff --git a/src/LibRed/LibRed.Core/JetDatabase.cs b/src/LibRed/LibRed.Core/JetDatabase.cs index 7922012b..5bf98a10 100644 --- a/src/LibRed/LibRed.Core/JetDatabase.cs +++ b/src/LibRed/LibRed.Core/JetDatabase.cs @@ -190,7 +190,8 @@ public void RollbackToSavepoint(Savepoint savepoint) public void ReleaseSavepoint(Savepoint savepoint) => _channel.ReleaseSavepoint(savepoint); /// - public void DependOn(Func stillHolds, string violation) => _channel.DependOn(stillHolds, violation); + public void DependOn(Func stillHolds, string violation, object? key = null) => + _channel.DependOn(stillHolds, violation, key); // --- nested transactions (shared by the ADO API and SQL BEGIN/COMMIT/ROLLBACK) --- // One physical transaction; nesting maps onto the savepoint stack. The depth counts every open level, so a diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 3587a1ba..6acf0054 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -166,8 +166,9 @@ private static string DefaultRelationshipName(string childTable, ForeignKeyConst /// ACE's **MATCH FULL** rule (verified vs ACE, and unlike SQL Server's MATCH SIMPLE): a composite FK is /// skipped only when **every** column is null; a **partial** null (some null, some not) can never match a /// parent key and is rejected. Only enforced relationships (grbit without "don't enforce") are checked. + /// says the row is an UPDATE's rather than an INSERT's, for the message. /// - private void EnforceReferentialIntegrity(string childTable, Table table, object?[] values) + private void EnforceReferentialIntegrity(string childTable, Table table, object?[] values, bool update = false) { foreach (ForeignKey fk in _database.Catalog.ForeignKeysOf(childTable)) { @@ -199,19 +200,115 @@ private void EnforceReferentialIntegrity(string childTable, Table table, object? if (partialNull || !ParentRowExists(fk, target)) throw new InvalidOperationException( - $"INSERT into '{childTable}' violates foreign key '{fk.Name}': no matching row in '{fk.ReferencedTable}'."); + $"{(update ? "UPDATE of" : "INSERT into")} '{childTable}' violates foreign key '{fk.Name}': " + + $"no matching row in '{fk.ReferencedTable}'."); // Inside a transaction, finding the parent now is not enough: reading it writes no page, so the // commit's page-conflict check cannot see the dependency, and a concurrent transaction is free to // delete that parent and commit — each having checked the other's precondition, leaving a child row - // referencing nothing. The condition is re-checked when this transaction commits. + // referencing nothing. The condition is re-checked when this transaction commits, once per + // relationship and key however many rows relied on it. + var dependency = new ForeignKeyDependency( + Identify(fk) ?? throw new InvalidOperationException( + $"Relationship '{fk.Name}' names a table or column that does not exist."), + target); _database.DependOn( - () => ParentRowExists(fk, target), + () => ForeignKeyDependencyHolds(dependency), $"Transaction conflict on foreign key '{fk.Name}': the row in '{fk.ReferencedTable}' that " - + $"'{childTable}' was checked against no longer exists."); + + $"'{childTable}' was checked against no longer exists.", + key: dependency); } } + /// + /// Whether the parent a transaction's write relied on is still there, or no longer needed. It is needed only + /// while the relationship survives and a child row still holds the key: later statements may delete the child, + /// move its key, cascade it, or drop the relationship. The relationship is found by name and is the same one + /// only while it joins the same tables over the same columns, whatever they are called by now. The parent is + /// looked for first, since it is almost always still there. + /// + private bool ForeignKeyDependencyHolds(ForeignKeyDependency dependency) + { + ForeignKey? relationship = CurrentRelationship(dependency.Relationship); + if (relationship is null || ParentRowExists(relationship, dependency.Key)) return true; + + Table child = _database.OpenTable(relationship.Table); + int[] columns = [.. relationship.Columns.Select(c => child.Definition.RequireColumn(c.Column).Index)]; + return !RowsHoldingKey(child, columns, dependency.Key).Any(); + } + + /// The enforced relationship identifies, as the catalog holds it now; null + /// once it has been dropped. + private ForeignKey? CurrentRelationship(RelationshipIdentity identity) => + _database.Catalog.Relationships.FirstOrDefault(r => r.IsEnforced + && string.Equals(r.Name, identity.Name, StringComparison.OrdinalIgnoreCase) + && identity.Equals(Identify(r))); + + /// Whether every row of the relationship's child table has its parent, on the MATCH FULL terms + /// applies to one row: a key that is entirely null references + /// nothing and needs nothing, and a partly null one can never match. + private bool ExistingRowsHaveParents(ForeignKey fk) + { + Table child = _database.OpenTable(fk.Table); + int[] columns = [.. fk.Columns.Select(c => child.Definition.RequireColumn(c.Column).Index)]; + foreach (object?[] row in child.Rows(child.DecodeOnly(columns))) + { + object?[] key = [.. columns.Select(c => row[c])]; + int nulls = key.Count(v => v is null); + if (nulls == key.Length) continue; + if (nulls > 0 || !ParentRowExists(fk, key)) return false; + } + return true; + } + + /// A relationship as the file identifies it rather than by its names: the definition pages of the + /// tables it joins and the ids of its columns, which renaming a table or a column leaves alone. Null when one + /// of them no longer exists. + private RelationshipIdentity? Identify(ForeignKey fk) + { + if (_database.Catalog.FindTable(fk.Table) is not { } child + || _database.Catalog.FindTable(fk.ReferencedTable) is not { } parent) + return null; + var childColumns = new int[fk.Columns.Count]; + var parentColumns = new int[fk.Columns.Count]; + for (int i = 0; i < fk.Columns.Count; i++) + { + if (child.FindColumn(fk.Columns[i].Column) is not { } childColumn + || parent.FindColumn(fk.Columns[i].ReferencedColumn) is not { } parentColumn) + return null; + childColumns[i] = childColumn.ColumnId; + parentColumns[i] = parentColumn.ColumnId; + } + return new RelationshipIdentity(fk.Name, child.DefinitionPage, parent.DefinitionPage, childColumns, parentColumns); + } + + /// A relationship's name, and what it joins as finds it: equal only for the same + /// relationship, so one dropped and added again under its name over other columns is another. + private sealed record RelationshipIdentity( + string Name, int ChildTable, int ParentTable, int[] ChildColumns, int[] ParentColumns) + { + public bool Equals(RelationshipIdentity? other) => other is not null + && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) + && ChildTable == other.ChildTable && ParentTable == other.ParentTable + && ChildColumns.AsSpan().SequenceEqual(other.ChildColumns) + && ParentColumns.AsSpan().SequenceEqual(other.ParentColumns); + + public override int GetHashCode() => + HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(Name), ChildTable, ParentTable); + } + + /// A parent a transaction's write relied on: the relationship, and the key it was checked for. Equal + /// values are one dependency, which is what lets the channel hold it once. + private sealed record ForeignKeyDependency(RelationshipIdentity Relationship, object?[] Key) + { + public bool Equals(ForeignKeyDependency? other) => other is not null + && Relationship.Equals(other.Relationship) + && System.Collections.StructuralComparisons.StructuralEqualityComparer.Equals(Key, other.Key); + + public override int GetHashCode() => HashCode.Combine( + Relationship, System.Collections.StructuralComparisons.StructuralEqualityComparer.GetHashCode(Key)); + } + /// For a self-referencing FK, whether the row's own referenced-column values equal the FK /// target — i.e. the row points at itself (or at its own composite key), which satisfies the FK. private bool RowSatisfiesOwnKey(ForeignKey fk, Table table, object?[] values, object?[] target) => @@ -757,7 +854,30 @@ private int DropColumnDefault(string table, AlterColumnDropDefaultAction drop) private int AddForeignKey(string table, ForeignKeyConstraint fk) { - _database.AddForeignKey(table, ToRelationshipSpec(table, fk)); + RelationshipSpec relationship = ToRelationshipSpec(table, fk); + _database.AddForeignKey(table, relationship); + + // ACE refuses a relationship the existing rows already break — an orphan, or a partly null composite key + // (verified; the ON DELETE action makes no difference). Checked against the relationship as the catalog + // now holds it, a REFERENCES with no column list resolved to the parent's primary key; the statement's own + // rollback takes the relationship back out when the check fails. + ForeignKey added = _database.Catalog.Relationships.Single( + r => string.Equals(r.Name, relationship.Name, StringComparison.OrdinalIgnoreCase)); + RelationshipIdentity identity = Identify(added)!; + bool Holds() => CurrentRelationship(identity) is not { } current || ExistingRowsHaveParents(current); + if (!Holds()) + throw new InvalidOperationException( + "Cannot create relationships to enforce referential integrity. Existing data in table " + + $"'{added.Table}' violates referential integrity rules in table '{added.ReferencedTable}'."); + + // ACE then holds both tables exclusively until the transaction ends, so nothing can delete a parent the + // check found (verified: another connection's DELETE is refused). LibRed takes no table locks, so the + // same check is made again when the transaction commits, for as long as the relationship survives. + _database.DependOn( + Holds, + $"Transaction conflict on foreign key '{added.Name}': a row in '{added.ReferencedTable}' that the " + + $"existing rows of '{added.Table}' were checked against no longer exists.", + key: identity); return 0; } @@ -1823,7 +1943,7 @@ private int Update(UpdateStatement statement) // Child side: a changed FK column must still reference an existing parent (like an insert). if (_database.Catalog.ForeignKeysOf(table.Name).Any(f => f.IsEnforced && f.Columns.Any(c => changed.Contains(table.Definition.FindColumn(c.Column)!.Index)))) - EnforceReferentialIntegrity(table.Name, table, values); + EnforceReferentialIntegrity(table.Name, table, values, update: true); // A changed UNIQUE/PRIMARY key must not collide with another row (null keys are distinct — a // unique index permits multiple nulls, so they're skipped, matching the insert rule). @@ -1914,4 +2034,4 @@ private int Delete(DeleteStatement statement) if (_session is not null) _session.RowCount = affected; return affected; } -} \ No newline at end of file +} diff --git a/src/LibRed/docs/design/transactions.md b/src/LibRed/docs/design/transactions.md index f05292ef..dd8c1230 100644 --- a/src/LibRed/docs/design/transactions.md +++ b/src/LibRed/docs/design/transactions.md @@ -27,8 +27,16 @@ Status: **draft / accepted direction** · Date: 2026-07-18 > depend on (`PageChannel.DependOn`), each re-evaluated under the publication gate before anything is > published; a commit whose condition has stopped holding is refused like a write conflict. The conditions are > semantic rather than physical — the FK's parent is sought again, not its page compared — so an unrelated row -> on the same page cannot produce a false conflict. A savepoint rollback drops the conditions recorded after it -> along with the writes that needed them. +> on the same page cannot produce a false conflict. A missing parent only refuses the commit while it is still +> needed: while the relationship survives and a child row still holds the key, so a transaction that later +> deleted or moved its child, or dropped the relationship, commits. The relationship is identified by the +> definition pages of its tables and the ids of its columns, so renames inside the transaction do not lose it, +> and one dropped and added again under the same name over other columns is a different relationship. A +> condition registered under a key the transaction already holds (the same relationship and parent key) is held +> once. `ALTER TABLE … ADD FOREIGN KEY` reads the same way, checking every existing child row against its +> parent, so it registers one condition for the relationship: while it survives, every child row still has its +> parent. ACE gets the same guarantee by holding both tables exclusively until the transaction ends. A savepoint rollback drops the conditions recorded after it, and their keys, along with the writes that +> needed them. > 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 diff --git a/test/LibRed.Core.Tests/PageChannelTests.cs b/test/LibRed.Core.Tests/PageChannelTests.cs index 2aebb43f..6d683257 100644 --- a/test/LibRed.Core.Tests/PageChannelTests.cs +++ b/test/LibRed.Core.Tests/PageChannelTests.cs @@ -178,4 +178,36 @@ public void ReleaseSavepoint_merges_into_the_parent_so_an_outer_rollback_still_u } finally { TemporaryDatabase.Delete(path); } } + + // A dependency registered again under a key the transaction already holds is the same condition, so the + // commit checks it once. A savepoint rollback discards the dependencies made after it, and their keys with + // them: a write repeated after the rollback needs its condition held again. + [Fact] + public void A_dependency_is_held_once_per_key_and_a_savepoint_rollback_frees_its_key() + { + string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "libred-dep-"); + try + { + using var channel = PageChannel.Open(path, readOnly: false); + int checkedA = 0, checkedB = 0, checkedUnkeyed = 0; + + channel.BeginTransaction(); + channel.DependOn(() => ++checkedA > 0, "a", key: "A"); + channel.DependOn(() => ++checkedA > 0, "a", key: "A"); + channel.DependOn(() => ++checkedUnkeyed > 0, "u"); + channel.DependOn(() => ++checkedUnkeyed > 0, "u"); + + Savepoint sp = channel.CreateSavepoint(); + channel.DependOn(() => ++checkedB > 0, "b", key: "B"); + channel.RollbackToSavepoint(sp); + channel.DependOn(() => ++checkedB > 0, "b", key: "B"); + channel.DependOn(() => ++checkedB > 0, "b", key: "B"); + + channel.CommitTransaction(); + Assert.Equal(1, checkedA); + Assert.Equal(1, checkedB); + Assert.Equal(2, checkedUnkeyed); + } + finally { TemporaryDatabase.Delete(path); } + } } diff --git a/test/LibRed.Engine.Tests/AlterTableAddForeignKeyTests.cs b/test/LibRed.Engine.Tests/AlterTableAddForeignKeyTests.cs index fdf12a80..d7287d3a 100644 --- a/test/LibRed.Engine.Tests/AlterTableAddForeignKeyTests.cs +++ b/test/LibRed.Engine.Tests/AlterTableAddForeignKeyTests.cs @@ -39,6 +39,79 @@ public void Add_foreign_key_to_existing_table() finally { TemporaryDatabase.Delete(path); } } + // The relationship is refused when the rows already in the table break it, and nothing of it is left behind: + // an orphan inserted afterwards is accepted. The same rows ACE refuses and accepts (probed over OLE DB): MATCH + // FULL, so a composite key that is partly null is refused and one entirely null is not, and the ON DELETE + // action makes no difference. + [Theory] + [InlineData("10, 1, NULL", "(ParentId) REFERENCES Parents (Id)", true)] + [InlineData("10, NULL, NULL", "(ParentId) REFERENCES Parents (Id)", true)] + [InlineData("10, 2, NULL", "(ParentId) REFERENCES Parents (Id)", false)] + [InlineData("10, 2, NULL", "(ParentId) REFERENCES Parents", false)] + [InlineData("10, 2, NULL", "(ParentId) REFERENCES Parents (Id) ON DELETE CASCADE", false)] + [InlineData("10, 2, NULL", "(ParentId) REFERENCES Parents (Id) ON DELETE SET NULL", false)] + [InlineData("10, 1, 1", "(ParentId, Code) REFERENCES Parents (Id, Code)", true)] + [InlineData("10, NULL, NULL", "(ParentId, Code) REFERENCES Parents (Id, Code)", true)] + [InlineData("10, 1, NULL", "(ParentId, Code) REFERENCES Parents (Id, Code)", false)] + [InlineData("10, 1, 2", "(ParentId, Code) REFERENCES Parents (Id, Code)", false)] + public void Add_foreign_key_checks_the_rows_already_in_the_table(string row, string constraint, bool accepted) + { + string path = Fresh(); + try + { + using var db = JetDatabase.Open(path, readOnly: false); + var e = new QueryEngine(db); + e.ExecuteNonQuery("CREATE TABLE Parents (Id LONG PRIMARY KEY, Code LONG, CONSTRAINT UQ_Code UNIQUE (Id, Code))"); + e.ExecuteNonQuery("CREATE TABLE Children (Id LONG PRIMARY KEY, ParentId LONG, Code LONG)"); + e.ExecuteNonQuery("INSERT INTO Parents (Id, Code) VALUES (1, 1)"); + e.ExecuteNonQuery($"INSERT INTO Children (Id, ParentId, Code) VALUES ({row})"); + + const string add = "ALTER TABLE Children ADD CONSTRAINT FK_Child FOREIGN KEY "; + if (accepted) + { + e.ExecuteNonQuery(add + constraint); + Assert.Single(db.Catalog.ForeignKeysOf("Children")); + Assert.Throws( + () => e.ExecuteNonQuery("INSERT INTO Children (Id, ParentId, Code) VALUES (20, 3, 3)")); + } + else + { + var refused = Assert.Throws(() => e.ExecuteNonQuery(add + constraint)); + Assert.Equal( + "Cannot create relationships to enforce referential integrity. Existing data in table " + + "'Children' violates referential integrity rules in table 'Parents'.", refused.Message); + Assert.Empty(db.Catalog.ForeignKeysOf("Children")); + e.ExecuteNonQuery("INSERT INTO Children (Id, ParentId, Code) VALUES (20, 3, 3)"); + } + } + finally { TemporaryDatabase.Delete(path); } + } + + // A self-reference is checked against the table's own rows: a row may point at itself or at another row that + // is there, and not at one that isn't. + [Theory] + [InlineData("(1, 1), (2, 1)", true)] + [InlineData("(1, NULL), (2, 3)", false)] + public void Add_self_referencing_foreign_key_checks_the_rows_already_in_the_table(string rows, bool accepted) + { + string path = Fresh(); + try + { + using var db = JetDatabase.Open(path, readOnly: false); + var e = new QueryEngine(db); + e.ExecuteNonQuery("CREATE TABLE Staff (EmployeeID LONG PRIMARY KEY, ReportsTo LONG)"); + e.ExecuteNonQuery($"INSERT INTO Staff (EmployeeID, ReportsTo) VALUES {rows}"); + + const string add = "ALTER TABLE Staff ADD CONSTRAINT FK_Staff_Staff FOREIGN KEY (ReportsTo) REFERENCES Staff (EmployeeID)"; + if (accepted) + e.ExecuteNonQuery(add); + else + Assert.Throws(() => e.ExecuteNonQuery(add)); + Assert.Equal(accepted ? 1 : 0, db.Catalog.ForeignKeysOf("Staff").Count()); + } + finally { TemporaryDatabase.Delete(path); } + } + // A self-referencing foreign key (Northwind's Employees.ReportsTo → Employees.EmployeeID). [Fact] public void Add_self_referencing_foreign_key() diff --git a/test/LibRed.Engine.Tests/ForeignKeyDdlTests.cs b/test/LibRed.Engine.Tests/ForeignKeyDdlTests.cs index 158a4616..08ae3724 100644 --- a/test/LibRed.Engine.Tests/ForeignKeyDdlTests.cs +++ b/test/LibRed.Engine.Tests/ForeignKeyDdlTests.cs @@ -31,6 +31,11 @@ public void Foreign_key_persists_enforces_and_round_trips() // Orphan reference is rejected. AssertForeignKeyViolation(engine, "INSERT INTO `Child` (`Id`, `ParentId`) VALUES (3, 99)", "FK_Child_Parent", "Parent"); + + // So is moving a child to one, and the message names the statement that did it. + var moved = Assert.Throws( + () => engine.ExecuteNonQuery("UPDATE `Child` SET `ParentId` = 99 WHERE `Id` = 1")); + Assert.Contains("UPDATE of 'Child' violates foreign key 'FK_Child_Parent'", moved.Message, StringComparison.Ordinal); } using (var db = JetDatabase.Open(path)) diff --git a/test/LibRed.Engine.Tests/ReaderWriterIsolationTests.cs b/test/LibRed.Engine.Tests/ReaderWriterIsolationTests.cs index f9a3c3b3..d7f673bd 100644 --- a/test/LibRed.Engine.Tests/ReaderWriterIsolationTests.cs +++ b/test/LibRed.Engine.Tests/ReaderWriterIsolationTests.cs @@ -152,6 +152,107 @@ public void A_child_insert_cannot_commit_after_its_parent_is_deleted_by_another_ finally { TemporaryDatabase.Delete(path); } } + // The dependency is on the parent being needed, not on it having been checked: once the child has moved to + // another parent, the one it was inserted against can go, and the commit has nothing to object to. + [Fact] + public void A_child_moved_to_another_parent_commits_after_the_first_parent_is_deleted_by_another_connection() + { + string path = CreateDatabase("write-skew-moved-"); + try + { + using var childDb = JetDatabase.Open(path, readOnly: false); + using var parentDb = JetDatabase.Open(path, readOnly: false); + var child = new QueryEngine(childDb); + var parent = new QueryEngine(parentDb); + + child.ExecuteNonQuery("CREATE TABLE Parents (Id LONG PRIMARY KEY)"); + child.ExecuteNonQuery( + "CREATE TABLE Children (Id LONG PRIMARY KEY, ParentId LONG REFERENCES Parents (Id))"); + child.ExecuteNonQuery("INSERT INTO Parents (Id) VALUES (1)"); + child.ExecuteNonQuery("INSERT INTO Parents (Id) VALUES (2)"); + + child.ExecuteNonQuery("BEGIN TRANSACTION"); + child.ExecuteNonQuery("INSERT INTO Children (Id, ParentId) VALUES (10, 1)"); + child.ExecuteNonQuery("UPDATE Children SET ParentId = 2 WHERE Id = 10"); + + parent.ExecuteNonQuery("DELETE FROM Parents WHERE Id = 1"); + + child.ExecuteNonQuery("COMMIT"); + Assert.Equal(2, Convert.ToInt32(parent.ExecuteQuery("SELECT ParentId FROM Children").Rows.Single()[0])); + } + finally { TemporaryDatabase.Delete(path); } + } + + // Renaming inside the transaction changes every name the dependency was registered under — the child table, + // its column, the parent table — but not the relationship, which still joins the same tables over the same + // columns. The parent it relied on is still needed, so its deletion elsewhere still stops the commit. + [Fact] + public void A_child_insert_cannot_commit_after_its_parent_is_deleted_even_when_the_tables_were_renamed() + { + string path = CreateDatabase("write-skew-renamed-"); + try + { + using var childDb = JetDatabase.Open(path, readOnly: false); + using var parentDb = JetDatabase.Open(path, readOnly: false); + var child = new QueryEngine(childDb); + var parent = new QueryEngine(parentDb); + + child.ExecuteNonQuery("CREATE TABLE Parents (Id LONG PRIMARY KEY)"); + child.ExecuteNonQuery( + "CREATE TABLE Children (Id LONG PRIMARY KEY, ParentId LONG REFERENCES Parents (Id))"); + child.ExecuteNonQuery("INSERT INTO Parents (Id) VALUES (1)"); + + child.ExecuteNonQuery("BEGIN TRANSACTION"); + child.ExecuteNonQuery("INSERT INTO Children (Id, ParentId) VALUES (10, 1)"); + child.ExecuteNonQuery("ALTER TABLE Children RENAME TO Kids"); + child.ExecuteNonQuery("ALTER TABLE Kids RENAME COLUMN ParentId TO Pid"); + child.ExecuteNonQuery("ALTER TABLE Parents RENAME TO Folks"); + + parent.ExecuteNonQuery("DELETE FROM Parents WHERE Id = 1"); + + var conflict = Assert.Throws(() => child.ExecuteNonQuery("COMMIT")); + Assert.Contains("no longer exists", conflict.Message, StringComparison.Ordinal); + child.ExecuteNonQuery("ROLLBACK"); + Assert.Empty(parent.ExecuteQuery("SELECT Id FROM Children").Rows); + } + finally { TemporaryDatabase.Delete(path); } + } + + // Adding a relationship checks the child rows already there against their parents, which is the same write + // skew as an insert's check: nothing it read is written. ACE closes it by holding both tables exclusively + // until the transaction ends; LibRed checks the rows again when the transaction commits. The parent goes by a + // key UPDATE rather than a DELETE: a delete rewrites the parent TDEF's row count, and the ADD wrote that page + // too (its incoming relationship block), so the page check alone already refuses that commit. + [Fact] + public void An_added_relationship_cannot_commit_after_a_parent_it_was_checked_against_is_gone() + { + string path = CreateDatabase("write-skew-add-"); + try + { + using var childDb = JetDatabase.Open(path, readOnly: false); + using var parentDb = JetDatabase.Open(path, readOnly: false); + var child = new QueryEngine(childDb); + var parent = new QueryEngine(parentDb); + + child.ExecuteNonQuery("CREATE TABLE Parents (Id LONG PRIMARY KEY)"); + child.ExecuteNonQuery("CREATE TABLE Children (Id LONG PRIMARY KEY, ParentId LONG)"); + child.ExecuteNonQuery("INSERT INTO Parents (Id) VALUES (1)"); + child.ExecuteNonQuery("INSERT INTO Children (Id, ParentId) VALUES (10, 1)"); + + child.ExecuteNonQuery("BEGIN TRANSACTION"); + child.ExecuteNonQuery( + "ALTER TABLE Children ADD CONSTRAINT FK_Child FOREIGN KEY (ParentId) REFERENCES Parents (Id)"); + + parent.ExecuteNonQuery("UPDATE Parents SET Id = 5 WHERE Id = 1"); + + var conflict = Assert.Throws(() => child.ExecuteNonQuery("COMMIT")); + Assert.Contains("no longer exists", conflict.Message, StringComparison.Ordinal); + child.ExecuteNonQuery("ROLLBACK"); + Assert.Empty(childDb.Catalog.ForeignKeysOf("Children")); + } + finally { TemporaryDatabase.Delete(path); } + } + private static void RunCrossingCommit(string? password) { string path = CreateDatabase("reader-crossing-"); diff --git a/test/LibRed.Engine.Tests/SqlTransactionControlTests.cs b/test/LibRed.Engine.Tests/SqlTransactionControlTests.cs index e36339cb..314831ee 100644 --- a/test/LibRed.Engine.Tests/SqlTransactionControlTests.cs +++ b/test/LibRed.Engine.Tests/SqlTransactionControlTests.cs @@ -109,6 +109,41 @@ public void Commit_with_no_transaction_open_throws() Assert.Throws(() => e.ExecuteNonQuery("COMMIT")); } + [Theory] + [InlineData("DELETE FROM Children WHERE Id = 10", 0)] + [InlineData("UPDATE Children SET ParentId = 2 WHERE Id = 10", 1)] + [InlineData("UPDATE Children SET ParentId = NULL WHERE Id = 10", 1)] + public void Commit_does_not_require_a_parent_after_its_child_reference_is_removed(string change, int children) + { + QueryEngine e = Fresh(); + e.ExecuteNonQuery("CREATE TABLE Parents (Id LONG PRIMARY KEY)"); + e.ExecuteNonQuery("CREATE TABLE Children (Id LONG PRIMARY KEY, ParentId LONG REFERENCES Parents (Id))"); + e.ExecuteNonQuery("INSERT INTO Parents VALUES (1), (2)"); + e.ExecuteNonQuery("BEGIN TRANSACTION"); + e.ExecuteNonQuery("INSERT INTO Children VALUES (10, 1)"); + e.ExecuteNonQuery(change); + e.ExecuteNonQuery("DELETE FROM Parents WHERE Id = 1"); + e.ExecuteNonQuery("COMMIT"); + Assert.Equal(children, Convert.ToInt32(e.ExecuteQuery("SELECT COUNT(*) FROM Children").Rows.Single()[0])); + Assert.Equal(2, Convert.ToInt32(e.ExecuteQuery("SELECT Id FROM Parents").Rows.Single()[0])); + } + + [Fact] + public void Commit_does_not_require_a_parent_after_its_relationship_is_dropped() + { + QueryEngine e = Fresh(); + e.ExecuteNonQuery("CREATE TABLE Parents (Id LONG PRIMARY KEY)"); + e.ExecuteNonQuery("CREATE TABLE Children (Id LONG PRIMARY KEY, ParentId LONG, CONSTRAINT FK_Child FOREIGN KEY (ParentId) REFERENCES Parents (Id))"); + e.ExecuteNonQuery("INSERT INTO Parents VALUES (1)"); + e.ExecuteNonQuery("BEGIN TRANSACTION"); + e.ExecuteNonQuery("INSERT INTO Children VALUES (10, 1)"); + e.ExecuteNonQuery("ALTER TABLE Children DROP CONSTRAINT FK_Child"); + e.ExecuteNonQuery("DELETE FROM Parents WHERE Id = 1"); + e.ExecuteNonQuery("COMMIT"); + Assert.Single(e.ExecuteQuery("SELECT * FROM Children").Rows); + Assert.Empty(e.ExecuteQuery("SELECT * FROM Parents").Rows); + } + [Fact] public void Sql_outer_transaction_and_ado_inner_transaction_share_one_controller() { From 1ef1342d2189281a37a80610ab8de57d2ad2a546 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 30 Sep 2026 19:29:40 +0800 Subject: [PATCH 84/88] Add a self-referencing foreign key through the same path as any other TableCreator.AddForeignKey had a branch for a self-reference that repeated the rest of the method. What differs is only where the parent's definition page, key index and incoming block number come from: the child itself, with the incoming block taking the next free number above the outgoing one. Those three are chosen first and everything after is shared, with the same calls in the same order, so the bytes written are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../LibRed.Core/Storage/TableCreator.cs | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs index 062a6c7d..4d0e0fd2 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs @@ -903,21 +903,11 @@ public void AddForeignKey(string childTable, RelationshipSpec fk) // A self-reference (child == parent) hosts both ends in the same TDEF: the outgoing block takes the // lowest free number and the incoming block the next free one above it (verified vs ACE: with 1 free // below a live 2, the outgoing block is 1 and the incoming 3). - if (string.Equals(fk.ReferencedTable, childTable, StringComparison.OrdinalIgnoreCase)) - { - int selfRefOrdinal = ReferencedOrdinalIn(child, fk); - int inNum = NextLogicalIndexNumber(child.DefinitionPage, above: NextLogicalIndexNumber(child.DefinitionPage)); - int outNum = InsertIndex(child, fk.Name, slots, - unique: false, required: false, ignoreNulls: false, - (num, ord) => BuildOutgoingInfoBlock(num, ord, FkTypeOutgoing, inNum, child.DefinitionPage, upd, del)); - AddIncomingRelationshipBlock(new IncomingRelationship( - child.DefinitionPage, inNum, selfRefOrdinal, (uint)outNum, child.DefinitionPage, upd, del)); - AddRelationshipRows(childTable, fk); - _catalog.Invalidate(); - return; - } - - (int parentPage, int refOrdinal, int parentNum) = ResolveParent(fk, child.DefinitionPage); + (int parentPage, int refOrdinal, int parentNum) = + string.Equals(fk.ReferencedTable, childTable, StringComparison.OrdinalIgnoreCase) + ? (child.DefinitionPage, ReferencedOrdinalIn(child, fk), + NextLogicalIndexNumber(child.DefinitionPage, above: NextLogicalIndexNumber(child.DefinitionPage))) + : ResolveParent(fk, child.DefinitionPage); int childBlockNum = InsertIndex(child, fk.Name, slots, unique: false, required: false, ignoreNulls: false, (num, ord) => BuildOutgoingInfoBlock(num, ord, FkTypeOutgoing, parentNum, parentPage, upd, del)); From 4e478c77c1a6c9b513583619570e0b5a81130ed7 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Thu, 1 Oct 2026 01:23:53 +0800 Subject: [PATCH 85/88] Check a cascaded row as an updated one, and a parent's end at commit A cascade rewrote a child with its own partial copy of the UPDATE checks: no null-key check, so ON DELETE SET NULL could null a primary-key column, and none of the child's own relationships, so a cascade stopped one level down and ON DELETE SET NULL onto a key other rows referenced left them pointing at nothing. The UPDATE loop's per-row write is now UpdateRow, and SetChildKey hands the rewritten child to it: a cascaded row is checked exactly as a directly updated one, and a key it changes applies its own ON UPDATE rules, cascading on down or refused. The parent's cascade runs once the parent row is written, so the child's foreign-key check finds the new key. The commit-time re-check only ran from the child's end. A DELETE, or a parent key change, that found no child writes nothing another connection's child insert conflicts with, so both could commit. It now registers the same condition for the key it removed, held once with the child side's. SELECT INTO writes through InsertNewRow like any insert. A deleted row, a cascade's and a complex column's values go through one DeleteRow; the MATCH FULL key extraction exists once, shared with ADD FOREIGN KEY's check of existing rows; the "related records" refusal is one throw. CHECK expressions, relationships in both directions, their tables and column positions, and an outer-join UPDATE's defaults are resolved once per statement rather than once per row. PageChannel.PageCount inside a transaction is never below the file's own page count, so a transaction can read what another connection committed on pages appended since it began, which the commit-time check has to read. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/IO/PageChannel.cs | 9 +- .../Execution/StatementExecutor.cs | 401 ++++++++++-------- .../ReaderWriterIsolationTests.cs | 40 ++ .../ReferentialActionTests.cs | 90 ++++ 4 files changed, 352 insertions(+), 188 deletions(-) diff --git a/src/LibRed/LibRed.Core/IO/PageChannel.cs b/src/LibRed/LibRed.Core/IO/PageChannel.cs index aa174332..b2f2e68e 100644 --- a/src/LibRed/LibRed.Core/IO/PageChannel.cs +++ b/src/LibRed/LibRed.Core/IO/PageChannel.cs @@ -151,7 +151,14 @@ internal void MarkSchemaChanged() /// Number of pages currently in the file — or, inside a transaction, the logical count including /// pages the overlay has allocated but not yet written to disk. Read from the shared cache's record of the /// file's length rather than the stream, so it costs no syscall. - public int PageCount => _active is not null ? _txPageCount : (int)(_cache.FileLength / PageSize); + /// Inside a transaction it is never less than the file's own count: another handle can commit pages + /// onto the end of the file while this transaction is open, and a row, index entry or long value on one of them + /// is committed data this transaction must be able to read — a commit-time dependency check reads exactly + /// that. Alone on the file the two are the same, since this transaction's own pages do not reach the file + /// until it commits. + public int PageCount => _active is not null + ? Math.Max(_txPageCount, (int)(_cache.FileLength / PageSize)) + : (int)(_cache.FileLength / PageSize); /// /// Opens a database file, sniffs its Jet/ACE version from page 0 and resolves the diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 6acf0054..901f1c4c 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -19,6 +19,14 @@ internal sealed class StatementExecutor(JetDatabase database, IReadOnlyDictionar // For evaluating VALUES expressions (literals, parameters, and any scalar subqueries). private readonly QueryExecutor _scalarRunner = new(database, parameters, session); + // What the constraint checks look up, resolved once per statement rather than once per row: CHECK expressions + // parsed, each table's enforced relationships from either end, and each relationship's identity. + private readonly Dictionary _checkExpressions = []; + private readonly Dictionary _foreignKeysOf = new([], StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _referencesTo = new([], StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _identities = []; + private readonly Dictionary _ends = []; + public int Execute(SqlStatement statement) => statement switch { CreateTableStatement create => ExecuteCreateTable(create), @@ -133,10 +141,14 @@ private void EnforceCheckConstraints(TableDef definition, object?[] values) var schema = definition.Columns.Select(c => OutputColumn.Of(definition.Name, c)).ToList(); var evaluator = new ExpressionEvaluator(new EvalScope(schema, values, null), _scalarRunner, _parameters, _session); foreach (var (name, expression) in definition.CheckConstraints) - if (evaluator.Evaluate(_parser.ParseExpression(expression)) is false) + { + if (!_checkExpressions.TryGetValue(expression, out Expression? parsed)) + _checkExpressions[expression] = parsed = _parser.ParseExpression(expression); + if (evaluator.Evaluate(parsed) is false) throw new InvalidOperationException( $"One or more values are prohibited by the validation rule '{name}' set for '{definition.Name}'. " + "Enter a value that the expression for this field can accept."); + } } /// The relationship a parsed foreign key creates on , for CREATE TABLE and @@ -170,35 +182,21 @@ private static string DefaultRelationshipName(string childTable, ForeignKeyConst /// private void EnforceReferentialIntegrity(string childTable, Table table, object?[] values, bool update = false) { - foreach (ForeignKey fk in _database.Catalog.ForeignKeysOf(childTable)) + foreach (ForeignKey fk in ForeignKeysOf(childTable)) { - if (!fk.IsEnforced) continue; - - var target = new object?[fk.Columns.Count]; - int nullCount = 0; - for (int i = 0; i < fk.Columns.Count; i++) - { - ColumnDef col = table.Definition.FindColumn(fk.Columns[i].Column) - ?? throw new InvalidOperationException($"Column '{fk.Columns[i].Column}' does not exist in '{childTable}'."); - object? v = values[col.Index]; - if (v is null) nullCount++; - else target[i] = v; - } - if (nullCount == fk.Columns.Count) continue; // all FK columns null → the FK is not applied to this row - - // MATCH FULL: a partial null can never reference a full parent key, so it's a violation — treated - // like a missing parent (ACE gives the same "a related record is required" error). - bool partialNull = nullCount > 0; + RelationshipEnds ends = EndsOf(fk); + // All FK columns null → the FK is not applied to this row. + if (ForeignKeyValue(values, ends.ChildColumns, out bool partialNull) is not { } target) continue; // A **self-referencing** FK: a fully-specified row that points at its own key (the root of a // required self-ref, e.g. Inverse1Id = Id) satisfies the FK even though it isn't on disk yet — the // parent scan runs before the insert. Access allows this (EF's ComplexNavigations seed relies on it). if (!partialNull && string.Equals(fk.ReferencedTable, childTable, StringComparison.OrdinalIgnoreCase) - && RowSatisfiesOwnKey(fk, table, values, target)) + && KeyEquals(values, ends.ParentColumns, target)) continue; - if (partialNull || !ParentRowExists(fk, target)) + if (partialNull || !ParentRowExists(ends, target)) throw new InvalidOperationException( $"{(update ? "UPDATE of" : "INSERT into")} '{childTable}' violates foreign key '{fk.Name}': " + $"no matching row in '{fk.ReferencedTable}'."); @@ -208,18 +206,29 @@ private void EnforceReferentialIntegrity(string childTable, Table table, object? // delete that parent and commit — each having checked the other's precondition, leaving a child row // referencing nothing. The condition is re-checked when this transaction commits, once per // relationship and key however many rows relied on it. - var dependency = new ForeignKeyDependency( - Identify(fk) ?? throw new InvalidOperationException( - $"Relationship '{fk.Name}' names a table or column that does not exist."), - target); - _database.DependOn( - () => ForeignKeyDependencyHolds(dependency), + DependOnRelationship(fk, target, $"Transaction conflict on foreign key '{fk.Name}': the row in '{fk.ReferencedTable}' that " - + $"'{childTable}' was checked against no longer exists.", - key: dependency); + + $"'{childTable}' was checked against no longer exists."); } } + /// + /// Makes the transaction's commit re-check that no row of 's child table holds + /// without a parent row holding it too. Both ends of a relationship need it, because + /// each end's check reads a row the other end's transaction can change and reading writes no page: a child + /// write found its parent, or a parent delete or key change found no children, and a concurrent transaction + /// can commit the opposite write in between. The condition is the same from either end, so it is held once per + /// relationship and key however many rows relied on it. + /// + private void DependOnRelationship(ForeignKey fk, object?[] key, string conflict) + { + var dependency = new ForeignKeyDependency( + IdentityOf(fk) ?? throw new InvalidOperationException( + $"Relationship '{fk.Name}' names a table or column that does not exist."), + key); + _database.DependOn(() => ForeignKeyDependencyHolds(dependency), conflict, key: dependency); + } + /// /// Whether the parent a transaction's write relied on is still there, or no longer needed. It is needed only /// while the relationship survives and a child row still holds the key: later statements may delete the child, @@ -229,12 +238,10 @@ private void EnforceReferentialIntegrity(string childTable, Table table, object? /// private bool ForeignKeyDependencyHolds(ForeignKeyDependency dependency) { - ForeignKey? relationship = CurrentRelationship(dependency.Relationship); - if (relationship is null || ParentRowExists(relationship, dependency.Key)) return true; - - Table child = _database.OpenTable(relationship.Table); - int[] columns = [.. relationship.Columns.Select(c => child.Definition.RequireColumn(c.Column).Index)]; - return !RowsHoldingKey(child, columns, dependency.Key).Any(); + if (CurrentRelationship(dependency.Relationship) is not { } relationship) return true; + RelationshipEnds ends = Ends(relationship); // as the catalog holds them now, not when the write was made + return ParentRowExists(ends, dependency.Key) + || !RowsHoldingKey(ends.Child, ends.ChildColumns, dependency.Key).Any(); } /// The enforced relationship identifies, as the catalog holds it now; null @@ -249,18 +256,48 @@ private bool ForeignKeyDependencyHolds(ForeignKeyDependency dependency) /// nothing and needs nothing, and a partly null one can never match. private bool ExistingRowsHaveParents(ForeignKey fk) { - Table child = _database.OpenTable(fk.Table); - int[] columns = [.. fk.Columns.Select(c => child.Definition.RequireColumn(c.Column).Index)]; - foreach (object?[] row in child.Rows(child.DecodeOnly(columns))) - { - object?[] key = [.. columns.Select(c => row[c])]; - int nulls = key.Count(v => v is null); - if (nulls == key.Length) continue; - if (nulls > 0 || !ParentRowExists(fk, key)) return false; - } + RelationshipEnds ends = Ends(fk); + foreach (object?[] row in ends.Child.Rows(ends.Child.DecodeOnly(ends.ChildColumns))) + if (ForeignKeyValue(row, ends.ChildColumns, out bool partialNull) is { } key + && (partialNull || !ParentRowExists(ends, key))) + return false; return true; } + /// The foreign key a row holds, on ACE's **MATCH FULL** terms (verified vs ACE, and unlike SQL + /// Server's MATCH SIMPLE): null when every column is null — the row references nothing and the relationship is + /// not applied to it — and when only some are, a key that can never match a full + /// parent key and so breaks the relationship (ACE gives the same "a related record is required" error). + private static object?[]? ForeignKeyValue(object?[] row, int[] columns, out bool partialNull) + { + object?[] key = [.. columns.Select(c => row[c])]; + int nulls = key.Count(v => v is null); + partialNull = nulls > 0 && nulls < key.Length; + return nulls == key.Length ? null : key; + } + + /// A relationship's two tables, and the positions its columns occupy in each one's rows. + private sealed record RelationshipEnds(Table Child, int[] ChildColumns, Table Parent, int[] ParentColumns); + + /// The relationship's two ends as the catalog holds them now. + private RelationshipEnds Ends(ForeignKey fk) + { + Table child = _database.OpenTable(fk.Table); + Table parent = _database.OpenTable(fk.ReferencedTable); + return new RelationshipEnds( + child, [.. fk.Columns.Select(c => child.Definition.RequireColumn(c.Column).Index)], + parent, [.. fk.Columns.Select(c => parent.Definition.RequireColumn(c.ReferencedColumn).Index)]); + } + + /// , once per relationship per statement — what every row the statement writes + /// checks against. A commit-time check resolves them afresh, since later statements may have changed them. + private RelationshipEnds EndsOf(ForeignKey fk) + { + if (!_ends.TryGetValue(fk, out RelationshipEnds? ends)) + _ends[fk] = ends = Ends(fk); + return ends; + } + /// A relationship as the file identifies it rather than by its names: the definition pages of the /// tables it joins and the ids of its columns, which renaming a table or a column leaves alone. Null when one /// of them no longer exists. @@ -309,18 +346,9 @@ public override int GetHashCode() => HashCode.Combine( Relationship, System.Collections.StructuralComparisons.StructuralEqualityComparer.GetHashCode(Key)); } - /// For a self-referencing FK, whether the row's own referenced-column values equal the FK - /// target — i.e. the row points at itself (or at its own composite key), which satisfies the FK. - private bool RowSatisfiesOwnKey(ForeignKey fk, Table table, object?[] values, object?[] target) => - KeyEquals(values, ReferencedColumnsOf(fk, table), target); - - /// Scans the parent table for a row whose referenced columns equal the child key values. - private bool ParentRowExists(ForeignKey fk, object?[] target) - { - Table parent = _database.OpenTable(fk.ReferencedTable); - int[] parentCols = ReferencedColumnsOf(fk, parent); - return RowsHoldingKey(parent, parentCols, target).Any(); - } + /// Whether a parent row holds the key a child row references. + private bool ParentRowExists(RelationshipEnds ends, object?[] key) => + RowsHoldingKey(ends.Parent, ends.ParentColumns, key).Any(); /// The rows of whose hold /// on 's terms, read in full: seeked through an index on those columns when the key @@ -342,36 +370,42 @@ private bool ParentRowExists(ForeignKey fk, object?[] target) return table.RowsWithKey(columns, seekKey, Holds); } - /// The positions the relationship's referenced columns occupy in a row of the parent table. - private static int[] ReferencedColumnsOf(ForeignKey fk, Table parent) => - [.. fk.Columns.Select(c => - (parent.Definition.FindColumn(c.ReferencedColumn) - ?? throw new InvalidOperationException( - $"Column '{c.ReferencedColumn}' does not exist in '{parent.Name}'.")).Index)]; - /// The enforced relationships for which is the referenced /// (parent) side — i.e. those whose child rows a delete/key-update of a parent row must handle. - private IEnumerable ChildRelationshipsOf(string parentTable) => - _database.Catalog.Relationships.Where(r => - r.IsEnforced && string.Equals(r.ReferencedTable, parentTable, StringComparison.OrdinalIgnoreCase)); + private ForeignKey[] ChildRelationshipsOf(string parentTable) + { + if (!_referencesTo.TryGetValue(parentTable, out ForeignKey[]? found)) + _referencesTo[parentTable] = found = [.. _database.Catalog.Relationships.Where(r => + r.IsEnforced && string.Equals(r.ReferencedTable, parentTable, StringComparison.OrdinalIgnoreCase))]; + return found; + } - /// The referenced-column values from a parent row (the key children point at). - private object?[] ReferencedKey(ForeignKey fk, object?[] parentValues) + /// The enforced relationships holds the foreign key of. + private ForeignKey[] ForeignKeysOf(string childTable) { - Table parent = _database.OpenTable(fk.ReferencedTable); - return fk.Columns.Select(c => parentValues[parent.Definition.FindColumn(c.ReferencedColumn)!.Index]).ToArray(); + if (!_foreignKeysOf.TryGetValue(childTable, out ForeignKey[]? found)) + _foreignKeysOf[childTable] = found = [.. _database.Catalog.ForeignKeysOf(childTable).Where(f => f.IsEnforced)]; + return found; + } + + /// , once per relationship per statement. + private RelationshipIdentity? IdentityOf(ForeignKey fk) + { + if (!_identities.TryGetValue(fk, out RelationshipIdentity? identity)) + _identities[fk] = identity = Identify(fk); + return identity; } + /// ACE's refusal to delete a parent row, or change its key, while a child row still holds it. + private static InvalidOperationException RelatedRecords(ForeignKey fk) => + new($"The record cannot be deleted or changed because table '{fk.Table}' includes related records."); + /// Child rows whose FK columns equal . Both callers skip a key with a null /// in it — nothing references one — so a null FK column simply fails the comparison. - private List<(RowId Id, object?[] Values)> FindChildRows(ForeignKey fk, object?[] key) - { - Table child = _database.OpenTable(fk.Table); - int[] childCols = [.. fk.Columns.Select(c => child.Definition.FindColumn(c.Column)!.Index)]; + private List<(RowId Id, object?[] Values)> FindChildRows(RelationshipEnds ends, object?[] key) => // Seeked where the child has an index on its key (ACE gives an enforced relationship one), else found by // a scan of the key; a match is read whole either way, because the cascade rewrites or deletes it. - return [.. RowsHoldingKey(child, childCols, key)]; - } + [.. RowsHoldingKey(ends.Child, ends.ChildColumns, key)]; /// /// Deletes the given root rows and everything ON DELETE CASCADE reaches from them, applying SET NULL and @@ -407,12 +441,17 @@ private void CascadeDelete(Table rootTable, IEnumerable<(RowId Id, object?[] Val } foreach (var (table, id, values) in order) - { - DeleteComplexValues(table, values); - foreach (IndexDef index in table.Definition.RealIndexes) - table.RemoveIndexEntry(index, values, id); - table.Delete(id); - } + DeleteRow(table, id, values); + } + + /// Deletes one row — the values its complex columns hold, its index entries, then the row itself. + /// The one place a row is removed: a statement's own rows, a cascade's, and a complex column's values. + private void DeleteRow(Table table, RowId id, object?[] values) + { + DeleteComplexValues(table, values); + foreach (IndexDef index in table.Definition.RealIndexes) + table.RemoveIndexEntry(index, values, id); + table.Delete(id); } /// @@ -440,11 +479,7 @@ private void DeleteComplexValues(Table table, object?[] values) foreach ((RowId flatId, object?[] flatValues) in flat.RowsWithKey([linkColumn], [recordId], values => values[linkColumn] is { } link && Convert.ToInt32(link, System.Globalization.CultureInfo.InvariantCulture) == recordId).ToList()) - { - foreach (IndexDef index in flat.Definition.RealIndexes) - flat.RemoveIndexEntry(index, flatValues, flatId); - flat.Delete(flatId); - } + DeleteRow(flat, flatId, flatValues); } } @@ -458,17 +493,20 @@ private void EnqueueCascadeChildren(string parentTable, object?[] parentValues, { foreach (ForeignKey fk in ChildRelationshipsOf(parentTable)) { - object?[] key = ReferencedKey(fk, parentValues); + RelationshipEnds ends = EndsOf(fk); + object?[] key = [.. ends.ParentColumns.Select(c => parentValues[c])]; if (key.Any(k => k is null)) continue; // a null parent key is referenced by nobody - var children = FindChildRows(fk, key); + DependOnRelationship(fk, key, + $"Transaction conflict on foreign key '{fk.Name}': a row in '{fk.Table}' was added for the row " + + $"of '{fk.ReferencedTable}' this transaction deleted."); + var children = FindChildRows(ends, key); if (children.Count == 0) continue; if (fk.CascadeDelete) { - Table child = _database.OpenTable(fk.Table); foreach (var (cid, cvals) in children) - if (scheduled.Add((child.Name, cid))) - stack.Push((child, cid, cvals, false)); + if (scheduled.Add((ends.Child.Name, cid))) + stack.Push((ends.Child, cid, cvals, false)); } else if (fk.DeleteSetNull) foreach (var (cid, cvals) in children) @@ -479,12 +517,11 @@ private void EnqueueCascadeChildren(string parentTable, object?[] parentValues, // ("entry not found"). Reachable on a self-referencing table, where the parent and the // child are rows of one table and one DELETE can name both. Skipping costs nothing: the row // and its entries go in a moment either way, and nobody can observe the intermediate null. - if (scheduled.Contains((fk.Table, cid))) continue; - SetChildKey(fk, cid, cvals, newKey: null); + if (scheduled.Contains((ends.Child.Name, cid))) continue; + SetChildKey(ends, cid, cvals, newKey: null); } else - throw new InvalidOperationException( - $"The record cannot be deleted or changed because table '{fk.Table}' includes related records."); + throw RelatedRecords(fk); } } @@ -498,17 +535,20 @@ private void CascadeParentKeyUpdate(string parentTable, object?[] oldValues, obj { foreach (ForeignKey fk in ChildRelationshipsOf(parentTable)) { - object?[] oldKey = ReferencedKey(fk, oldValues); - object?[] newKey = ReferencedKey(fk, newValues); + RelationshipEnds ends = EndsOf(fk); + object?[] oldKey = [.. ends.ParentColumns.Select(c => oldValues[c])]; + object?[] newKey = [.. ends.ParentColumns.Select(c => newValues[c])]; if (oldKey.Any(k => k is null) || KeyEquals(oldKey, columns: null, newKey)) continue; - var children = FindChildRows(fk, oldKey); + DependOnRelationship(fk, oldKey, + $"Transaction conflict on foreign key '{fk.Name}': a row in '{fk.Table}' was added for the key of " + + $"'{fk.ReferencedTable}' this transaction changed."); + var children = FindChildRows(ends, oldKey); if (children.Count == 0) continue; if (fk.CascadeUpdate) - foreach (var (cid, cvals) in children) SetChildKey(fk, cid, cvals, newKey, stillToWrite); + foreach (var (cid, cvals) in children) SetChildKey(ends, cid, cvals, newKey, stillToWrite); else - throw new InvalidOperationException( - $"The record cannot be deleted or changed because table '{fk.Table}' includes related records."); + throw RelatedRecords(fk); } } @@ -526,54 +566,88 @@ private bool KeyEquals(object?[] values, int[]? columns, object?[] key) return true; } - /// Rewrites a child row's FK columns to (or NULL for SET NULL), - /// maintaining any index over them. - private void SetChildKey(ForeignKey fk, RowId childId, object?[] childValues, object?[]? newKey, + /// Rewrites a child row's FK columns to (or NULL for SET NULL). + private void SetChildKey(RelationshipEnds ends, RowId childId, object?[] childValues, object?[]? newKey, Dictionary<(int Tdef, RowId Id), object?[]>? stillToWrite = null) { - Table child = _database.OpenTable(fk.Table); var newValues = (object?[])childValues.Clone(); - var changed = new HashSet(); - for (int i = 0; i < fk.Columns.Count; i++) - { - int idx = child.Definition.FindColumn(fk.Columns[i].Column)!.Index; - object? nv = newKey?[i]; - if (!Equals(nv, childValues[idx])) { newValues[idx] = nv; changed.Add(idx); } - } - if (changed.Count == 0) return; + for (int i = 0; i < ends.ChildColumns.Length; i++) + newValues[ends.ChildColumns[i]] = newKey?[i]; // A child row the running statement has yet to write itself: hand it the new key and let its own write // carry it, so the row it writes and the index entries it moves both hold the cascaded value. Writing // here as well would be undone by that write — it was built before this cascade ran — while the index // entry moved here would keep the new key, which is a row disagreeing with its own index. - if (stillToWrite?.TryGetValue((child.Definition.DefinitionPage, childId), out object?[]? waiting) == true) + if (stillToWrite?.TryGetValue((ends.Child.Definition.DefinitionPage, childId), out object?[]? waiting) == true) { - foreach (int idx in changed) waiting[idx] = newValues[idx]; + foreach (int idx in ends.ChildColumns) + if (!Equals(newValues[idx], childValues[idx])) waiting[idx] = newValues[idx]; return; } - // A cascade rewrites a row, so it owes the row the same invariants an UPDATE does. It used to apply - // none of them: ON DELETE SET NULL would write NULL into a column carrying the Required property, and - // ON UPDATE CASCADE could drive two children onto the same unique key — states the UPDATE path a few - // lines below explicitly refuses, reached by a statement that never names the child table. - // Table.Update carries no enforcement of its own (unlike Insert), so there is no backstop under this. - EnforceRequired(child.Name, child.Definition.Columns, newValues); + // A cascade rewrites a row, so it owes the row everything an UPDATE of it does — and the same code does it. + UpdateRow(ends.Child, childId, childValues, newValues, stillToWrite); + } - foreach (IndexDef index in child.Definition.RealIndexes + /// + /// Rewrites one row whose values now hold — an UPDATE's own row, or a child a cascade + /// reaches — after checking it against every rule an updated row is held to, then applies the ON UPDATE rules + /// of any relationship whose key it changed. The one place a row is rewritten: a cascaded row is checked + /// exactly as a directly updated one, and a cascade that changes a key others reference carries on down. + /// + private void UpdateRow(Table table, RowId id, object?[] original, object?[] values, + Dictionary<(int Tdef, RowId Id), object?[]>? stillToWrite) + { + var changed = new HashSet(); + for (int i = 0; i < values.Length; i++) + if (!Equals(original[i], values[i])) changed.Add(i); + if (changed.Count == 0) + { + stillToWrite?.Remove((table.Definition.DefinitionPage, id)); + return; // unchanged after all + } + + // UPDATE must preserve the same Required/NOT NULL invariant as INSERT. Check the complete + // post-assignment row before any referential action, row rewrite, or index mutation occurs. + EnforceRequired(table.Name, table.Definition.Columns, values); + + // A changed primary-key or WITH DISALLOW NULL column must not become Null, the insert rule. + foreach (IndexDef index in table.Definition.Indexes + .Where(i => (i.IsPrimaryKey || i.Required) && i.Columns.Any(c => changed.Contains(c.Column.Index)))) + if (index.Columns.FirstOrDefault(c => values[c.Column.Index] is null).Column is { } nullColumn) + throw new InvalidOperationException( + $"Index or primary key cannot contain a Null value: column '{nullColumn.Name}' of " + + $"'{table.Name}' is Null, and index '{index.Name}' does not allow it."); + + // Child side: a changed FK column must still reference an existing parent (like an insert). + if (ForeignKeysOf(table.Name).Any(f => EndsOf(f).ChildColumns.Any(changed.Contains))) + EnforceReferentialIntegrity(table.Name, table, values, update: true); + + // A changed UNIQUE/PRIMARY key must not collide with another row (null keys are distinct — a + // unique index permits multiple nulls, so they're skipped, matching the insert rule). + foreach (IndexDef index in table.Definition.RealIndexes .Where(i => i.IsUnique && i.Columns.Any(c => changed.Contains(c.Column.Index)))) - if (!index.Columns.Any(c => newValues[c.Column.Index] is null) && child.HasDuplicateKey(index, newValues, childId)) + if (!index.Columns.Any(c => values[c.Column.Index] is null) && table.HasDuplicateKey(index, values, id)) throw new ConstraintViolationException( - $"Cannot cascade to '{child.Name}': a row with the same " + + $"Cannot update '{table.Name}': a row with the same " + $"{(index.IsPrimaryKey ? "primary key" : "unique key")} already exists (index '{index.Name}').", index.Name, index.IsPrimaryKey); - EnforceCheckConstraints(child.Definition, newValues); + // The updated row must still satisfy every CHECK constraint (evaluated against the full new row). + EnforceCheckConstraints(table.Definition, values); - child.Update(childId, newValues, changed); - foreach (IndexDef index in child.Definition.RealIndexes + table.Update(id, values, changed); + foreach (IndexDef index in table.Definition.RealIndexes .Where(i => i.Columns.Any(c => changed.Contains(c.Column.Index)))) - child.MoveIndexEntry(index, childValues, newValues, childId); + table.MoveIndexEntry(index, original, values, id); + stillToWrite?.Remove((table.Definition.DefinitionPage, id)); + + // Parent side, once the row holds its new key: a changed referenced-key column triggers each relationship's + // ON UPDATE action (CASCADE rewrites the children, NO ACTION rejects if any exist). After the write, so a + // cascaded child's own foreign-key check finds the new key — and a row referencing itself is found as its + // own child and rewritten like any other. + CascadeParentKeyUpdate(table.Name, original, values, stillToWrite); } private int ExecuteCreateIndex(CreateIndexStatement statement) @@ -965,12 +1039,15 @@ private int ExecuteSelectInto(SelectStatement statement) .ToList(); _database.CreateTable(target, specs); + // Written the way every inserted row is, so whatever the new table carries is checked as INSERT checks it. Table table = _database.OpenTable(target); + RowDefaults defaults = DefaultsOf(table.Definition); + var provided = Enumerable.Range(0, specs.Count).ToHashSet(); foreach (object?[] row in rows) { var values = new object?[specs.Count]; Array.Copy(row, values, Math.Min(row.Length, values.Length)); - table.Insert(values); + InsertNewRow(target, table, defaults, values, provided); } if (_session is not null) _session.RowCount = rows.Count; @@ -1918,65 +1995,15 @@ private int Update(UpdateStatement statement) stillToWrite[(table.Definition.DefinitionPage, id)] = values; foreach (var (table, id, original, values) in dirty.Values) - { - var changed = new HashSet(); - for (int i = 0; i < values.Length; i++) - if (!Equals(original[i], values[i])) changed.Add(i); - if (changed.Count == 0) - { - stillToWrite.Remove((table.Definition.DefinitionPage, id)); - continue; // unchanged after all - } - - // UPDATE must preserve the same Required/NOT NULL invariant as INSERT. Check the complete - // post-assignment row before any referential action, row rewrite, or index mutation occurs. - EnforceRequired(table.Name, table.Definition.Columns, values); - - // A changed primary-key or WITH DISALLOW NULL column must not become Null, the insert rule. - foreach (IndexDef index in table.Definition.Indexes - .Where(i => (i.IsPrimaryKey || i.Required) && i.Columns.Any(c => changed.Contains(c.Column.Index)))) - if (index.Columns.FirstOrDefault(c => values[c.Column.Index] is null).Column is { } nullColumn) - throw new InvalidOperationException( - $"Index or primary key cannot contain a Null value: column '{nullColumn.Name}' of " - + $"'{table.Name}' is Null, and index '{index.Name}' does not allow it."); - - // Child side: a changed FK column must still reference an existing parent (like an insert). - if (_database.Catalog.ForeignKeysOf(table.Name).Any(f => f.IsEnforced && - f.Columns.Any(c => changed.Contains(table.Definition.FindColumn(c.Column)!.Index)))) - EnforceReferentialIntegrity(table.Name, table, values, update: true); - - // A changed UNIQUE/PRIMARY key must not collide with another row (null keys are distinct — a - // unique index permits multiple nulls, so they're skipped, matching the insert rule). - foreach (IndexDef index in table.Definition.RealIndexes - .Where(i => i.IsUnique && i.Columns.Any(c => changed.Contains(c.Column.Index)))) - if (!index.Columns.Any(c => values[c.Column.Index] is null) && table.HasDuplicateKey(index, values, id)) - throw new ConstraintViolationException( - $"Cannot update '{table.Name}': a row with the same " + - $"{(index.IsPrimaryKey ? "primary key" : "unique key")} already exists (index '{index.Name}').", - index.Name, - index.IsPrimaryKey); - - // The updated row must still satisfy every CHECK constraint (evaluated against the full new row). - EnforceCheckConstraints(table.Definition, values); - - // Parent side: a changed referenced-key column triggers each relationship's ON UPDATE action - // (CASCADE rewrites children, NO ACTION rejects if children exist). - CascadeParentKeyUpdate(table.Name, original, values, stillToWrite); - - // The cascade may have folded a new key into this very row — a row that references itself — so the - // set of changed columns is re-derived before the row and its index entries are written. - for (int i = 0; i < values.Length; i++) - if (!Equals(original[i], values[i])) changed.Add(i); - - table.Update(id, values, changed); - foreach (IndexDef index in table.Definition.RealIndexes - .Where(i => i.Columns.Any(c => changed.Contains(c.Column.Index)))) - table.MoveIndexEntry(index, original, values, id); - stillToWrite.Remove((table.Definition.DefinitionPage, id)); - } + UpdateRow(table, id, original, values, stillToWrite); + var newRowDefaults = new Dictionary(); foreach (var (values, (table, provided)) in newRows) - InsertNewRow(table.Name, table, DefaultsOf(table.Definition), values, provided); + { + if (!newRowDefaults.TryGetValue(table.Definition.DefinitionPage, out RowDefaults? defaults)) + newRowDefaults[table.Definition.DefinitionPage] = defaults = DefaultsOf(table.Definition); + InsertNewRow(table.Name, table, defaults, values, provided); + } int affected = joinRows.Count; if (_session is not null) _session.RowCount = affected; diff --git a/test/LibRed.Engine.Tests/ReaderWriterIsolationTests.cs b/test/LibRed.Engine.Tests/ReaderWriterIsolationTests.cs index d7f673bd..29a75b1d 100644 --- a/test/LibRed.Engine.Tests/ReaderWriterIsolationTests.cs +++ b/test/LibRed.Engine.Tests/ReaderWriterIsolationTests.cs @@ -152,6 +152,46 @@ public void A_child_insert_cannot_commit_after_its_parent_is_deleted_by_another_ finally { TemporaryDatabase.Delete(path); } } + // The same write skew from the other end: the delete found no children, and another connection's child insert + // found the parent it had not seen deleted. The delete's dependency is re-checked at its commit. + [Fact] + public void A_parent_delete_cannot_commit_after_another_connection_adds_a_child_for_it() + { + string path = CreateDatabase("write-skew-parent-"); + try + { + using var parentDb = JetDatabase.Open(path, readOnly: false); + using var childDb = JetDatabase.Open(path, readOnly: false); + var parent = new QueryEngine(parentDb); + var child = new QueryEngine(childDb); + + parent.ExecuteNonQuery("CREATE TABLE Parents (Id LONG PRIMARY KEY)"); + parent.ExecuteNonQuery( + "CREATE TABLE Children (Id LONG PRIMARY KEY, ParentId LONG REFERENCES Parents (Id))"); + parent.ExecuteNonQuery("INSERT INTO Parents (Id) VALUES (1)"); + parent.ExecuteNonQuery("INSERT INTO Parents (Id) VALUES (2)"); + + parent.ExecuteNonQuery("BEGIN TRANSACTION"); + parent.ExecuteNonQuery("DELETE FROM Parents WHERE Id = 1"); // no children yet + + child.ExecuteNonQuery("INSERT INTO Children (Id, ParentId) VALUES (10, 1)"); // ... until now + + var conflict = Assert.Throws(() => parent.ExecuteNonQuery("COMMIT")); + Assert.Contains("was added for the row", conflict.Message, StringComparison.Ordinal); + + parent.ExecuteNonQuery("ROLLBACK"); + Assert.Equal(2, child.ExecuteQuery("SELECT Id FROM Parents").Rows.Count()); + + // Specific to the key: a child added for a DIFFERENT parent does not hold up the delete. + parent.ExecuteNonQuery("BEGIN TRANSACTION"); + parent.ExecuteNonQuery("DELETE FROM Parents WHERE Id = 2"); + child.ExecuteNonQuery("INSERT INTO Children (Id, ParentId) VALUES (11, 1)"); + parent.ExecuteNonQuery("COMMIT"); + Assert.Single(child.ExecuteQuery("SELECT Id FROM Parents").Rows); + } + finally { TemporaryDatabase.Delete(path); } + } + // The dependency is on the parent being needed, not on it having been checked: once the child has moved to // another parent, the one it was inserted against can go, and the commit has nothing to object to. [Fact] diff --git a/test/LibRed.Engine.Tests/ReferentialActionTests.cs b/test/LibRed.Engine.Tests/ReferentialActionTests.cs index 862db6bd..60cee541 100644 --- a/test/LibRed.Engine.Tests/ReferentialActionTests.cs +++ b/test/LibRed.Engine.Tests/ReferentialActionTests.cs @@ -232,4 +232,94 @@ public void Cascade_refuses_to_create_a_duplicate_child_key() Assert.ThrowsAny(() => e.ExecuteNonQuery("UPDATE P SET Id = 2 WHERE Id = 1")); }); } + + // SET NULL may not null a primary-key column either — the same rule an UPDATE of the child meets. + [Fact] + public void Set_null_refuses_to_null_a_primary_key_column() + { + string path = Fresh(); + try + { + using var db = JetDatabase.Open(path, readOnly: false); + var e = new QueryEngine(db); + e.ExecuteNonQuery("CREATE TABLE P (Id long PRIMARY KEY)"); + e.ExecuteNonQuery("CREATE TABLE C (ParentId long, X long, CONSTRAINT PK_C PRIMARY KEY (ParentId, X), " + + "CONSTRAINT FK_C FOREIGN KEY (ParentId) REFERENCES P (Id) ON DELETE SET NULL)"); + e.ExecuteNonQuery("INSERT INTO P (Id) VALUES (1)"); + e.ExecuteNonQuery("INSERT INTO C (ParentId, X) VALUES (1, 1)"); + + var error = Assert.Throws(() => e.ExecuteNonQuery("DELETE FROM P WHERE Id = 1")); + Assert.Contains("cannot contain a Null value", error.Message, StringComparison.Ordinal); + Assert.Equal(1, Convert.ToInt32(e.ExecuteQuery("SELECT ParentId FROM C").Rows.Single()[0])); + } + finally { TemporaryDatabase.Delete(path); } + } + + // A child whose key a cascade rewrites is a parent in its own right: A -> B -> C, each ON UPDATE CASCADE over + // B's unique Aid, carries A's new key all the way down. + [Fact] + public void Cascade_update_continues_down_a_chain() + { + string path = Fresh(); + try + { + using var db = JetDatabase.Open(path, readOnly: false); + var e = ChainOf(db, " ON UPDATE CASCADE", " ON UPDATE CASCADE"); + + e.ExecuteNonQuery("UPDATE A SET Id = 5"); + + Assert.Equal(5, Convert.ToInt32(e.ExecuteQuery("SELECT Aid FROM B").Rows.Single()[0])); + Assert.Equal(5, Convert.ToInt32(e.ExecuteQuery("SELECT Baid FROM C").Rows.Single()[0])); + } + finally { TemporaryDatabase.Delete(path); } + } + + // ON DELETE SET NULL changes the child's key, so the rows referencing THAT key follow their own rule: a + // cascade takes them to null too, and NO ACTION refuses — neither leaves a row pointing at a key that is gone. + [Fact] + public void Set_null_onto_a_referenced_key_cascades_to_its_children() + { + string path = Fresh(); + try + { + using var db = JetDatabase.Open(path, readOnly: false); + var e = ChainOf(db, " ON DELETE SET NULL", " ON UPDATE CASCADE"); + + e.ExecuteNonQuery("DELETE FROM A"); + + Assert.Null(e.ExecuteQuery("SELECT Aid FROM B").Rows.Single()[0]); + Assert.Null(e.ExecuteQuery("SELECT Baid FROM C").Rows.Single()[0]); + } + finally { TemporaryDatabase.Delete(path); } + } + + [Fact] + public void Set_null_onto_a_referenced_key_with_no_action_children_is_refused() + { + string path = Fresh(); + try + { + using var db = JetDatabase.Open(path, readOnly: false); + var e = ChainOf(db, " ON DELETE SET NULL", ""); + + var error = Assert.Throws(() => e.ExecuteNonQuery("DELETE FROM A")); + Assert.Contains("table 'C' includes related records", error.Message, StringComparison.Ordinal); + Assert.Equal(1, Convert.ToInt32(e.ExecuteQuery("SELECT Aid FROM B").Rows.Single()[0])); + } + finally { TemporaryDatabase.Delete(path); } + } + + // A(Id) <- B(Aid, unique) <- C(Baid), one row each, all holding key 1. + private static QueryEngine ChainOf(JetDatabase db, string bRule, string cRule) + { + var e = new QueryEngine(db); + e.ExecuteNonQuery("CREATE TABLE A (Id long PRIMARY KEY)"); + e.ExecuteNonQuery($"CREATE TABLE B (Id long PRIMARY KEY, Aid long, CONSTRAINT FK_B FOREIGN KEY (Aid) REFERENCES A (Id){bRule})"); + e.ExecuteNonQuery("CREATE UNIQUE INDEX UX_B ON B (Aid)"); + e.ExecuteNonQuery($"CREATE TABLE C (Id long PRIMARY KEY, Baid long, CONSTRAINT FK_C FOREIGN KEY (Baid) REFERENCES B (Aid){cRule})"); + e.ExecuteNonQuery("INSERT INTO A (Id) VALUES (1)"); + e.ExecuteNonQuery("INSERT INTO B (Id, Aid) VALUES (1, 1)"); + e.ExecuteNonQuery("INSERT INTO C (Id, Baid) VALUES (1, 1)"); + return e; + } } From 82ef0145a26a254d70d7dd2cb9609f621c9d7a6e Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Thu, 1 Oct 2026 01:24:07 +0800 Subject: [PATCH 86/88] Bring the docs in line with the code CLAUDE.md and AGENTS.md: LibRed.Ado.Tests and LibRed.EFCore.Tests use no driver and only run in the Windows ACE job; four functional-suite shards, each run up to three times; pass-lists exist for the two ACE 2010 x86 legs only, and auto_commit extends them after a successful PR run; both connection env vars; fixed order and the en-US culture lock apply to the EF functional suites; two test projects are MSTest; dotnet test builds only net11.0; the Northwind.accdb and LibRed.Shared consumers; page locks are process-local. AGENTS.md also catches up on LibRed.Core.AccessTests, the AccessTests split, the deferred-write overlay and the Core type guard. The LibRed README's "Not yet" lists only open work: stored action queries other than crosstab, pass-through and UNION-kind are written and run, HAVING views work, the 1:1 flag and ValidationRule are read, a column-level CHECK is dropped, RENAME INDEX throws. The referential-integrity and SQL statement descriptions, LOG(base, x), the SQL-mode readers and the Jet3Format stub are corrected. transactions.md gains the parent-side commit check and marks the undo-log design as superseded; the package readme, format index, benchmark and JetLockTrace docs and copilot-instructions are corrected too. LibRed.Engine.Tests no longer suppresses CA1416: it has no ACE code, and the analyzer is what keeps a cross-platform suite that way. Co-Authored-By: Claude Opus 5.5 --- .github/copilot-instructions.md | 2 +- .github/workflows/push.yml | 2 +- AGENTS.md | 93 ++++++++++----- CLAUDE.md | 63 ++++++---- docs/README.md | 9 +- src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 | 5 +- src/LibRed/PackageReadme.md | 17 +-- src/LibRed/README.md | 74 +++++++----- src/LibRed/docs/design/transactions.md | 108 +++++++++++------- src/LibRed/docs/format/README.md | 8 +- test/LibRed.Benchmarks/Harness/Corpus.cs | 5 +- test/LibRed.Benchmarks/Program.cs | 3 +- test/LibRed.Benchmarks/README.md | 9 +- .../LibRed.Engine.Tests.csproj | 4 +- tools/JetLockTrace/README.md | 6 +- 15 files changed, 254 insertions(+), 154 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9be132d5..6ed0b0e9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,4 +4,4 @@ - This project (EntityFrameworkCore.Jet) targets the Microsoft Access Jet/ACE database engine, not SQL Server. Generated SQL, type mappings, and literals must be Jet/ACE-compliant (e.g. decimal/currency instead of bigint, #...# date literals, TIMEVALUE()). Note: Access 2016+ (ACE) does have a native BIGINT (Large Number) type, which is a SIGNED 64-bit integer and cannot hold unsigned values like ulong.MaxValue; unsigned ulong/uint should map to decimal(20,0) to avoid overflow. Provider distinctions are OLE DB / ODBC via the ACE/Jet driver rather than SqlClient. Do not assume SQL Server/T-SQL semantics. - The Jet/Access SQL dialect does not support COALESCE or NZ. When rewriting queries for Jet, prefer IIF( IS NULL, , ) instead. - That rule is about the **Jet dialect**, not the whole repository. This repo also contains LibRed (`src/LibRed`), a from-scratch managed Jet/ACE engine, and its EF Core provider has two SQL modes. In `LibRedSqlMode.Compatible` the rule above applies, because the SQL is generated to also run against ACE. In `LibRedSqlMode.Extended` (the default) it does not: LibRed owns the engine that parses the SQL, so the generator deliberately emits standard SQL that ACE has no syntax for — COALESCE, CASE, NULLIF, CROSS/OUTER APPLY, window functions, OFFSET/FETCH paging and FULL OUTER JOIN. Do not "fix" those back into Jet workarounds; extended mode exists precisely to remove them. -- Deferred enhancement for EntityFrameworkCore.Jet: add ACE engine-version detection so that on Access 2016+ the provider can map to native BIGINT (signed long; unsigned ulong/uint still go to decimal(20,0)) and native DATETIME2, while falling back to decimal(20,0)/legacy datetime on older ACE versions. Not a priority right now. The gap is in the **provider's type mapping**, not in the engine: ACE itself accepts both types, and LibRed already reads, writes and indexes them, gating on the on-disk format version and raising a file's version byte rather than refusing the DDL. Caveat before treating this as a straightforward port: DATETIME2 through Jet/ACE is badly behaved, so it needs its own investigation rather than being mapped by analogy with BIGINT. The asymmetry between the two providers is deliberate, so do not assume either side behaves like the other. \ No newline at end of file +- Deferred enhancement for EntityFrameworkCore.Jet: add ACE engine-version detection so that the provider can map to native BIGINT on Access 2016+ (signed long; unsigned ulong/uint still go to decimal(20,0)) and native DATETIME2 on Access 2019+ — two different thresholds, not one — while falling back to decimal(20,0)/legacy datetime on older ACE versions. Not a priority right now. The gap is in the **provider's type mapping**, not in the engine: ACE itself accepts both types, and LibRed already reads, writes and indexes them, gating on the on-disk format version and raising a file's version byte rather than refusing the DDL. Caveat before treating this as a straightforward port: DATETIME2 through Jet/ACE is badly behaved, so it needs its own investigation rather than being mapped by analogy with BIGINT. The asymmetry between the two providers is deliberate, so do not assume either side behaves like the other. \ No newline at end of file diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 467dbf45..7ecc936b 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -118,7 +118,7 @@ jobs: - 'test/EFCore.LibRed.FunctionalTests/**' - 'test/EFCore.LibRed.Extended.FunctionalTests/**' - 'src/EFCore.Jet.Common/**' - # Northwind.accdb lives here and every LibRed suite links to it as its fixture. + # Northwind.accdb lives here and every LibRed.* test project links to it as its fixture. - 'test/JetProviderExceptionTests/**' - name: 'Decide What To Run' id: Decide diff --git a/AGENTS.md b/AGENTS.md index 6e58ede9..134ac1f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,12 +7,13 @@ It is kept in sync with `CLAUDE.md`; if you change guidance in one, check whethe 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.0-alpha.4` (`Version.props`) targeting EF Core 11 and `net11.0`; `global.json` pins the 11.0.100 RC1 SDK with `rollForward: latestFeature`. The test projects use **xunit v3**. +Current version: `11.0.0-alpha.4` (`Version.props`) targeting EF Core 11 and `net11.0`; `global.json` pins the 11.0.100 RC1 SDK with `rollForward: latestFeature`. The test projects use **xunit v3**, except `EFCore.Jet.Data.Tests` and `EFCore.Jet.IntegrationTests`, which use MSTest. `LibRed.Core`, `LibRed.Sql`, `LibRed.Engine` and `LibRed.Ado` are the exception: they multi-target `$(LibRedTargetFrameworks)` = `net10.0;net11.0`, since none of them depends on an EF Core package. `LibRed.EFCore` -does, so it stays `net11.0`. Every test project is single-target `net11.0` deliberately: the `net10.0` leg is -compiled but never run, so tests stay one run per suite and CI needs no second runtime. +does, so it stays `net11.0`. Every test project is single-target `net11.0` (the Jet ones as `net11.0-windows7.0`, +via `$(JetTestWindowsOnlyTargetFramework)`) deliberately: the `net10.0` leg is compiled but never run, so tests stay +one run per suite and CI needs no second runtime. ### Which layer am I touching? @@ -51,14 +52,23 @@ To develop against a local EF Core build instead of NuGet packages, copy `Develo ## Tests -**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: +**Jet** tests require a real Microsoft Access driver installed (ODBC or OLE DB) and an actual `.accdb` file — no mocks. The EF suites' connection strings (the LibRed ones included, which need no driver) are 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.Jet.Tests/config.json` (ODBC `DBQ=` form) - `test/EFCore.LibRed.FunctionalTests/config.json` and `test/EFCore.LibRed.Extended.FunctionalTests/config.json` (LibRed connection, one per SQL mode) -- Or env var `EFCoreJet_DefaultConnection` +- Or env var `EFCoreJet_DefaultConnection` (the Jet suite) / `EFCoreLibRed_DefaultConnection` (both LibRed + suites) -**LibRed** tests split in two: `LibRed.Engine.Tests`, `EFCore.LibRed.FunctionalTests` and `EFCore.LibRed.Extended.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. +**LibRed** tests split in two: `LibRed.Core.Tests`, `LibRed.Engine.Tests`, `EFCore.LibRed.FunctionalTests` and `EFCore.LibRed.Extended.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.AccessTests` and `LibRed.Engine.AccessTests` deliberately cross-check LibRed's output against the real engine over OLE DB, so they need Windows + ACE. `LibRed.Ado.Tests` and `LibRed.EFCore.Tests` use no driver (no OLE DB, no DAO), but CI only runs them in the Windows ACE job for now. + +> **The `*.AccessTests` split is by which engine a test needs, not by subject.** A file-format test belongs in +> `LibRed.Core.Tests` if LibRed alone can decide the answer, and in `LibRed.Core.AccessTests` if ACE has to be +> asked — the two halves share a namespace and their helpers (`test/LibRed.Shared/`), so moving a test between +> them is a file move and nothing else. The ACE half runs serially; the plain half runs **in parallel** and +> takes seconds, which is why adding an ACE dependency to it costs more than it looks. Note the dependency is +> not always visible in a name or a `using`: three DAO probes reach ACE through `Type.GetTypeFromProgID`, and +> were only caught because the plain project does not suppress `CA1416`. Leave that suppression off. **Run all tests** (requires x86 or x64 matching your driver bitness): @@ -89,7 +99,8 @@ is the most common way to waste minutes here. **When you do run 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. -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`). +The three EF functional suites run in **fixed order by default** (`FIXED_TEST_ORDER` compile constant, set unless `-p:FixedTestOrder=false`; see `test/Directory.Build.props` — every project gets the constant, but only those suites and the empty +`EFCore.Jet.Tests` act on it) and lock culture to `en-US` via a module initializer (`test/Shared/ModuleInitializer.cs`, compiled into those suites only). The other test projects set culture themselves where it matters, and `LibRed.Core.Tests` runs its collections in parallel. Tests that require features Jet doesn't support are skipped with a reason on the test. @@ -99,13 +110,18 @@ Tests that require features Jet doesn't support are skipped with a reason on the `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. The two `EFCore.LibRed*.FunctionalTests` suites are `continue-on-error` -for now — they still run on every push, but their remaining failures don't block. +previously for that matrix leg. Only two legs have one today — `ace_2010_odbc_x86` and `ace_2010_oledb_x86` — and +the check runs only where the file exists, so the other six legs are not gated. CI merges the shards' `.trx` +files and **fails if any listed test stops passing**; after a successful pull-request run, newly-passing tests are +appended to an existing list and pushed back by the `auto_commit` workflow (it is triggered by +`pull_request.yml`, not by `push.yml`, and never creates a list). 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 four shards (query core / Northwind+GearsOfWar / non-query without +CompiledModel / CompiledModel) and runs a shard up to three times if the runner crashes (on a leg with a +pass-list, a shard that crashes all three times fails it). The two `EFCore.LibRed*.FunctionalTests` suites are +`continue-on-error` for now — they run on every push that touches LibRed, but their remaining failures don't +block. ### Docker images @@ -138,20 +154,30 @@ test/ 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] + which every LibRed.* test project and LibRed.Benchmarks + link to as their fixture (the EF functional suites + build theirs from test/Northwind.sql) [Windows + ACE] + LibRed.Core.Tests/ File-format read/write through LibRed alone [cross-platform] + LibRed.Core.AccessTests/ File-format tests 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.Ado.Tests/ ADO.NET surface [no driver; CI's Windows ACE job] LibRed.EFCore.Tests/ LibRed EF Core provider: query round-trip, - database-first scaffolding [Windows + ACE] + database-first scaffolding [no driver; CI's Windows ACE job] + LibRed.Shared/ Helpers compiled into the Core/Engine test pairs and LibRed.Ado.Tests + (TemporaryDatabase, …; AceTestDatabase into the AccessTests only) EFCore.LibRed.FunctionalTests/ EF Core specification suite over LibRed, compatible SQL mode (Jet-dialect SQL) [cross-platform] EFCore.LibRed.Extended.FunctionalTests/ The same suite in extended SQL mode; its own baselines, because the SQL differs [cross-platform] - LibRed.Benchmarks/ BenchmarkDotNet harness (not a test project) - Shared/ ModuleInitializer.cs — locks culture to en-US + LibRed.Benchmarks/ BenchmarkDotNet harness over the engine directly — no EF, no + ADO. Suites for end-to-end queries, the parse/plan/execute + split, storage, writes, DDL, catalog, and an opt-in ACE + head-to-head. `-- --validate` checks the SQL corpus. + See its README.md [cross-platform] + Shared/ For the EF functional suites: ModuleInitializer.cs (locks culture to + en-US) and TestUtilities/ (test orderers, crash detection, conditions) tools/ sortkey-table/ Generates the Windows NLS sort-weight table LibRed's index keys use @@ -270,7 +296,10 @@ the names are `JetVersion` / `RequiredVersion` / `EnsureFormatAtLeast`: `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. + (read-only database) fails loudly instead of writing a column Access couldn't read. That guard is on the SQL + path; the same rule is enforced again in Core over `JetDataType` (`JetDataTypeVersions.EnsureStorable`, called + from `TdefBuilder` and `TableCreator`), because every `JetDatabase` method that defines a column takes a raw `ColumnSpec` and + would otherwise write the descriptor with nothing objecting. - `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; @@ -297,7 +326,9 @@ Jet 4 / ACE format, **split one file per page type** (plus cross-cutting topics) 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.) 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` +`docs/design/transactions.md` covers the deferred-write page overlay, +`docs/design/index-key-checksum.md` records how the index-key truncation checksum was pinned down (and why the +rule it replaced looked verified for years), 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 @@ -317,9 +348,12 @@ parser: the lexer/parser are pre-generated and committed under `LibRed.Sql/Gramm `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. +> EF's own test infra keeps a store connection open), but there is no lock file and no cross-process page/record locking (its page locks are process-local), so +> two concurrent writers corrupt the file. Transactions use a **deferred-write overlay** per `PageChannel`, not +> an undo log: writes buffer until commit and publish under a lock, so a reader cannot see another handle's +> uncommitted pages and a rollback cannot discard another channel's committed work, and a stale writer fails +> commit with a write-conflict error. 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 @@ -339,7 +373,9 @@ and via `git grep`/`git cat-file`, which read the same way but slip past the rea 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. +commands are the expected shell usage. Run `LibRed.Core.AccessTests` (~7 min) and `LibRed.Engine.AccessTests` +(~1.5 min) with a `--filter` naming the tests that cover the change, or in the background — Claude Code's hooks +refuse either suite whole in the foreground. ## CI @@ -351,7 +387,8 @@ flags — `jet` and `libred` — and skips the jobs that don't apply: partway through the largest shard. - **LibRed** — `LibRed.Engine.Tests` and `LibRed.Core.Tests` on Linux/Windows/macOS + ubuntu-arm/windows-arm, no ACE anywhere. -- **LibRedAccess** — the ACE cross-check suites on `windows-latest` with ACE 2016. +- **LibRedAccess** — the ACE cross-check suites on `windows-latest` with ACE 2016, plus `LibRed.Ado.Tests` and + `LibRed.EFCore.Tests`, which need no driver but run only there for now. - **LibRedFunctional** — both `EFCore.LibRed.FunctionalTests` (compatible mode) and `EFCore.LibRed.Extended.FunctionalTests` (extended mode) on the five-platform matrix, `continue-on-error`. - **NuGet** — packs for `master`, `*-servicing`, `*-wip` and release tags, attaching the packages to the run diff --git a/CLAUDE.md b/CLAUDE.md index 15aa3107..9291b971 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,16 +6,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 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.0-alpha.4` (`Version.props`) targeting EF Core 11 and `net11.0`; `global.json` pins the 11.0.100 RC1 SDK with `rollForward: latestFeature`. The test projects use **xunit v3**. +Current version: `11.0.0-alpha.4` (`Version.props`) targeting EF Core 11 and `net11.0`; `global.json` pins the 11.0.100 RC1 SDK with `rollForward: latestFeature`. The test projects use **xunit v3**, except `EFCore.Jet.Data.Tests` and `EFCore.Jet.IntegrationTests`, which use MSTest. **One exception to `net11.0`:** `LibRed.Core`, `LibRed.Sql`, `LibRed.Engine` and `LibRed.Ado` multi-target `$(LibRedTargetFrameworks)` = `net10.0;net11.0`, because none of them depends on an EF Core package. `LibRed.EFCore` is deliberately **not** in that list — EF Core 11 is `net11.0`-only — and neither is anything under `src/EFCore.Jet*`. -**Every test project stays single-target `net11.0`, and that is deliberate — don't "fix" it.** The `net10.0` +**Every test project stays single-target `net11.0` (the Jet ones as `net11.0-windows7.0`, via +`$(JetTestWindowsOnlyTargetFramework)`), and that is deliberate — don't "fix" it.** The `net10.0` leg is *compiled*, never run: multi-targeting the suites would double every run locally and on all five CI platforms, and would need the .NET 10 runtime installed in CI, which the SDK `global.json` pins does not carry. -So `dotnet test` on any suite builds both frameworks and runs only `net11.0`. +A solution build (`dotnet build EFCore.Jet.sln`) or a pack compiles the `net10.0` leg; `dotnet test` on a +single suite builds only the `net11.0` leg it runs. ### Which layer am I touching? @@ -54,14 +56,15 @@ To develop against a local EF Core build instead of NuGet packages, copy `Develo ## Tests -**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: +**Jet** tests require a real Microsoft Access driver installed (ODBC or OLE DB) and an actual `.accdb` file — no mocks. The EF suites' connection strings (the LibRed ones included, which need no driver) are 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.Jet.Tests/config.json` (ODBC `DBQ=` form) - `test/EFCore.LibRed.FunctionalTests/config.json` and `test/EFCore.LibRed.Extended.FunctionalTests/config.json` (LibRed connection, one per SQL mode) -- Or env var `EFCoreJet_DefaultConnection` +- Or env var `EFCoreJet_DefaultConnection` (the Jet suite) / `EFCoreLibRed_DefaultConnection` (both LibRed + suites) -**LibRed** tests split in two: `LibRed.Core.Tests`, `LibRed.Engine.Tests`, `EFCore.LibRed.FunctionalTests` and `EFCore.LibRed.Extended.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.AccessTests`, `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. +**LibRed** tests split in two: `LibRed.Core.Tests`, `LibRed.Engine.Tests`, `EFCore.LibRed.FunctionalTests` and `EFCore.LibRed.Extended.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.AccessTests` and `LibRed.Engine.AccessTests` deliberately cross-check LibRed's output against the real engine over OLE DB, so they need Windows + ACE. `LibRed.Ado.Tests` and `LibRed.EFCore.Tests` use no driver (no OLE DB, no DAO), but CI only runs them in the Windows ACE job for now. > **The `*.AccessTests` split is by which engine a test needs, not by subject.** A file-format test belongs in > `LibRed.Core.Tests` if LibRed alone can decide the answer, and in `LibRed.Core.AccessTests` if ACE has to be @@ -100,7 +103,8 @@ is the most common way to waste minutes here. **When you do run 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. -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`). +The three EF functional suites run in **fixed order by default** (`FIXED_TEST_ORDER` compile constant, set unless `-p:FixedTestOrder=false`; see `test/Directory.Build.props` — every project gets the constant, but only those suites and the empty +`EFCore.Jet.Tests` act on it) and lock culture to `en-US` via a module initializer (`test/Shared/ModuleInitializer.cs`, compiled into those suites only). The other test projects set culture themselves where it matters, and `LibRed.Core.Tests` runs its collections in parallel. Tests that require features Jet doesn't support are skipped with a reason on the test. @@ -110,13 +114,18 @@ Tests that require features Jet doesn't support are skipped with a reason on the `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 +previously for that matrix leg. Only two legs have one today — `ace_2010_odbc_x86` and `ace_2010_oledb_x86` — and +the check runs only where the file exists, so the other six legs are not gated. CI merges the shards' `.trx` +files and **fails if any listed test stops passing**; after a successful pull-request run, newly-passing tests are +appended to an existing list and pushed back by the `auto_commit` workflow (it is triggered by +`pull_request.yml`, not by `push.yml`, and never creates a list). 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. The two `EFCore.LibRed*.FunctionalTests` suites are `continue-on-error` -for now — they still run on every push, but their remaining failures don't block. +CI splits the functional suite into four shards (query core / Northwind+GearsOfWar / non-query without +CompiledModel / CompiledModel) and runs a shard up to three times if the runner crashes (on a leg with a +pass-list, a shard that crashes all three times fails it). The two `EFCore.LibRed*.FunctionalTests` suites are +`continue-on-error` for now — they run on every push that touches LibRed, but their remaining failures don't +block. ### Docker images @@ -149,14 +158,18 @@ test/ 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] + which every LibRed.* test project and LibRed.Benchmarks + link to as their fixture (the EF functional suites + build theirs from test/Northwind.sql) [Windows + ACE] LibRed.Core.Tests/ File-format read/write through LibRed alone [cross-platform] LibRed.Core.AccessTests/ File-format tests 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.Ado.Tests/ ADO.NET surface [no driver; CI's Windows ACE job] LibRed.EFCore.Tests/ LibRed EF Core provider: query round-trip, - database-first scaffolding [Windows + ACE] + database-first scaffolding [no driver; CI's Windows ACE job] + LibRed.Shared/ Helpers compiled into the Core/Engine test pairs and LibRed.Ado.Tests + (TemporaryDatabase, …; AceTestDatabase into the AccessTests only) EFCore.LibRed.FunctionalTests/ EF Core specification suite over LibRed, compatible SQL mode (Jet-dialect SQL) [cross-platform] EFCore.LibRed.Extended.FunctionalTests/ @@ -167,7 +180,8 @@ test/ split, storage, writes, DDL, catalog, and an opt-in ACE head-to-head. `-- --validate` checks the SQL corpus. See its README.md [cross-platform] - Shared/ ModuleInitializer.cs — locks culture to en-US + Shared/ For the EF functional suites: ModuleInitializer.cs (locks culture to + en-US) and TestUtilities/ (test orderers, crash detection, conditions) tools/ sortkey-table/ Generates the Windows NLS sort-weight table LibRed's index keys use @@ -288,7 +302,7 @@ the names are `JetVersion` / `RequiredVersion` / `EnsureFormatAtLeast`: - `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. That guard is on the SQL path; the same rule is enforced again in Core over `JetDataType` (`JetDataTypeVersions.EnsureStorable`, called - from `TdefBuilder` and `TableCreator`), because every `JetDatabase` DDL method takes a raw `ColumnSpec` and + from `TdefBuilder` and `TableCreator`), because every `JetDatabase` method that defines a column takes a raw `ColumnSpec` and would otherwise write the descriptor with nothing objecting. - `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 @@ -338,7 +352,7 @@ parser: the lexer/parser are pre-generated and committed under `LibRed.Sql/Gramm `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 and no page/record locking, so +> EF's own test infra keeps a store connection open), but there is no lock file and no cross-process page/record locking (its page locks are process-local), so > two concurrent writers corrupt the file. Transactions use a **deferred-write overlay** per `PageChannel`, not > an undo log: writes buffer until commit and publish under a lock, so a reader cannot see another handle's > uncommitted pages and a rollback cannot discard another channel's committed work — both hazards the older @@ -358,7 +372,7 @@ was written**, not what is true now. `.claude/settings.json` installs `PreToolUse` hooks that **deny** three 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/`. + use `Read`, `Grep`, `Glob` instead. Shell text tools stay allowed on paths containing `scratchpad`, `.log`, `$log` or `/tmp/`. `git grep` and `git cat-file` are denied with them: read-only `git` is otherwise pre-allowed, which made them a way to spell the same read and skip the prompt. `git diff`/`log`/`show` stay allowed for reviewing history. - **Editing source files through the shell** (`sed -i`, redirects/`tee` into `.cs`/`.md`/`.csproj`/`.props`/`.json`/ @@ -368,8 +382,10 @@ was written**, not what is true now. ~7 and ~1.5 minutes, and waiting on them is dead session time. Add a `--filter` naming the tests that cover the change, or pass `run_in_background` and carry on working while they run. The other suites are unaffected. -`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. +`dotnet build`/`test`/`restore` and, in Bash, `git status`/`diff`/`log`/`show`/`branch`/`add`/`commit`/`push` +are pre-allowed, and PowerShell is allowed wholesale — so don't work around the hooks; the denial message is +telling you which tool to use, not that the action is forbidden. A `PostToolUse` hook also rewrites every file +the `Write` tool writes to CRLF line endings. ## CI @@ -381,7 +397,8 @@ flags — `jet` and `libred` — and skips the jobs that don't apply: partway through the largest shard. - **LibRed** — `LibRed.Engine.Tests` and `LibRed.Core.Tests` on Linux/Windows/macOS + ubuntu-arm/windows-arm, no ACE anywhere. -- **LibRedAccess** — the ACE cross-check suites on `windows-latest` with ACE 2016. +- **LibRedAccess** — the ACE cross-check suites on `windows-latest` with ACE 2016, plus `LibRed.Ado.Tests` and + `LibRed.EFCore.Tests`, which need no driver but run only there for now. - **LibRedFunctional** — both `EFCore.LibRed.FunctionalTests` (compatible mode) and `EFCore.LibRed.Extended.FunctionalTests` (extended mode) on the five-platform matrix, `continue-on-error`. - **NuGet** — packs for `master`, `*-servicing`, `*-wip` and release tags, attaching the packages to the run diff --git a/docs/README.md b/docs/README.md index aedbc8c5..1716e849 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,6 +17,8 @@ The major version corresponds to the major version of EF Core (i.e. EFCore.Jet `3.x` is compatible with EF Core `3.y`). It runs on Windows operating systems only and can be used with either ODBC or OLE DB together with their respective Access Database driver/provider. +(`EntityFrameworkCore.Jet.Common`, which carries this readme too, is the exception: it holds the Jet SQL dialect +shared with the LibRed provider, and is cross-platform.) ## Requirements @@ -34,8 +36,8 @@ The provider works with Microsoft Access `MDB` and `ACCDB` database files. from-scratch, fully managed implementation of the Jet/ACE engine that also lives in this repository. It reads and writes `MDB`/`ACCDB` files **directly** - no ODBC, OLE DB, DAO or ADOX - so **none of the requirements above apply to it**: no Windows, no installed Access driver, and no need to match your process architecture to -one. It runs on Linux, macOS and ARM64, which CI proves by running its suites on all five platforms with no -Access engine installed anywhere. +one. It runs on Linux, macOS and ARM64, which CI proves by running its engine, file-format and EF Core +specification suites on all five platforms with no Access engine installed anywhere. It reads and writes real database files, creates them from nothing (no DAO, no template file), runs SQL end to end, and an EF Core `DbContext` round-trips through it. The on-disk format it depends on is documented and @@ -130,7 +132,8 @@ All official releases are available on [nuget.org](https://www.nuget.org/package ### CI Builds -There is no CI package feed. Every build of `master` packs the projects and attaches them to its workflow +There is no CI package feed. Every build of `master`, of a `*-servicing` or `*-wip` branch, and of a release +tag packs the projects and attaches them to its workflow run as the `nupkgs` artifact, which can be downloaded from the run's page on GitHub and restored from a local folder. The artifact holds optimized `Release` packages and unoptimized `Debug` ones, both with PDBs. All packages use SourceLink. diff --git a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 index e9d8cd16..8f3098c5 100644 --- a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 +++ b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 @@ -213,8 +213,9 @@ tableConstraint | (CONSTRAINT name=identifier)? CHECK LPAREN checkBody RPAREN # CheckTableConstraint ; -// A CHECK expression is parsed as balanced-paren token soup and ignored (not enforced yet), so any -// expression ACE accepts parses without needing full expression support. +// A CHECK expression is parsed as balanced-paren token soup and kept as its original text, so any +// expression ACE accepts parses without needing full expression support. A table-level CHECK's text is +// stored and enforced (parsed properly when evaluated); a column-level one is dropped by AstBuilder. checkBody : ( ~(LPAREN | RPAREN) | LPAREN checkBody RPAREN )* ; // ON UPDATE / ON DELETE may appear in either order (Access documents UPDATE-then-DELETE; EF Core diff --git a/src/LibRed/PackageReadme.md b/src/LibRed/PackageReadme.md index 154333ff..8c4de3ee 100644 --- a/src/LibRed/PackageReadme.md +++ b/src/LibRed/PackageReadme.md @@ -5,8 +5,9 @@ Access `.mdb` and `.accdb` files. LibRed reads, writes and creates those files **directly**. There is no ODBC, no OLE DB, no DAO and no ADOX, so there is no Access Database Engine to install, no Windows requirement, and no need to match your process -architecture to a driver. It runs on Linux, macOS and ARM64, and its test suites run on all of them with no -Access engine present anywhere. +architecture to a driver. It runs on Linux, macOS and ARM64, and its core, engine and EF Core specification +suites run on all of them with no Access engine present anywhere. (A further set of suites exists only to cross-check +LibRed's output against the real Access engine, and runs where that engine is installed.) It ships with an Entity Framework Core provider. @@ -48,7 +49,8 @@ generates, not what the engine accepts. ## What works Reading and writing every page type, full B-tree index maintenance, `CREATE`/`ALTER`/`DROP TABLE`, primary -and foreign keys with referential integrity and cascade actions, `DEFAULT` and `CHECK` constraints, calculated +and foreign keys with referential integrity and cascade actions, `DEFAULT` and table-level `CHECK` +constraints, calculated columns (they map to EF Core's `ComputedColumnSql`, and scaffold back out of an existing database), views and stored procedures written the way Access writes them, transactions with real commit and rollback, and text index keys for both sort-order versions across the whole Basic Multilingual Plane plus the locale sort @@ -73,10 +75,11 @@ been through production use. no lock file, and none of the page/record locking Access coordinates through its side-car `.laccdb`/`.ldb`. Safe for any number of readers with no writer, or one writer with serialized access. Two concurrent writers will corrupt the file, so this is not the library for a shared network database. -- Jet 3 (Access 97) files are not supported; the Jet 4 / ACE family is. -- **Validation rules authored in the Access UI are read but not enforced.** They survive a round trip and are - reported through `INFORMATION_SCHEMA`, but LibRed will accept a row that Access would have rejected. SQL - `CHECK` constraints *are* enforced. +- Jet 3 (Access 97) files are not supported. +- **Validation rules authored in the Access UI are not enforced**: LibRed will accept a row that Access would + have rejected. +- **A column-level `CHECK`** (`Col LONG CHECK (Col > 0)`) is dropped — neither stored nor enforced. Declare it + at table level instead. ## Packages diff --git a/src/LibRed/README.md b/src/LibRed/README.md index b7771ba9..34079a93 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -50,7 +50,7 @@ Pages (DatabaseDefinition, TableDefinition, Data, Index, UsageMap, Lval) ↓ IO (PageChannel, PageBuffer) ← Crypto decrypts pages here ↓ -Formats (JetFormatBase — Jet 4 / ACE offsets & constants; a future Jet3Format overrides) +Formats (JetFormatBase — Jet 4 / ACE offsets & constants; Jet3Format is a stub overriding none yet) ``` ## Status @@ -58,7 +58,7 @@ Formats (JetFormatBase — Jet 4 / ACE offsets & constants; a future Jet3Format Well past scaffolding — LibRed reads, writes and **creates** real `.mdb`/`.accdb` files, runs SQL end-to-end, and EF Core runs its specification suite against it in two SQL modes. -The cross-platform claim is tested rather than asserted: CI runs the engine suite and both EF Core +The cross-platform claim is tested rather than asserted: CI runs the engine and core suites and both EF Core specification suites on Linux, Windows, macOS, ubuntu-arm and windows-arm, with no Access engine installed on any of them. The suites that *do* install ACE exist to cross-check LibRed's output against the real engine, which is a different job. @@ -80,10 +80,12 @@ Treat the number as of its date — an EF Core version bump moves it. - **Views & stored queries** — a query is written the way Access does: an `MSysObjects` type-5 row (negative synthetic id) plus the query decomposed byte-faithfully into `MSysQueries` rows — and Access opens the file and runs it. Covered: `CREATE VIEW` (`SELECT` with joins, `WHERE`, `DISTINCT`, `BETWEEN`, - `#date#` literals, column aliases, `Table.*`, nested/parenthesised joins), **GROUP BY totals**, **`TOP` + `#date#` literals, column aliases, `Table.*`, nested/parenthesised joins), **GROUP BY totals** with + `HAVING`, **`TOP` + `ORDER BY`**, and **derived-table / `UNION` sources** (the long subquery text lands on an LVAL page so Access executes it); **`CREATE PROCEDURE`** (parameterized — bare or parenthesised list, `@name`), - including **action-query** bodies (`CREATE TABLE` / `INSERT … VALUES`); and **`EXECUTE`/`EXEC`**. + including **action-query** bodies (`CREATE TABLE`, `INSERT … VALUES`, `INSERT … SELECT`, `UPDATE`, `DELETE` + and `SELECT … INTO`); and **`EXECUTE`/`EXEC`**. LibRed's own engine **reads them all back** — reconstructing the SQL from `MSysQueries` and expanding a view referenced in `FROM` (or inside an expression subquery) to a derived table — so they run through LibRed too. Reading is not limited to what LibRed writes: the `Attribute=1` operation row is read as the @@ -166,21 +168,30 @@ Treat the number as of its date — an EF Core version bump moves it. in every documented Access form: table-level `FOREIGN KEY [NO INDEX] (…) REFERENCES …`, column-level `… REFERENCES …`, and `ON UPDATE`/`ON DELETE` in either order. A relationship is persisted to `MSysRelationships` with a child-side FK index and **byte-faithful** logical-index linkage on both - tables' TDEFs (Access opens the file and enumerates it), with referential-integrity enforcement on - `INSERT` and **`CASCADE` / `SET NULL` referential actions** on `UPDATE`/`DELETE`. `UNIQUE` creates a unique - non-primary index, and self-referencing foreign keys are handled inline. + tables' TDEFs (Access opens the file and enumerates it). Referential integrity is enforced on `INSERT`, on + an `UPDATE` of a child's key and on every row a cascade rewrites; `ALTER TABLE … ADD FOREIGN KEY` checks the + rows already there; and the referential actions — `ON UPDATE CASCADE`, `ON DELETE CASCADE`, + `ON DELETE SET NULL` — carry on down a chain, since a cascaded row applies its own relationships too. Inside a + transaction the check is made again at commit from both ends, so neither a child added nor a parent removed + by another connection meanwhile slips through. `UNIQUE` creates a unique non-primary index, and + self-referencing foreign keys are handled inline. Column `DEFAULT` values are persisted to the table's `LvProp` property blob (on an LVAL page), read back onto the column, and applied when an insert omits the column — **and Access honors them too**. Table-level `CHECK` constraints are persisted to the `LvProp` `CheckConstraints` property (verbatim expression text) and read back onto `TableDef.CheckConstraints` — **and Access enforces them**. + From files Access wrote, a relationship's 1:1 flag (`grbit` `0x01`) is read and reported as ONE/MANY by + `INFORMATION_SCHEMA`, and a column's `ValidationRule`/`ValidationText` are read off the same property blob, + reported by `INFORMATION_SCHEMA` and kept through a rewrite. - **SQL** — ANTLR front end (parser → binder via `ISchemaProvider` → planner → executor). Statements: `CREATE TABLE`, `CREATE [UNIQUE] INDEX … [WITH {PRIMARY|DISALLOW NULL|IGNORE NULL}]`, `CREATE VIEW`, `CREATE PROCEDURE`, `ALTER TABLE` (ADD/DROP COLUMN, ADD PK/FK/UNIQUE/CHECK, ALTER COLUMN incl. the - byte-faithful in-place type change, DROP CONSTRAINT), `DROP {TABLE|INDEX|VIEW|PROCEDURE}`, `INSERT` + byte-faithful in-place type change and `SET`/`DROP DEFAULT`, DROP CONSTRAINT, `RENAME TO`/`RENAME COLUMN`), + `DROP {TABLE|INDEX|VIEW|PROCEDURE}`, `INSERT` (with AutoNumber) — from `VALUES`, from a multi-row table value constructor, or from a query (`INSERT INTO … SELECT`, the append query) — `SELECT … INTO` (the make-table query), `UPDATE`, `DELETE`, - `EXECUTE`, `IF … THEN`, and `SELECT` with `WHERE`, joins, `GROUP BY`/aggregates, `HAVING`, `ORDER BY`, - `TOP [PERCENT]`, `ALL`/`DISTINCT`/`DISTINCTROW`, `UNION`/`INTERSECT`/`EXCEPT`, subqueries, and parameters. + `EXECUTE`, `IF … THEN`, `BEGIN`/`COMMIT`/`ROLLBACK [TRANSACTION|WORK]`, `SELECT @@IDENTITY`, and `SELECT` + with `WHERE`, joins, `GROUP BY`/aggregates, `HAVING`, `ORDER BY`, `TOP [PERCENT]`, + `ALL`/`DISTINCT`/`DISTINCTROW`, `UNION`/`INTERSECT`/`EXCEPT`, subqueries, and parameters. `WITH OWNERACCESS OPTION` is taken wherever ACE takes it and changes nothing — LibRed has no users to act for — but a stored query keeps it. Access's bang notation: `[Table]![Column]` is `Table.Column`, and an unaliased one is named as written, as @@ -234,7 +245,8 @@ Treat the number as of its date — an EF Core version bump moves it. columns in order, as EF Core writes an inline collection. A view cannot store one. - **Access's `Nz`**, which the Access application has and ACE's OLE DB provider does not, with Access's own results — see [functions.md](docs/functions.md#nz). - - **Arguments ACE's own functions do not take**: `LOG(x, base)`, and SQL Server 2022's + - **Arguments ACE's own functions do not take**: `LOG(base, x)` (base first — SQL Server's is the other way + round), and SQL Server 2022's `LTRIM(x, characters)` / `RTRIM(x, characters)`, where the second argument is a set of characters to strip rather than a substring. - **Set operations in a subquery predicate** — `IN (… UNION …)`, `EXISTS (… EXCEPT …)`, and a scalar @@ -259,27 +271,24 @@ Format-level detail on each on-disk gap lives in `docs/format/`. *On-disk / write gaps:* - **`ON UPDATE SET NULL`** — pathway threaded but throws; its Jet storage bytes are unverified because the - ACE OLE DB provider rejects the DDL (needs a UI/DAO-created sample to probe). `ON DELETE SET NULL` and - 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. -- **`ValidationRule`/`ValidationText` are read but not enforced** — UI-authored validation, distinct from a - SQL `CHECK`. `JetCatalog` parses both off the property blob, `INFORMATION_SCHEMA` reports them and they - survive a rewrite; evaluation is what is missing, so a row violating an Access-authored rule is accepted - where ACE would refuse it. The asymmetry is invisible at the call site — `CheckConstraints` comes from the - same blob, read by the same code, and *is* enforced. -- **`AllowZeroLength` not modelled**, and column-level `CHECK` persistence is unprobed (its ACE storage - differs from the table-level form). + ACE OLE DB provider rejects the DDL (needs a UI/DAO-created sample to probe). +- **1:1 relationships** (`dbRelationUnique` = `0x01`) — never written; a 1:1 migration would render as 1:many + in Access (the unique index still enforces uniqueness). +- **`ValidationRule`/`ValidationText` are not enforced** — UI-authored validation, distinct from a SQL `CHECK`: + a row violating an Access-authored rule is accepted where ACE would refuse it. +- **`AllowZeroLength` not modelled.** +- **A column-level `CHECK`** (`Col LONG CHECK (Col > 0)`) is parsed and silently dropped — neither stored nor + enforced. Its ACE storage differs from the table-level form and is unprobed; a table-level `CHECK` over the + same column is the working route. - **Jet 3** format; strict **DAO Compact & Repair** compatibility (checklist captured — only relevant if targeting DAO C&R rather than "ACE opens + queries"). - **`CREATE TEMPORARY TABLE`** — parsed only to throw `NotSupportedException`. *SQL surface / engine gaps:* -- **Stored action queries** — only INSERT…VALUES and DDL bodies are written back to `MSysQueries`. The other - kinds (INSERT…SELECT, UPDATE, DELETE, make-table, crosstab, UNION, pass-through) read back as a *named* - refusal rather than executing, and a view carrying `HAVING` (attribute `0x0A`) is refused rather than - rebuilt. The gap is the write-back, not the grammar: all of these parse and execute as plain statements. +- **Stored crosstab, pass-through and UNION-kind queries** — read back as a *named* refusal rather than + executing. +- **`ALTER TABLE … RENAME INDEX`** — parsed and routed, but throws: the TDEF rewrite is not implemented. - **Function surface** — the evaluator's whitelist isn't proven identical to ACE's JES, and `Format`'s named date/currency formats are locale-dependent by design (not byte-identical cross-locale). - **Single-writer concurrency** — LibRed is a **single-writer engine that merely tolerates extra open @@ -345,11 +354,14 @@ a Jet workaround removed, then a capability added underneath it. The mode is not visible to the engine. Anything the generator emits is ordinary SQL the parser accepts from any caller, so a hand-written Access query keeps behaving the way Access does — where the two disagree, the -distinction is carried by **syntax**, not by a mode flag. `LibRedParameterBasedSqlProcessor` is the one place -the mode is read in the pipeline, to skip the Jet-dialect rewrites (and `JetCompatibilityExpressionVisitor`, -whose rejections — `ROW_NUMBER`, `CROSS`/`OUTER APPLY`, `EXCEPT`, `INTERSECT` — are exactly what extended -mode is for). +distinction is carried by **syntax**, not by a mode flag. The mode is read only on the EF side, to choose +what LINQ becomes: `LibRedQuerySqlGeneratorFactory` picks the generator, `LibRedQueryTranslationPostprocessor` +gates the skip/take emulation and ORDER BY lifting, `LibRedMethodCallTranslatorProvider` and +`LibRedSqlTranslatingExpressionVisitorFactory` choose the translations, and `LibRedParameterBasedSqlProcessor` +skips the Jet-dialect rewrites (and `JetCompatibilityExpressionVisitor`, whose rejections — `ROW_NUMBER`, +`CROSS`/`OUTER APPLY`, `EXCEPT`, `INTERSECT` — are exactly what extended mode is for). Each mode has its own EF Core specification suite, because the same LINQ query produces different SQL and so needs different baselines: `test/EFCore.LibRed.FunctionalTests` (compatible) and -`test/EFCore.LibRed.Extended.FunctionalTests` (extended). Both run on every push across all five platforms. +`test/EFCore.LibRed.Extended.FunctionalTests` (extended). Both run across all five platforms on every push +that touches LibRed. diff --git a/src/LibRed/docs/design/transactions.md b/src/LibRed/docs/design/transactions.md index dd8c1230..8ae44c82 100644 --- a/src/LibRed/docs/design/transactions.md +++ b/src/LibRed/docs/design/transactions.md @@ -31,19 +31,26 @@ Status: **draft / accepted direction** · Date: 2026-07-18 > needed: while the relationship survives and a child row still holds the key, so a transaction that later > deleted or moved its child, or dropped the relationship, commits. The relationship is identified by the > definition pages of its tables and the ids of its columns, so renames inside the transaction do not lose it, -> and one dropped and added again under the same name over other columns is a different relationship. A -> condition registered under a key the transaction already holds (the same relationship and parent key) is held -> once. `ALTER TABLE … ADD FOREIGN KEY` reads the same way, checking every existing child row against its +> and one dropped and added again under the same name over other columns is a different relationship. The same +> hole exists from the parent's end: a `DELETE` of a parent row, or a change to its key, that found no child +> (or cascaded or nulled the ones it found) writes nothing another connection's child insert would conflict +> with, and that insert finds the parent it has not seen deleted. So the parent side registers the same +> condition for the key it removed — no child row may hold it without a parent row holding it too. It is one +> condition from either end, so a condition registered under a key the transaction already holds (the same +> relationship and key) is held once. `ALTER TABLE … ADD FOREIGN KEY` reads the same way, checking every existing child row against its > parent, so it registers one condition for the relationship: while it survives, every child row still has its > parent. ACE gets the same guarantee by holding both tables exclusively until the transaction ends. A savepoint rollback drops the conditions recorded after it, and their keys, along with the writes that > needed them. > 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]]. - -> **Deferred (2026-09-24) — the commit-byte protocol and the Jet lock manager (§4 L0, phases 7–8).** The two +> DML does not force a catalog reload. The undo log described below is gone, and with it the before-image +> `SavepointStack`: savepoint frames live in `Transaction` and snapshot overlay state. Everything else here — the +> lock-manager layering (L0), the ACE co-residency constraint (§2), commit-byte / cross-process protocol — still +> stands as the roadmap. Of the cascade design in §3 L3, the **delete** cascade is the bounded worklist; an +> **ON UPDATE** cascade recurses one call per level instead (a rewritten child applies its own relationships), and +> terminates because a row already holding the new key is not rewritten again. See `TransactionIsolationTests`. + +> **Deferred (2026-09-24) — the commit-byte protocol and the Jet lock manager (§3 L0, phases 7–8).** The two > are one job, not two: a writer's commit slot is `0xE00 + 2n` for *its own* user number *n*, and that number > comes from the position it takes in the `.laccdb` — so there is no writing the commit table without first > doing the lock-file registration. What is missing is measured behaviour: the values ACE writes into its slot @@ -55,7 +62,7 @@ Status: **draft / accepted direction** · Date: 2026-07-18 > write-conflict check on commit, and process-local page locks. This is the ground-up design for LibRed's transaction support and the concurrency -infrastructure it sits on. It replaces the ad-hoc page-level undo log currently in +infrastructure it sits on. It replaced the ad-hoc page-level undo log that used to be in `PageChannel`, and is written so the localized atomicity gaps the production audit found (`LIBRED_AUDIT_REPORT.md`, the P0s) are closed by *one* model rather than patched site by site. @@ -68,8 +75,9 @@ patched site by site. index, usage map, LVAL chain, TDEF, or catalog entry. - **Explicit transactions.** `BEGIN`/`COMMIT`/`ROLLBACK` (and the ADO `LibRedTransaction`) group many statements atomically, with **savepoints** (nesting). -- **Cascade correctness.** Cascade delete/update runs as a bounded worklist inside the - transaction — no unbounded recursion, no double-mutate on diamond graphs. +- **Cascade correctness.** Cascade delete runs as a bounded worklist inside the + transaction — no unbounded recursion, no double-mutate on diamond graphs. (Cascade + update recurses per level; see the implementation note above.) - **Concurrency infrastructure, built in from day one.** Every read/write flows through explicit lock-acquisition seams and per-connection transaction context, so the real lock manager is a *fill-in*, not a later rewrite. @@ -79,13 +87,13 @@ patched site by site. (LibRed↔LibRed): a correct page-locking protocol using our own offsets. Reproducing Jet's exact `LockFileEx` offset bands, the `.laccdb` per-user records, and the page-0 commit-byte polling — so a live `MSACCESS.EXE` can share the file — is a later - swap of *values and detection*, not of *structure*. See [[jet-locking-user-registry]]. + swap of *values and detection*, not of *structure*. - **Crash durability beyond Jet's.** We match Jet's model: detect-and-repair, not a write-ahead log. A WAL is actively incompatible with ACE co-residency (§5). - **Record-level locking.** Page-level only. ACCDB per-page encryption already forces whole-page granularity — you cannot sub-page lock what you decrypt as a unit. - **Multi-writer performance.** Writers serialize for now (priority order: - correctness → speed → concurrency, [[libred-priority-order]]). + correctness → speed → concurrency). ## 2. Why not a WAL / journal (the ACE-co-residency constraint) @@ -100,7 +108,7 @@ same time**. That rules out any LibRed-private durability side-channel: DB suspect and repairs. So LibRed **matches Jet's on-file commit protocol** and provides atomicity/rollback -**in-process** (while the process lives) via an undo log. Cross-process consistency +**in-process** (while the process lives) via the deferred-write overlay (originally an undo log). Cross-process consistency comes from the lock protocol; crash consistency is Jet's (weak-by-design) detect-and-repair. We may be *more disciplined* about flush ordering within that protocol, but we add no structure to the file that Access doesn't understand. @@ -111,32 +119,33 @@ structure to the file that Access doesn't understand. L4 ADO surface LibRedTransaction / LibRedCommand enforce against L2 L3 Statement layer QueryEngine: implicit per-statement txn; cascade worklist L2 Transaction manager per-connection Transaction: begin/commit/rollback + savepoints - L1 PageChannel write choke point: lock seams + in-process undo log + L1 PageChannel write choke point: lock seams + per-transaction write overlay L0 Lock manager page locks, commit-byte map, .ldb — self-consistent now, Jet later ``` The invariant that makes L0 a later fill-in: **L1 already calls -`AcquireShared(page)` / `AcquireExclusive(page)` around every read/write and threads a -per-connection transaction context.** Today those calls resolve to a no-op (or a -process-local monitor) coordinator; the Jet coordinator is dropped in behind the same -interface. +`EnterShared(page)` / `EnterExclusive(page)` (and the matching `Exit…`) around every read/write and threads a +per-connection transaction context.** Today those calls resolve to the process-local `MonitorLockManager`; the +Jet coordinator is dropped in behind the same interface. ### L0 — Lock manager (`ILockManager`) -Owns cross-connection/cross-process coordination. Interface (stable; implementations vary): +Owns cross-connection/cross-process coordination. The interface as it ships: ``` -interface ILockManager : IDisposable +public interface ILockManager { - IDisposable AcquireShared(int page); // read lock; multiple readers - IDisposable AcquireExclusive(int page); // write lock; single writer, excludes readers - void MarkCommitPending(); // set our commit-byte slot -> mid-write - void MarkCommitDone(); // clear -> idle - int RegisterUser(); // claim a slot; returns user index - void ReleaseUser(int index); + void EnterShared(int page); // read lock; multiple readers + void ExitShared(int page); + void EnterExclusive(int page); // write lock; single writer, excludes readers + void ExitExclusive(int page); } ``` +The commit-byte and user-registry members the Jet coordinator will need — mark our slot mid-write and idle, claim +and release a user index — are **roadmap**: they arrive with the lock-file registration (see the deferral note +above), not before. + - **`SelfConsistentLockManager` (initial):** page locks via `FileStream.Lock` on the main handle at *our own* deterministic offset band (`page → base + page*stride`); a simple in-file or side-file presence map. Correct LibRed↔LibRed, Windows-and-Unix @@ -145,10 +154,14 @@ interface ILockManager : IDisposable - **`JetLockManager` (later):** the exact Jet 4/ACE offsets (~10M/20M bands on the `.laccdb` handle), the page-0 commit-byte table registration/polling, the `.laccdb` 32+32-byte identity records. Windows-only. This is a values+detection swap; the L1 - call sites and L2 semantics do not change. Source facts: [[jet-locking-user-registry]]. + call sites and L2 semantics do not change. ### L1 — PageChannel (write choke point) +> **Superseded — kept as the original sketch.** The before-image undo and held-to-commit exclusive locks below +> were replaced by the deferred-write overlay (see the implementation note at the top): writes buffer per +> transaction, and the exclusive page lock is held only while a committed page is written. + - `ReadPage(p)`: `using (locks.AcquireShared(p))` → decode/return (existing parsed-page cache unchanged). - `WritePage(p, bytes)` under a transaction: @@ -164,8 +177,12 @@ interface ILockManager : IDisposable ### L2 — Transaction manager +> **Superseded in part.** `Transaction` is per connection as designed, but it holds overlay state rather than +> before-images: a savepoint frame snapshots the overlay, rollback discards it, and there is no truncation or +> lock release to do. The frame stack below is the original sketch. + `Transaction` is **per connection** (EF holds several connections on one shared -`PageChannel`; a single global undo log is the current bug). Contents: +`PageChannel`; a single global undo log was the original bug). Contents: - `SavepointStack` — each frame holds its own `Dictionary` of before-images and the set of exclusive locks first taken in that frame. `Begin` pushes; `Release` @@ -186,9 +203,11 @@ interface ILockManager : IDisposable throw it rolls back. If an explicit user transaction is open, statements are savepoints within it instead. This single change closes the statement-atomicity P0 for *all* writers (row insert, DDL, view create, LVAL) without per-site edits. -- **Cascade as a worklist:** delete/update collects affected child rows into a queue with - a `visited` (in-progress) set; cycles terminate, diamonds mutate once. All mutations - are in the one transaction, so a mid-cascade failure rolls the whole thing back. +- **Cascade as a worklist:** delete collects affected child rows into a stack with + a `visited` (in-progress) set; cycles terminate, diamonds mutate once. An update cascade + rewrites each child through the same code as a directly updated row, which applies that + child's own relationships in turn. All mutations are in the one transaction, so a + mid-cascade failure rolls the whole thing back. ### L4 — ADO surface @@ -201,10 +220,10 @@ interface ILockManager : IDisposable ## 4. Transaction semantics - **Atomicity unit:** the statement (implicit) or the explicit `BEGIN…COMMIT` span. -- **Isolation (initial):** single-writer / many-readers via strict two-phase page - locking — exclusive locks held to commit. This yields serializable behavior for the - single-writer case. MVCC/snapshot is a later concurrency-phase option and is not - designed in here beyond "don't preclude" (before-images already exist). +- **Isolation:** read-committed, from the deferred-write overlay — see the implementation + note at the top. (The original plan was strict two-phase page locking with exclusive + locks held to commit; the overlay made that unnecessary.) MVCC/snapshot is a later + concurrency-phase option and is not designed in here beyond "don't preclude". - **Savepoints:** nesting via the frame stack; an inner statement inside an explicit txn is a frame, so its failure rolls back just that statement, not the user's transaction. - **Nested transactions** (SQL `BEGIN`/`COMMIT`/`ROLLBACK`, and any caller that nests) map @@ -266,15 +285,17 @@ concurrency-visible state, exactly as Jet uses them. Build correctness first with lock seams stubbed; drop the Jet lock manager in last. -1. **L2 core.** ✅ done. `Transaction` + `SavepointStack` with before-image undo, moved out of - `PageChannel`. `PageChannel.WritePage` records into the active transaction. Unit tests - for commit/rollback/nested rollback, allocate-then-rollback truncation. +1. **L2 core.** ✅ done, then superseded by the overlay (see the implementation note): the + before-image undo and `SavepointStack` shipped first and were replaced — a transaction's + writes now buffer in the overlay, savepoint frames live in `Transaction`, and rollback + discards pages rather than restoring and truncating them. 2. **L3 statement atomicity.** ✅ done. Wrap every `QueryEngine` statement in an implicit txn; convert the audit's non-atomic writers (`RowInserter`, `TableCreator`, `ViewCreator`, usage-map/LVAL) to rely on it. Regression: inject a late failure mid-statement, assert no partial state. -3. **Cascade worklist.** ✅ done. Replace recursive cascade with the queue+visited worklist - inside the txn. Tests: cyclic FK, diamond FK, deep chain (former stack-overflow). +3. **Cascade worklist.** ✅ done for DELETE. Replace recursive cascade delete with the + stack+visited worklist inside the txn. Tests: cyclic FK, diamond FK, deep chain (former + stack-overflow). An update cascade recurses per level. 4. **L1 lock seams + `MonitorLockManager`.** ✅ done. Introduce `ILockManager`, route read/write through it (cache-hit reads stay lock-free via copy-on-write `Store`), ship the process-local monitor implementation. @@ -296,8 +317,7 @@ Build correctness first with lock seams stubbed; drop the Jet lock manager in la map; multi-*process* single-writer LibRed↔LibRed. 8. **(Concurrency phase) `JetLockManager`.** Jet-exact offsets, commit-byte polling, `.laccdb` records → live co-residency with `MSACCESS.EXE`. Characterize the remaining - unknowns from [[jet-locking-user-registry]] (own-slot writes, poll interval, Jet-4 - `.laccdb` record shape) first. + unknowns (own-slot writes, poll interval, Jet-4 `.laccdb` record shape) first. Steps 1–3 close every transaction-related P0 in the audit (✅). Step 5 completes the in-process transaction contract (✅), and step 6 adds the SQL front door onto it. Steps 7–8 @@ -321,4 +341,4 @@ don't move. a global writer lock (one writer at a time), so no page-order deadlock exists yet; revisit when finer locking arrives. - **`.ldb`/`.laccdb` lifecycle** (create on first open, delete on last close) — belongs to - step 6/7; not needed for in-process correctness. + steps 7–8; not needed for in-process correctness. diff --git a/src/LibRed/docs/format/README.md b/src/LibRed/docs/format/README.md index aa2c3c30..7a525569 100644 --- a/src/LibRed/docs/format/README.md +++ b/src/LibRed/docs/format/README.md @@ -10,8 +10,9 @@ Unless noted, everything here describes **Jet 4 and ACE (12/14/16/17)**, which s structural layout. **Jet 3** (Access 97) differs in many of these and is *not yet implemented* — see [Version differences](#version-differences). -Implemented by `src/LibRed/LibRed.Core/`. The canonical offsets live in -`Formats/JetFormatBase.cs`. +Implemented by `src/LibRed/LibRed.Core/`. The canonical offsets live under `Formats/` — +`JetFormatBase.cs` for the page, TDEF and row layout, and `IndexBlockFormat.cs`, `LongValueFormat.cs` and +`CatalogFormat.cs` for their own sub-structures. > **This reference is split across several files** — one per page type, plus cross-cutting topics; each is > self-contained (its structures *and* its read/write mechanics live together). Most describe the **on-disk @@ -133,7 +134,8 @@ the Jet 4 / ACE layout documented across these files: Jet 4 and all later ACE versions (12/14/16/17) share the structural layout documented here; differences between *those* are additive at the type/feature level (new data types, encryption schemes), not the page offsets. In LibRed this is reflected by `JetFormatBase` virtual members -with Jet 4/ACE defaults; a future `Jet3Format` overrides the ones that differ. +with Jet 4/ACE defaults; `Jet3Format` (today a stub setting only the 2 KB page size, and never constructed — +a Jet 3 file is refused on open) will override the ones that differ. --- diff --git a/test/LibRed.Benchmarks/Harness/Corpus.cs b/test/LibRed.Benchmarks/Harness/Corpus.cs index 40fa8421..638928af 100644 --- a/test/LibRed.Benchmarks/Harness/Corpus.cs +++ b/test/LibRed.Benchmarks/Harness/Corpus.cs @@ -183,8 +183,9 @@ private static void FillSmall(JetDatabase db) } } - /// A note whose text is stable per row and contains the qx needle in roughly one row in - /// twenty — enough for the unanchored-LIKE case to return rows without returning most of the table. + /// A note whose text is stable per row and contains the qx needle in every twentieth row — + /// one row in forty overall, since half of those are the rows with no note — enough for the unanchored-LIKE + /// case to return rows without returning most of the table. private static string Note(Random rnd, int i) { string body = $"note {i} {rnd.Next(100_000):D5} lorem ipsum dolor sit amet consectetur"; diff --git a/test/LibRed.Benchmarks/Program.cs b/test/LibRed.Benchmarks/Program.cs index 565f7bb2..5293fd78 100644 --- a/test/LibRed.Benchmarks/Program.cs +++ b/test/LibRed.Benchmarks/Program.cs @@ -15,7 +15,8 @@ // dotnet run -c Release -- --filter "*Ace*" head-to-head against ACE OLE DB (Windows + ACE only) // dotnet run -c Release -- --record also append the results to History.tsv // -// Results land in Results/ as GitHub markdown and CSV; commit them when you want a run to be diffable later. +// Results land in Results/ as GitHub markdown and CSV, which is untracked; --record is what keeps a run +// diffable later, in History.tsv (git add -f a report if you want the full detail too). bool longRun = false; bool validate = false; diff --git a/test/LibRed.Benchmarks/README.md b/test/LibRed.Benchmarks/README.md index 59fd479c..ec019f79 100644 --- a/test/LibRed.Benchmarks/README.md +++ b/test/LibRed.Benchmarks/README.md @@ -26,14 +26,15 @@ dotnet run --project test\LibRed.Benchmarks -c Release -- --record | `PipelineBenchmarks` | Parse vs bind+plan vs execute vs end-to-end, on a representative spread. Attribution. | | `StorageBenchmarks` | Cursors and index seeks with no SQL above them — the floor everything else sits on. | | `WriteBenchmarks` | INSERT/UPDATE/DELETE throughput: SQL autocommit, SQL in a transaction, and raw storage. | -| `DdlBenchmarks` | Creating a database from nothing, creating tables, back-filling an index. | +| `DdlBenchmarks` | Creating a database from nothing, creating tables, back-filling an index, adding a column to a populated table. | | `CatalogBenchmarks` | Open + catalog decode: the fixed price every connection pays. | | `AceComparisonBenchmarks` | Identical SQL and file against ACE OLE DB. ACE is the baseline; ratio < 1 is a win. | ## The corpus -`SqlCorpus` is the single list of benchmarked statements; every suite draws from it, so adding a benchmark is -adding a line. Cases are named `category.what_it_does`, which is also what `--filter` matches. +`SqlCorpus` is the single list of benchmarked statements; every query suite (Synthetic, Northwind, Pipeline, +AceComparison) draws from it, so adding a query benchmark is adding a line. The storage, write, DDL and catalog +suites drive their operations directly. Cases are named `category.what_it_does`, which is also what `--filter` matches. `Corpus` generates the synthetic database — three tables at a caller-chosen scale factor, with known cardinalities: @@ -45,7 +46,7 @@ cardinalities: | `Bench.Bucket` | 8 values, varying *within* a `K` group — the second column of the composite index. | | `Bench.G` | scale/4 distinct values, **unindexed**. High-cardinality grouping, hash-join key. | | `Bench.Label` | 5,000 distinct values, indexed text. | -| `Bench.Note` | Unindexed text, null in one row of eight, needle `qx` in one of twenty. | +| `Bench.Note` | Unindexed text, null in one row of eight, needle `qx` in one of forty (every twentieth row, less the half of those that are null). | | `BenchChild` | 2× `Bench` rows, two children per parent: joins with real fan-out. | | `BenchSmall` | 100 rows: the small side of a hash join. | diff --git a/test/LibRed.Engine.Tests/LibRed.Engine.Tests.csproj b/test/LibRed.Engine.Tests/LibRed.Engine.Tests.csproj index 6c5a346e..601b7063 100644 --- a/test/LibRed.Engine.Tests/LibRed.Engine.Tests.csproj +++ b/test/LibRed.Engine.Tests/LibRed.Engine.Tests.csproj @@ -13,8 +13,8 @@ true $(MSBuildThisFileDirectory)..\..\Key.snk AnyCPU;x86;x64 - - $(NoWarn);CA1416 + diff --git a/tools/JetLockTrace/README.md b/tools/JetLockTrace/README.md index ebc5f860..adbe85f3 100644 --- a/tools/JetLockTrace/README.md +++ b/tools/JetLockTrace/README.md @@ -78,8 +78,10 @@ plus enough beyond that to identify the holder). It also decodes I/O against the database file — page numbers, sub-page field writes, and the **commit-byte table** at page 0 `0xE00`–`0xFFF` (256 users × 2 bytes), which Access writes immediately before and after a -batch of page writes. A nonzero commit byte with no matching user lock is what makes Access declare a file -suspect and demand a repair. +batch of page writes. Each slot is one little-endian 16-bit value: the bytes `00 00` (mid-write) or `01 00` +(accessed a corrupted page) with no matching user lock are what make Access declare the file suspect and demand +a repair. The idle value is `00 01` (256), and above that the slot counts the user's committed writes — see +[page-00 §2.2](../../src/LibRed/docs/format/page-00-database.md). Region names are the Jet development team's own, from `docs/JetWhitePapers_UPDATE1/Jetlock.docx`. From 0d999b84e6dc62a68a8a553a10ab37b08c8bf226 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Fri, 2 Oct 2026 01:24:48 +0800 Subject: [PATCH 87/88] Derive every stored SID from page 0, and re-mask them when a password changes The SID keystream is folded from page 0's header (the 0x42 field and the creation date), so a database is no longer created from one baked date/SID pair: it is dated now and its SIDs are the default workgroup accounts XOR the stream that header gives. The accounts carry their real roles - admin owns MSysDb and new objects, Engine the system tables, Users holds the grants, Creator the inheritable placeholder. Anything that changes the field changes the stream, so the stored SIDs are re-masked with it: the legacy Jet password on an .mdb, and an .accdb's whole-file encryption, whose 0x42 field Access fills with the database key's low byte. The .accdb re-mask runs in a transaction the page copy reads through and then rolls back, so the original file is untouched unless the replacement lands. Co-Authored-By: Claude Opus 5.5 --- src/LibRed/LibRed.Core/Catalog/JetCatalog.cs | 39 +++--- .../LibRed.Core/Crypto/DatabaseEncryption.cs | 128 +++++++++++++----- src/LibRed/LibRed.Core/Crypto/SidKeystream.cs | 59 ++++++++ .../LibRed.Core/Storage/CatalogWriter.cs | 18 +-- .../LibRed.Core/Storage/DatabaseCreator.cs | 90 ++++++------ src/LibRed/docs/format/appendix-structures.md | 4 +- src/LibRed/docs/format/page-00-database.md | 121 +++++++++++------ src/LibRed/docs/format/system-catalog.md | 24 ++-- .../JetPasswordSidAccessTests.cs | 93 +++++++++++++ .../LibRed.Core.Tests/DatabaseCreatorTests.cs | 46 +++++++ .../LegacyJetPasswordTests.cs | 97 +++++++++++++ .../PermissionRowsAccessTests.cs | 2 +- 12 files changed, 558 insertions(+), 163 deletions(-) create mode 100644 src/LibRed/LibRed.Core/Crypto/SidKeystream.cs create mode 100644 test/LibRed.Core.AccessTests/JetPasswordSidAccessTests.cs diff --git a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs index cd987f7f..496eccb5 100644 --- a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs +++ b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs @@ -39,37 +39,40 @@ public sealed class JetCatalog(PageChannel channel, int catalogPage = 2) private Dictionary? _actionQueries; private Dictionary>? _queryParameters; private List? _complexColumns; - private (byte[] Users, byte[] Admin, byte[] Creator)? _securitySids; + private (byte[] Admin, byte[] Users, byte[] Creator)? _securitySids; private long _seenSchemaGeneration = channel.SchemaGeneration; /// All tables in the database (user and system). public IReadOnlyList Tables { get { EnsureFresh(); return _tables ??= LoadTables(); } } /// - /// The account SIDs as this database masks them: the owner a new object takes (the Users group), the - /// administrator, and the Creator placeholder a container's inheritable grant names in place of whoever - /// creates an object in it. Every on-disk SID is a workgroup account SID XOR'd with a mask that differs per - /// file and is stored nowhere (page-00 §2.3) — but MSysObjects is owned by the Engine account in every - /// file, ACE-written and LibRed-written alike, so its own catalog row gives the mask away. An object written - /// into a file has to carry that file's SIDs, or its owner decodes to no account at all. + /// The account SIDs as this database masks them: the admin user, who owns an object LibRed creates + /// (LibRed acts as the default workgroup's admin), the Users group, and the Creator placeholder a container's + /// inheritable grant names in place of whoever creates an object in it. Every on-disk SID is a workgroup + /// account SID XOR'd with a keystream that differs per file and is stored nowhere (page-00 §2.3) — but + /// MSysObjects is owned by the Engine account in every file, ACE-written and LibRed-written alike, so + /// its own catalog row gives the keystream's first bytes away. An object written into a file has to carry that + /// file's SIDs, or its owner decodes to no account at all. /// - /// Falls back to the SIDs bakes in, which is what a file this - /// engine created carries, when the row cannot be read. - public (byte[] Users, byte[] Admin, byte[] Creator) SecuritySids + /// When that row cannot be read, the keystream is worked out from page 0 instead, which determines + /// it (page-00 §2.3). + public (byte[] Admin, byte[] Users, byte[] Creator) SecuritySids { get { - _ = Tables; // reading the catalog captures the mask from MSysObjects' own row - return _securitySids - ?? (Storage.DatabaseCreator.SidUsers, Storage.DatabaseCreator.SidAdmin, Storage.DatabaseCreator.SidCreator); + _ = Tables; // reading the catalog captures the keystream from MSysObjects' own row + if (_securitySids is { } sids) return sids; + byte[] engine = Crypto.SidKeystream.MaskAccount(_channel.ReadPageShared(0).Span, EngineAccount); + return (AsMaskedInThisFile(engine, AdminAccount), AsMaskedInThisFile(engine, UsersAccount), + AsMaskedInThisFile(engine, CreatorAccount)); } } - // The default workgroup's account SIDs (page-00 §2.3), in the "user" form object ownership uses. - private static readonly byte[] EngineAccount = [0x03, 0x03]; - private static readonly byte[] UsersAccount = [0x02, 0x01]; + // The default workgroup's account SIDs, as a stock System.mdw's MSysAccounts holds them (page-00 §2.3). + private static readonly byte[] EngineAccount = [0x02, 0x03]; private static readonly byte[] AdminAccount = [0x03, 0x01]; - private static readonly byte[] CreatorAccount = [0x03, 0x04]; + private static readonly byte[] UsersAccount = [0x02, 0x01]; + private static readonly byte[] CreatorAccount = [0x02, 0x04]; private static byte[] AsMaskedInThisFile(byte[] engineOnDisk, byte[] account) => [ @@ -218,7 +221,7 @@ private List LoadTables() // MSysObjects' own row carries the Engine account's SID as this file masks it — the one anchor // for every SID a new object in this file must be written with (see SecuritySids). if (definitionPage == _catalogPage && row[ownerIndex] is byte[] { Length: 2 } engine) - _securitySids = (AsMaskedInThisFile(engine, UsersAccount), AsMaskedInThisFile(engine, AdminAccount), + _securitySids = (AsMaskedInThisFile(engine, AdminAccount), AsMaskedInThisFile(engine, UsersAccount), AsMaskedInThisFile(engine, CreatorAccount)); uint flags = unchecked((uint)(int)row[flagsIndex]!); diff --git a/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs b/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs index b360e6b4..2166e06f 100644 --- a/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs +++ b/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs @@ -28,10 +28,12 @@ namespace LibRed.Crypto; /// buffer, so the memory cost is a page rather than the database; and because the channel hands them over /// decrypted, a password change re-encrypts in that same pass and the plaintext database never reaches disk. /// The replacement cannot be moved into place under an open handle, so these operations close the -/// database — page for page it is a different file, and the caller reopens it under the new password. -/// An operation that only edits page 0 — the legacy Jet password field — writes that one page through the -/// open channel instead, and leaves the database open: copying a whole database to change 40 bytes is the -/// greater risk, and the field is not a pointer, so a torn write costs the password rather than the data. +/// database — page for page it is a different file, and the caller reopens it under the new password. +/// On an .accdb page 0's 40-byte 0x42 field carries the low byte of the database key, as Access +/// writes it, and the stored SIDs are re-masked to the keystream that field now gives (page-00 §2.3). +/// The legacy Jet password instead changes page 0's 40-byte field and re-masks the stored SIDs, which the +/// field's keystream covers, through the open channel in one transaction, and leaves the database open: copying +/// a whole database to change those is the greater risk. /// public static class DatabaseEncryption { @@ -142,38 +144,91 @@ private static void ValidateRc4Options(int keyBits, StandardHash 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 - /// double at 0x72, all within the header-masked region. Verified byte-identical to Access's own output. - /// Only page 0 changes, so the database stays open. + /// double at 0x72, all within the header-masked region; the field's bytes are verified byte-identical + /// to Access's own output. The field also feeds the keystream every stored SID is masked with, so the SIDs are + /// re-masked with it, as Access's own password change does. The database stays open. public static void SetJetPassword(JetDatabase database, string password) { ArgumentException.ThrowIfNullOrEmpty(password); if (password.Length > JetPasswordSize / 2) throw new ArgumentException($"A Jet database password is at most {JetPasswordSize / 2} characters.", nameof(password)); - RewriteHeader(database, (page0, format) => - { - if (format.IsAccdb) - throw new ArgumentException("The legacy Jet password applies to .mdb, not .accdb — use SetPassword.", nameof(database)); - WriteJetPasswordField(page0, password); - }); + RewriteJetPassword(database, password, + "The legacy Jet password applies to .mdb, not .accdb — use SetPassword."); } /// Removes a legacy Jet 4 (.mdb) database password by clearing the password field (equivalent to - /// setting an empty password). Only page 0 changes, so the database stays open. - public static void RemoveJetPassword(JetDatabase database) + /// setting an empty password), re-masking the stored SIDs as does. The database + /// stays open. + public static void RemoveJetPassword(JetDatabase database) => + // An empty password: the field encodes the mask alone, decoding back to "". + RewriteJetPassword(database, "", + "The legacy Jet password applies to .mdb, not .accdb — use RemovePassword."); + + /// + /// Writes into the page-0 field and re-masks every stored SID to match, in one + /// transaction. The field is half of the header region the SID keystream is folded from (page-00 §2.3), so a + /// new password is a new keystream; each SID in MSysObjects.Owner and MSysACEs.SID becomes + /// stored XOR oldStream XOR newStream over its whole length — exactly what Access's own password change + /// writes, the 102-byte SIDs included (verified against DAO's NewPassword). Left as they were, they + /// would belong to no account under the new key. + /// + private static void RewriteJetPassword(JetDatabase database, string password, string accdbMessage) { - RewriteHeader(database, (page0, format) => + PageChannel channel = RequireExclusive(database); + if (channel.Format.IsAccdb) throw new ArgumentException(accdbMessage, nameof(database)); + + var page0 = new byte[channel.PageSize]; + channel.ReadPage(0, page0); + byte[] before = (byte[])page0.Clone(); + WritePasswordField(page0, Encoding.Unicode.GetBytes(password)); + + database.BeginTransaction(); + try { - if (format.IsAccdb) - throw new ArgumentException("The legacy Jet password applies to .mdb, not .accdb — use RemovePassword.", nameof(database)); - WriteJetPasswordField(page0, ""); // empty password → field encodes the mask alone, decoding back to "" - }); + channel.WritePage(0, page0); + RemaskSids(database, before, page0); + database.Commit(); + } + catch + { + database.Rollback(); + throw; + } + database.Catalog.Invalidate(); // it holds the SIDs it read from MSysObjects' own row } - // Encodes the password into the header-masked 0x42 field: plaintext = UTF-16LE(password) zero-padded to 40 bytes, - // XORed with the 4-byte little-endian (int)creationDateDouble mask (cycled); the on-disk bytes are that plaintext - // XORed with the page-0 header mask. Reading (jackcess/LibRed) is the exact inverse. - private static void WriteJetPasswordField(byte[] page0, string password) + /// + /// Re-masks every SID in MSysObjects.Owner and MSysACEs.SID from the keystream page 0 + /// folds to into the one does (page-00 §2.3), inside the + /// caller's transaction: each becomes stored XOR oldStream XOR newStream over its whole length. + /// + private static void RemaskSids(JetDatabase database, byte[] before, byte[] after) + { + foreach ((string tableName, string columnName) in new[] { ("MSysObjects", "Owner"), ("MSysACEs", "SID") }) + { + Table table = database.OpenTable(tableName); + int column = table.Definition.RequireColumn(columnName).Index; + var rows = table.RowsWhere([column], values => values[column] is byte[] { Length: > 0 }).ToList(); + if (rows.Count == 0) continue; + + int longest = rows.Max(r => ((byte[])r.Values[column]!).Length); + byte[] oldStream = SidKeystream.For(before, longest), newStream = SidKeystream.For(after, longest); + var changed = new HashSet { column }; + foreach ((RowId id, object?[] values) in rows) + { + byte[] sid = (byte[])values[column]!; + for (int i = 0; i < sid.Length; i++) sid[i] ^= (byte)(oldStream[i] ^ newStream[i]); + table.Update(id, values, changed); + } + } + } + + // Encodes the header-masked 0x42 field: plaintext = value zero-padded to 40 bytes, XORed with the 4-byte + // little-endian (int)creationDateDouble mask (cycled); the on-disk bytes are that plaintext XORed with the page-0 + // header mask. Reading (jackcess/LibRed) is the exact inverse. An .mdb's value is its Jet password, UTF-16LE; + // an .accdb's is the low byte of its database key, 40 times over (zero when unencrypted). + private static void WritePasswordField(byte[] page0, ReadOnlySpan value) { ReadOnlySpan hmask = JetFormatBase.PageZeroHeaderMask; int start = JetFormatBase.PageZeroHeaderMaskStart; @@ -185,7 +240,7 @@ private static void WriteJetPasswordField(byte[] page0, string password) Span field = stackalloc byte[JetPasswordSize]; field.Clear(); - Encoding.Unicode.GetBytes(password).CopyTo(field); + value.CopyTo(field); for (int i = 0; i < JetPasswordSize; i++) page0[JetPasswordOffset + i] = (byte)(field[i] ^ dateMask[i % 4] ^ hmask[JetPasswordOffset - start + i]); } @@ -250,13 +305,25 @@ private static void Rewrite(JetDatabase database, FuncEdits page 0 alone, through the open channel, leaving the database open on it. - private static void RewriteHeader(JetDatabase database, Action change) - { - PageChannel channel = RequireExclusive(database); - var page0 = new byte[channel.PageSize]; - channel.ReadPage(0, page0); - change(page0, channel.Format); - channel.WritePage(0, page0); - channel.Flush(); - } - /// The state every operation here demands of the database it is given: opened for this caller /// alone, writable, and not mid-transaction. private static PageChannel RequireExclusive(JetDatabase database) diff --git a/src/LibRed/LibRed.Core/Crypto/SidKeystream.cs b/src/LibRed/LibRed.Core/Crypto/SidKeystream.cs new file mode 100644 index 00000000..eeea445b --- /dev/null +++ b/src/LibRed/LibRed.Core/Crypto/SidKeystream.cs @@ -0,0 +1,59 @@ +using System.Buffers.Binary; +using LibRed.Formats; + +namespace LibRed.Crypto; + +/// +/// The per-file RC4 keystream every security SID in MSysObjects.Owner and MSysACEs.SID is stored +/// XOR'd with (page-00 §2.3). Its key is folded from page 0's decoded header — the database-password field, the +/// creation date and the header bytes around them — so a file's SIDs belong to that header, and changing one +/// of its inputs means re-masking every SID. +/// +internal static class SidKeystream +{ + private const int FoldRegion = 0x42; // 80 bytes from here: the 40-byte password field and 40 after it + private const int PasswordFieldSize = 40; + private const int CreationDateOffset = JetFormatBase.CreationDateOffset; + + /// The first keystream bytes for a page 0 as stored on disk. Each SID is + /// XOR'd from the stream's first byte, whatever its length. + public static byte[] For(ReadOnlySpan page0, int length) + { + // 1. Remove the fixed header mask. + int start = JetFormatBase.PageZeroHeaderMaskStart; + ReadOnlySpan headerMask = JetFormatBase.PageZeroHeaderMask; + Span header = stackalloc byte[start + headerMask.Length]; + page0[..header.Length].CopyTo(header); + for (int i = 0; i < headerMask.Length; i++) header[start + i] ^= headerMask[i]; + + // 2-3. The 80-byte region, its first 40 bytes (the password field) unmasked with the creation date's + // integer part, as the field is stored. + Span region = stackalloc byte[2 * PasswordFieldSize]; + header.Slice(FoldRegion, region.Length).CopyTo(region); + Span dateMask = stackalloc byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(dateMask, + (int)BinaryPrimitives.ReadDoubleLittleEndian(header.Slice(CreationDateOffset, 8))); + for (int i = 0; i < PasswordFieldSize; i++) region[i] ^= dateMask[i % 4]; + + // 4-5. Seeded with the creation date's low four raw bytes, folding every even byte of the region. + uint key = BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(CreationDateOffset, 4)); + for (int i = 0; i < PasswordFieldSize; i++) key ^= (uint)region[2 * i] << (i % 24); + + // 6. RC4 under the key's four little-endian bytes. + Span rc4Key = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(rc4Key, key); + var stream = new byte[length]; + Rc4Cipher.Apply(rc4Key, stream); + return stream; + } + + /// A workgroup account's SID — as the workgroup file's MSysAccounts holds it — the way this + /// file stores it: the SID XOR the keystream, from its first byte. + public static byte[] MaskAccount(ReadOnlySpan page0, ReadOnlySpan account) + { + byte[] stream = For(page0, account.Length); + byte[] masked = account.ToArray(); + for (int i = 0; i < masked.Length; i++) masked[i] ^= stream[i]; + return masked; + } +} diff --git a/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs b/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs index aa7f84e7..19c8f9e6 100644 --- a/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs @@ -36,7 +36,7 @@ public void AddObjectRow( Set(msysObjects, values, "Type", type); Set(msysObjects, values, "Name", name); Set(msysObjects, values, "Flags", flags); - Set(msysObjects, values, "Owner", catalog.SecuritySids.Users); + Set(msysObjects, values, "Owner", catalog.SecuritySids.Admin); Set(msysObjects, values, "DateCreate", now); Set(msysObjects, values, "DateUpdate", now); @@ -56,17 +56,17 @@ public void AddObjectRow( /// Inserts the object's MSysACEs rows, maintaining the ObjectId index so Access's security /// check finds them — without them it warns about permissions on opening the object. /// The grants are the ones the object's container passes down, derived as ACE derives them - /// (verified): the Creator account's inheritable grant becomes the owner's — the Users group — and every - /// other inheritable grant is copied for its own account, OR'd into the owner's row when it names the owner's - /// account. The owner's row comes first, the rest in the container's order. So the masks follow the database: - /// a table's owner gets 0xF00FE where the Tables container grants the Creator that alone, and 0xFFEFF where it - /// also grants Users 0xFFEFF, as Northwind's does. + /// (verified): the Creator account's inheritable grant becomes the owner's — the admin user, as whom LibRed + /// creates every object — and every other inheritable grant is copied for its own account, OR'd into the + /// owner's row when it names the owner's account. The owner's row comes first, the rest in the container's + /// order. So the masks follow the database: a table's owner gets 0xF00FE where the Tables container grants the + /// Creator that alone, and 0xFFEFF where it also grants admin 0xFFEFF, as Northwind's does. public void AddPermissionRows(int objectId, int containerId) { TableDef msysAces = catalog.RequireTable("MSysACEs"); int idIndex = msysAces.RequireColumn("ObjectId").Index, sidIndex = msysAces.RequireColumn("SID").Index; int acmIndex = msysAces.RequireColumn("ACM").Index, inheritIndex = msysAces.RequireColumn("FInheritable").Index; - (byte[] users, _, byte[] creator) = catalog.SecuritySids; + (byte[] owner, _, byte[] creator) = catalog.SecuritySids; var grants = new List<(byte[] Sid, int Acm)>(); var aces = new Table(channel, msysAces); @@ -74,13 +74,13 @@ public void AddPermissionRows(int objectId, int containerId) { if (row[idIndex] is not int id || id != containerId || row[inheritIndex] is not true) continue; byte[] sid = (byte[])row[sidIndex]!; - if (sid.AsSpan().SequenceEqual(creator)) sid = users; + if (sid.AsSpan().SequenceEqual(creator)) sid = owner; int at = grants.FindIndex(g => g.Sid.AsSpan().SequenceEqual(sid)); if (at < 0) grants.Add((sid, (int)row[acmIndex]!)); else grants[at] = (grants[at].Sid, grants[at].Acm | (int)row[acmIndex]!); } - foreach ((byte[] sid, int acm) in grants.OrderBy(g => g.Sid.AsSpan().SequenceEqual(users) ? 0 : 1)) + foreach ((byte[] sid, int acm) in grants.OrderBy(g => g.Sid.AsSpan().SequenceEqual(owner) ? 0 : 1)) { var values = new object?[msysAces.Columns.Count]; Set(msysAces, values, "ACM", acm); diff --git a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs index 34ecae02..c7f11349 100644 --- a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs @@ -1,4 +1,5 @@ using LibRed.Catalog; +using LibRed.Crypto; using LibRed.Formats; using LibRed.Pages; using System.Buffers.Binary; @@ -28,8 +29,8 @@ public static class DatabaseCreator /// The database's default collation — its LCID and sort-order version /// (1033 / version 0 is General Legacy en-US). /// Creation timestamp as an OLE-automation date (days since 1899-12-30), passed - /// 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 ). + /// as the raw double so the exact millisecond-precise bit pattern is preserved — the file's SIDs are masked + /// with a keystream folded from those bits and the rest of the header (page-00 §2.3). public static byte[] BuildDefinitionPage( byte version, bool isAccdb, int codePage, Collation collation, double creationDays) { @@ -211,27 +212,14 @@ private static readonly (string Name, ColumnSpec[] Columns)[] MSysComplexTypeTab private const int SystemFlag = unchecked((int)0x80000000); - // Per-file SID cluster. A database's on-disk SIDs are the DEFAULT WORKGROUP's account SIDs XOR'd with a - // per-file keystream, whose first two bytes are all a 2-byte SID meets — so it reads as a 2-byte mask. The - // account SIDs were read verbatim from a real System.mdw (the file Access opens first to authenticate): - // admin(user)=03-01, Users(group)=02-01, Engine=02-03, Creator=02-04; the Admins group alone has a 102-byte - // SID, which Access adds on first open, so we don't emit it. Object ownership uses the "user" form (byte0 - // 0x03) of Engine/Creator, matching real DAO files. Verified against WideTable with mask 24-CC: Users - // 02-01^24-CC = 26-CD, admin 03-01^24-CC = 27-CD, Engine-as-user 03-03^24-CC = 27-CF (system-object owner), - // Creator-as-user 03-04^24-CC = 27-C8. - // - // The keystream is bound to the millisecond-precise creation date, and how it derives from the date is not - // known (docs/format/page-00-database.md §2.3), so the two MUST travel together. We bake one verified, - // self-consistent (creation-date, mask) pair — the from-scratch analogue of the account-SID constants — - // giving an Access-openable file with no template/graft. - // TODO: per-file-random dates (and custom/secured workgroups) need the date<->keystream derivation. - internal const long SeedCreationDateBits = 0x40E68F1E8943D217L; // 2026-06-27 22:54:07.716 (WideTable) — pairs with SidMask - private static readonly byte[] SidMask = [0x24, 0xCC]; // WideTable's per-file mask (pairs with SeedCreationDateBits) - private static byte[] Masked(byte b0, byte b1) => [(byte)(b0 ^ SidMask[0]), (byte)(b1 ^ SidMask[1])]; - internal static readonly byte[] SidUsers = Masked(0x02, 0x01); // Users group — read grantee / owner of user tables - internal static readonly byte[] SidAdmin = Masked(0x03, 0x01); // admin user — full grantee - private static readonly byte[] SidEngine = Masked(0x03, 0x03); // Engine (user form) — owner of system tables + DAO containers - internal static readonly byte[] SidCreator = Masked(0x03, 0x04); // Creator (user form) — inheritable container grant + // The default workgroup's account SIDs, as a stock System.mdw's MSysAccounts holds them (the file Access opens + // first to authenticate). The Admins group alone has a 102-byte SID, which Access adds on first open, so it is + // not emitted. On disk each is XOR'd with the file's own keystream, which page 0 determines (page-00 §2.3), so + // the stored bytes are worked out per file from the header written for it — see SidKeystream. + private static readonly byte[] AdminAccount = [0x03, 0x01]; // admin user — owner of what it creates, read grantee on the system tables + private static readonly byte[] UsersAccount = [0x02, 0x01]; // Users group — full grantee + private static readonly byte[] EngineAccount = [0x02, 0x03]; // Engine — owner of the system tables and the DAO containers + private static readonly byte[] CreatorAccount = [0x02, 0x04]; // Creator — the inheritable container grant's placeholder for an object's owner /// /// Creates a new, empty database at from scratch — no DAO/ADOX. Hand-builds the @@ -307,10 +295,18 @@ public static void CreateEmpty(string path, byte version = 0x02, Collation? coll 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) + + // Created now, as Access would. The SIDs are worked out from the finished page 0 — its creation date, + // collation and empty password field all feed the keystream — so nothing in it may change after this. + byte[] page0 = BuildDefinitionPage(version, format.IsAccdb, codePage, sortOrder, DateTime.Now.ToOADate()); + byte[] sidUsers = SidKeystream.MaskAccount(page0, UsersAccount); + byte[] sidAdmin = SidKeystream.MaskAccount(page0, AdminAccount); + byte[] sidEngine = SidKeystream.MaskAccount(page0, EngineAccount); + byte[] sidCreator = SidKeystream.MaskAccount(page0, CreatorAccount); + byte[][] seed = [ - BuildDefinitionPage(version, format.IsAccdb, codePage, sortOrder, - BitConverter.Int64BitsToDouble(SeedCreationDateBits)), + page0, 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 @@ -343,25 +339,25 @@ void Ace(int objectId, byte[] sid, int acm, bool inherit = false) // Catalog rows in the real stored order: DAO containers + MSysDb first, then the system tables, then // the DAO "SingleRecord" pseudo-object. Access's catalog bootstrap walks MSysObjects in this order. - InsertCatalogRow(msysObjects, tablesC, "Tables", type: 3, SystemFlag, parentId: root, owner: SidEngine); - InsertCatalogRow(msysObjects, databasesC, "Databases", type: 3, SystemFlag, parentId: root, owner: SidEngine); - InsertCatalogRow(msysObjects, relationshipsC, "Relationships", type: 3, SystemFlag, parentId: root, owner: SidEngine); - InsertCatalogRow(msysObjects, 0x10000000, "MSysDb", type: 2, SystemFlag, parentId: databasesC, owner: SidUsers); - InsertCatalogRow(msysObjects, objPage, "MSysObjects", type: 1, SystemFlag, parentId: tablesC, owner: SidEngine); - InsertCatalogRow(msysObjects, acesPage, "MSysACEs", type: 1, SystemFlag, parentId: tablesC, owner: SidEngine); - InsertCatalogRow(msysObjects, queriesPage, "MSysQueries", type: 1, SystemFlag, parentId: tablesC, owner: SidEngine); - InsertCatalogRow(msysObjects, relPage, "MSysRelationships", type: 1, SystemFlag, parentId: tablesC, owner: SidEngine); + InsertCatalogRow(msysObjects, tablesC, "Tables", type: 3, SystemFlag, parentId: root, owner: sidEngine); + InsertCatalogRow(msysObjects, databasesC, "Databases", type: 3, SystemFlag, parentId: root, owner: sidEngine); + InsertCatalogRow(msysObjects, relationshipsC, "Relationships", type: 3, SystemFlag, parentId: root, owner: sidEngine); + InsertCatalogRow(msysObjects, 0x10000000, "MSysDb", type: 2, SystemFlag, parentId: databasesC, owner: sidAdmin); + InsertCatalogRow(msysObjects, objPage, "MSysObjects", type: 1, SystemFlag, parentId: tablesC, owner: sidEngine); + InsertCatalogRow(msysObjects, acesPage, "MSysACEs", type: 1, SystemFlag, parentId: tablesC, owner: sidEngine); + InsertCatalogRow(msysObjects, queriesPage, "MSysQueries", type: 1, SystemFlag, parentId: tablesC, owner: sidEngine); + InsertCatalogRow(msysObjects, relPage, "MSysRelationships", type: 1, SystemFlag, parentId: tablesC, owner: sidEngine); InsertCatalogRow(msysObjects, unchecked((int)0x80000000), "SingleRecord", type: 9, flags: 0x10000000, parentId: relationshipsC); // Access-control rows (verified per-object-class masks) in the same object order as the catalog rows. - Ace(tablesC, SidCreator, 0x0F00FE, inherit: true); Ace(tablesC, SidUsers, 0x060001); Ace(tablesC, SidAdmin, 0x0FFEFF, inherit: true); - Ace(databasesC, SidUsers, 0x060000); - Ace(relationshipsC, SidCreator, 0x0F00FE, inherit: true); Ace(relationshipsC, SidUsers, 0x060001); Ace(relationshipsC, SidAdmin, 0x0FFFFF, inherit: true); - Ace(0x10000000, SidUsers, 0x06000E); Ace(0x10000000, SidAdmin, 0x00000E); // MSysDb - Ace(objPage, SidUsers, 0x060000); Ace(objPage, SidAdmin, 0x000014); // MSysObjects - Ace(acesPage, SidUsers, 0x060000); // MSysACEs (Users only) - Ace(queriesPage, SidUsers, 0x060000); Ace(queriesPage, SidAdmin, 0x000014); // MSysQueries - Ace(relPage, SidUsers, 0x0E0000); Ace(relPage, SidAdmin, 0x000014); // MSysRelationships + Ace(tablesC, sidCreator, 0x0F00FE, inherit: true); Ace(tablesC, sidAdmin, 0x060001); Ace(tablesC, sidUsers, 0x0FFEFF, inherit: true); + Ace(databasesC, sidAdmin, 0x060000); + Ace(relationshipsC, sidCreator, 0x0F00FE, inherit: true); Ace(relationshipsC, sidAdmin, 0x060001); Ace(relationshipsC, sidUsers, 0x0FFFFF, inherit: true); + Ace(0x10000000, sidAdmin, 0x06000E); Ace(0x10000000, sidUsers, 0x00000E); // MSysDb + Ace(objPage, sidAdmin, 0x060000); Ace(objPage, sidUsers, 0x000014); // MSysObjects + Ace(acesPage, sidAdmin, 0x060000); // MSysACEs (admin only) + Ace(queriesPage, sidAdmin, 0x060000); Ace(queriesPage, sidUsers, 0x000014); // MSysQueries + Ace(relPage, sidAdmin, 0x0E0000); Ace(relPage, sidUsers, 0x000014); // MSysRelationships // The system tables carry the indexes Access uses to navigate the catalog. ParentIdName is the first // real index, Id (the PK) second — matching real files. @@ -374,7 +370,7 @@ void Ace(int objectId, byte[] sid, int acm, bool inherit = false) db.CreateIndex("MSysRelationships", "szReferencedObject", [("szReferencedObject", false)]); // Complex-column system tables — ACE 12 and later only (see CreateComplexSystemTables). - if (version >= 0x02) CreateComplexSystemTables(db); + if (version >= 0x02) CreateComplexSystemTables(db, sidEngine); // 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 @@ -400,7 +396,7 @@ void Ace(int objectId, byte[] sid, int acm, bool inherit = false) /// 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) + private static void CreateComplexSystemTables(JetDatabase db, byte[] sidEngine) { db.CreateTable("MSysComplexColumns", MSysComplexColumnsColumns); // Index names, order and flags as the engine writes them: the ComplexID primary key first, then the @@ -411,19 +407,19 @@ private static void CreateComplexSystemTables(JetDatabase db) disallowNull: true, ignoreNulls: true); db.CreateIndex("MSysComplexColumns", "IdxFlatTableID", [("FlatTableID", false)], disallowNull: true, ignoreNulls: true); - MarkAsSystemTable(db, "MSysComplexColumns", SystemFlag); + MarkAsSystemTable(db, "MSysComplexColumns", SystemFlag, sidEngine); foreach ((string name, ColumnSpec[] columns) in MSysComplexTypeTables) { db.CreateTable(name, columns); - MarkAsSystemTable(db, name, ComplexStorageFlags); + MarkAsSystemTable(db, name, ComplexStorageFlags, sidEngine); } } /// 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) + private static void MarkAsSystemTable(JetDatabase db, string name, int flags, byte[] sidEngine) { TableDef definition = db.Catalog.FindTable(name) ?? throw new InvalidOperationException($"'{name}' was not found after creating it."); @@ -438,7 +434,7 @@ private static void MarkAsSystemTable(JetDatabase db, string name, int flags) && Convert.ToInt32(id, CultureInfo.InvariantCulture) == definition.DefinitionPage)) { values[flagsIndex] = flags; - values[ownerIndex] = SidEngine; + values[ownerIndex] = sidEngine; msysObjects.Update(rowId, values, new HashSet { flagsIndex, ownerIndex }); break; } diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index 53147ce1..1ebdd845 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -37,10 +37,10 @@ All integers little-endian unless noted; offsets are hex, relative to the struct | `0x18` | 4 | Global free-pages map pointer `[row:1][page:3]` (`0x00000100` = page 1 row 0) | | `0x1C` | 4 | Global released-pages map pointer `[row:1][page:3]` (`0x00000101` = page 1 row 1) | | `0x20`–`0x2C` | 4×4 | Catalog bootstrap pointers — `MSysObjects`/`MSysACEs`/`MSysQueries`/`MSysRelationships` TDEF pages (`2`/`3`/`4`/`5`); `0x20` = catalog root | -| `0x30`–`0x3B` | 12 | Reserved (zero) | +| `0x30`–`0x3B` | 12 | `0x30`/`0x34`: `MSysAccounts`/`MSysGroups` TDEF pages in a workgroup file (`6`/`7`), zero otherwise; `0x38` zero | | `0x3C` | 2 | ANSI code page (LE; `0x04E4` = 1252) | | `0x3E` | 4 | Database/encryption key (`0` = not encrypted) | -| `0x42` | 40 | Password (Jet4; Jet3 = 20) — also XOR `(int)creationDate` | +| `0x42` | 40 | Password (Jet4; Jet3 = 20) — also XOR `(int)creationDate`; `.accdb`: 40 × low byte of the `0x3E` key | | `0x6A` | 4 | Creating engine's build number (`0x000011A6` = 4518 on everything ACE writes; Jet 4 files carry their `msjet40.dll` build) | | `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) | diff --git a/src/LibRed/docs/format/page-00-database.md b/src/LibRed/docs/format/page-00-database.md index bcce89c6..b7a89859 100644 --- a/src/LibRed/docs/format/page-00-database.md +++ b/src/LibRed/docs/format/page-00-database.md @@ -17,10 +17,10 @@ | `0x18` | 4 | **Global free-pages map pointer** — `[row:1][page:3]`; `0x00000100` = page 1 row 0 in every file ACE writes ([page-05 §9.1](page-05-usage-maps.md)) | | `0x1C` | 4 | **Global released-pages map pointer** — `[row:1][page:3]`; `0x00000101` = page 1 row 1 ([page-05 §9.1](page-05-usage-maps.md)) | | `0x20`–`0x2C` | 4×4 | **System-catalog bootstrap pointers**: TDEF pages of `MSysObjects` / `MSysACEs` / `MSysQueries` / `MSysRelationships` = `2, 3, 4, 5`. `0x20` is the **catalog root** (how the engine finds `MSysObjects`). | -| `0x30`–`0x3B` | 12 | Zero in every file seen, but **not merely reserved**: ACE range-checks `0x30` and `0x34` as `[row:1][page:3]` pointers, against the largest possible page only (see below) | +| `0x30`–`0x3B` | 12 | In a workgroup file (`Jet System DB`), `0x30`/`0x34` hold the TDEF pages of `MSysAccounts`/`MSysGroups`; zero in an ordinary database, which has neither table. ACE range-checks `0x30` and `0x34` as `[row:1][page:3]` pointers, against the largest possible page only (see below) | | `0x3C` | 2 | **ANSI code page** of the collation's language — LE (`0x04E4` = 1252, `0x04E2` = 1250, `0` = none); see below | | `0x3E` | 4 | **Database (encryption) key** — 0 when there is no password | -| `0x42` | 40 | **Password** (Jet 4; Jet 3 = 20 bytes) — additionally masked by a creation-date-derived value, so an empty password does not read as zeroes | +| `0x42` | 40 | **Password** (Jet 4; Jet 3 = 20 bytes) — additionally masked by a creation-date-derived value, so an empty password does not read as zeroes. On an `.accdb`, 40 × the database key's low byte instead (§2) | | `0x6A` | 4 | **Creating engine's build number** — `0x000011A6` (4518) on everything ACE writes, but **not a constant**: Jet-4-authored files carry the build of the `msjet40.dll` that created them (see below) | | `0x6E` | 4 | **Default text collating sort order** — a 32-bit LCID with the version in its unused top byte: LANGID (2, LE, `0x0409` = 1033 en-US), **sort id** at `0x70`, **sort-order version** at `0x71` (0 = legacy table, 1 = the Access-2010 order). Byte-for-byte the same layout as a column descriptor's `0x0B`–`0x0E` | | `0x72` | 8 | **Database creation timestamp** — OLE automation `double` (days from 1899-12-30) | @@ -112,11 +112,13 @@ definition is a page. What the maps hold, and how ACE validates and follows the two may name any page up to the largest a file can have: `0x080000FF` (page 524,288, row 255) opens and `0x08000100` (page 524,289) makes the database unopenable. The file's own length is **not** consulted — a page ten past the end of a 353-page file, or page 500,000, opens as readily — which is looser than the "page must -exist" check `0x18` and `0x1C` get. `0x38` takes any value at all. The boundary sits where a `[row][page]` -reading puts it, and read as a plain page number it would be meaningless, so `0x30` and `0x34` are most likely -**record pointers** of the shape `0x18`/`0x1C` have, rather than page pointers like the catalog block after them. -What they point at is **not known**: every file examined carries zero, zero is accepted, and a compact zeroes a -poked value. A writer must leave all three as it found them. +exist" check `0x18` and `0x1C` get. `0x38` takes any value at all. + +**What they point at (verified):** the workgroup tables. In a workgroup file `0x30` holds the TDEF page of +`MSysAccounts` and `0x34` that of `MSysGroups` — pages 6 and 7, straight after the four catalog TDEFs at +`0x20`–`0x2C` — each with the row byte zero, so they read the same as a plain page number or as `[row][page]`. +`0x38` is zero there too. An ordinary database has neither table and carries zero in both; zero is accepted, +and a compact zeroes a value poked into one. A writer must leave all three as it found them. **Catalog bootstrap.** Reading the database is a two-step hop from page 0: the pointer at `0x20` gives the `MSysObjects` TDEF page (2), and `MSysObjects` then lists every other object (each table's row `Id` is *its* @@ -240,9 +242,12 @@ no-password files and an empty password that unmasks to the creation-date-derive base header mask — the exact inverse of the read. **Verified byte-identical to Access's own output** in the `0x42` field. This is password-only obfuscation — the data pages stay plaintext (`0x3E` key = 0); it is a *different* feature from Jet RC4 page encryption (§2.4), which the "Encode/Encrypt" menu applies. - - **ACE `.accdb`**: real encryption — this region is an encryption **verifier**, not recoverable - plaintext (an actual password decodes to random-looking bytes under the Jet 4 scheme). Recovering - it is a crypto attack, not format work. + - **ACE `.accdb`**: the password is not here — it lives in the EncryptionInfo descriptor at `0x299` + (§2.5). Under the same date and base masks the field decodes to **40 copies of the low byte of the + database key at `0x3E`**, and to zeroes while the file is unencrypted (verified against Access's + whole-file encryption, password change and removal, and against an Access-encrypted file). The field + changes with every new key, so Access's encryption moves the SID keystream (§2.3). `DatabaseEncryption` + writes it the same way on every `.accdb` set, change and removal. - **Writing it:** `DatabaseCreator.CreateEmpty(path, version, collation)` (and `LibRedConnection.CreateDatabase(connectionString, collation)`) set this pair, defaulting to General-Legacy. The chosen collation goes into page 0 *and* into the system tables' column descriptors, and @@ -361,44 +366,80 @@ values Access shows in the `SID` column: | `Admins` | group | `01-DB-87-93-20-81-4F-AB-38-…` (102 bytes) | These short SIDs are the well-known Access defaults (identical on every stock install — which is why a captured -SID cluster opens cross-PC). Object ownership in a database uses the **"user" form** (byte0 `0x03`) of -`Engine`/`Creator`. - -An on-disk SID in `MSysACEs.SID` / `MSysObjects.Owner` is the **workgroup account SID XOR'd with a per-file -keystream**, from the keystream's first byte (verified). Every SID in a file shares the one keystream, so a -2-byte SID always meets its first two bytes, and against the short SIDs the keystream reads as a **2-byte -mask**. E.g. with mask `24-CC`: `Users 02-01 ^ 24-CC = 26-CD`, `admin 03-01 ^ 24-CC = 27-CD` (read grantee), -`Engine 03-03 ^ 24-CC = 27-CF` (system-object owner), `Creator 03-04 ^ 24-CC = 27-C8` (inheritable container -grant). - -Access adds **102-byte SIDs** on first open, and they use the same keystream: under the file's short-SID mask -their first two bytes read `00 DB`, and two of them in one file differ by the same bytes in files from different -installs, which only a shared keystream allows (verified). Beyond byte 1 neither their plaintext nor the -keystream is established, so only the first two keystream bytes are recoverable from a file. +SID cluster opens cross-PC). In a database, **Engine** owns the system tables and the DAO containers; the +**admin** user owns the objects it creates and `MSysDb`, and holds the read grants on the system tables; the +**Users** group is the full-rights grantee; and **Creator** stands, in a container's inheritable grant, for +whoever creates an object there. Any other account of the workgroup — `Admins`, or a user or group added to +it — appears under its own `MSysAccounts` SID in the same way. + +**An on-disk SID in `MSysACEs.SID` / `MSysObjects.Owner` is the account's SID XOR a per-file RC4 keystream**, +from its first byte, whatever its length (verified: in a database DAO built under a workgroup, every owner and +grantee — short and 102-byte, a table created by a non-admin user included — decodes to that workgroup's +`MSysAccounts` entry exactly). Every SID in a file shares the one keystream, so against the short SIDs it reads +as a **2-byte mask**. E.g. with mask `25-CC`: `Users 02-01 ^ 25-CC = 27-CD` (full grantee), `admin 03-01 ^ 25-CC += 26-CD` (owner, read grantee), `Engine 02-03 ^ 25-CC = 27-CF` (system-object owner), `Creator 02-04 ^ 25-CC = +27-C8` (inheritable container grant). + +**102-byte SIDs** use the same keystream over their full length. The default workgroup's `Admins` group +(`01-DB-87-93-…`) is the usual one; another workgroup's account — such as an `Admins` group whose SID starts +`01-C5` — appears in a file used under that workgroup, beside the default one. **The mask is recoverable from the file, even though it is stored nowhere (verified).** `MSysObjects` is owned -by the `Engine` account (`03-03`) in every file, so `mask = MSysObjects.Owner ^ 03-03`, and every other short -account follows from it: an owner of `680E` gives mask `6B-0D`, under which that file's `690C` / `680C` / `6809` -are `Users` / `admin` / `Creator`. That is how a writer adding an object to a file it did not create gets the -SIDs right ([system-catalog §11](system-catalog.md)); the pair baked in below fits only the files this engine -creates itself. +by the `Engine` account (`02-03`) in every file, so `mask = MSysObjects.Owner ^ 02-03`, and every other short +account follows from it: an owner of `680E` gives mask `6A-0D`, under which that file's `690C` / `680C` / `6809` +are `admin` / `Users` / `Creator`. That is how a writer adding an object to a file it did not create gets the +SIDs right ([system-catalog §11](system-catalog.md)). The whole keystream also follows from page 0 alone, by the +derivation below, which is what serves when that row cannot be read. The keystream is **bound to the exact millisecond-precise creation-date `double`** at `0x72`: a file with self-consistent SIDs but a *different* creation date is rejected with *"Record(s) cannot be read; no read permission on 'MSysObjects'/'MSysACEs'"* (Jet 3112). Grafting a real file's date **and** SIDs together opens clean; either alone fails. Dates within the same second give unrelated keystreams. -How the keystream derives from the date is **not known**. It is not RC4 under a 4-byte key made from the date's -low half, high half, their XOR or its day count combined with any constant by XOR, addition or subtraction; nor -RC4, MD5 or SHA-1 keyed by the date (its `double` in either byte order, either half, its day, second or -millisecond count, its `FILETIME` or `SYSTEMTIME`, or the date as stored), by any span of page 0 decoded or raw, -or by the header's `C7 DA 39 6B` key combined with the date; nor MSVCRT `rand` or the VB `Rnd` generator seeded -from page 0. -`DatabaseCreator` therefore **bakes one verified `(SeedCreationDateBits, SidMask)` pair** (`0x40E68F1E8943D217` -+ `24-CC`) rather than computing it — the from-scratch analogue of the account-SID constants. Limitations -(deferred): every LibRed-created file reports the same creation instant, and only the **default** workgroup is -supported; per-file-random dates and custom/secured workgroups both need the date↔keystream derivation (reading -a custom `System.mdw` itself works — §2.4). +#### Deriving the RC4 key (verified) + +The key is folded from the **decoded header**, not from the date alone. The algorithm was located in +[Jackcess Encrypt's `JetPasswordHandler.createSidKey`](https://github.com/jahlborn/jackcessencrypt/blob/master/src/main/java/com/healthmarketscience/jackcess/crypt/impl/JetPasswordHandler.java), +with the region preparation in +[`BaseCryptCodecHandler.readPasswordRegion`](https://github.com/jahlborn/jackcessencrypt/blob/master/src/main/java/com/healthmarketscience/jackcess/crypt/impl/BaseCryptCodecHandler.java). +Its short-SID result matches the stored SIDs of Jet 4 `.mdb` and ACE `.accdb` files alike, including freshly +DAO-created ones: the page-0 header it reads is the Jet 4 family's, shared by both. + +1. Remove the fixed header mask (§2.1), yielding decoded page bytes `H`. +2. Copy **80 bytes** `H[0x42..0x92)` into `R`: the 40-byte password field **and 40 bytes after it**. +3. Remove the additional password mask from **only `R[0..40)`**: XOR with the repeating four + little-endian bytes of `(int)LEDouble(H[0x72..0x7A))`. Do not apply it to the second half. +4. Seed a 32-bit unsigned accumulator with `LE32(H[0x72..0x76))`: the **low four raw bytes of the + creation-date double**, not its integer day count. +5. For `i = 0..39`, fold `K ^= (uint)R[2*i] << (i % 24)`. +6. Encode `K` as **four little-endian bytes**, use those as a standard RC4 key, and generate the + keystream from byte zero. Each SID starts at byte zero again, irrespective of its length. + +The unusual part is step 5: it reads every **even byte of an 80-byte region**, not every byte of the +40-byte password field. Thus it also folds bytes from the engine build, collation, creation date and +following header area. For an empty password the first 40 prepared bytes are zero, but the second half +still contributes. This explains why trying the date's halves directly as RC4 keys did not work. + +For `WideTable.accdb`, date bits `0x40E68F1E8943D217` produce key **`0x8163D5CE`**, encoded +**`CE D5 63 81`**, whose RC4 stream begins **`25 CC`**: its `MSysObjects` owner `27 CF` is Engine `02 03`. + +**A change of key re-masks every stored SID (verified).** Each SID — short and 102-byte alike — becomes +`stored XOR oldStream XOR newStream` over its whole length, which needs no knowledge of its plaintext. +Setting or removing the legacy database password changes the 40-byte field the fold reads, so it changes the +key, and Access's own password change (DAO `NewPassword`) re-masks exactly so — every SID in +`MSysObjects.Owner` and `MSysACEs.SID`, the 102-byte ones over their full length. An `.accdb`'s whole-file +encryption changes the same field (it carries the database key's low byte, §2), so Access re-masks there +too; but there it can leave some `MSysObjects.Owner` values — the system tables' among them — un-re-masked, +so in an Access-encrypted `.accdb` an owner need not decode to any account. LibRed re-masks every SID. + +**Creation.** A new database is dated when it is created. Its page 0 is built complete first — creation +timestamp, collation, empty password field — and its SIDs are the default workgroup's XOR the stream that +header gives. Nothing the fold reads may change after the SIDs are derived. Creating a database under any other +workgroup is unverified. + +Opening a file through ACE's OLE DB provider is **not** a check of its SIDs: it also opens a file whose SIDs +belong to a different date. The evidence is byte-for-byte agreement with SIDs Access itself wrote. Access +desktop's permission-sensitive open of a LibRed-created file has not been tested. ### 2.4 Legacy Jet 3/4 RC4 page encryption (verified) diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index d2b73798..56056d10 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -75,14 +75,16 @@ > **A new object's rows are its container's inheritable grants (verified).** Each container — > `Tables` (`0x0F000001`, which holds queries too) and `Relationships` (`0x0F000003`) — carries `MSysACEs` > rows of its own, some marked `FInheritable`. A new object gets one row per account those inheritable rows - > name: the **Creator** account's grant becomes the grant to the object's **owner**, the Users account, and - > every other inheritable grant is copied for its own account — OR'd into the owner's row when it names - > Users too. The owner's row comes first. So the masks are the database's, not the object class's: - > - A fresh DAO/ACE database's `Tables` container grants Creator `0xF00FE` and admin `0xFFEFF` (its Users - > row, `0x60001`, is not inheritable), so a new table, view or query gets Users `0xF00FE`, admin `0xFFEFF`. - > - Northwind's `Tables` container also grants Users `0xFFEFF`, inheritable, so there both rows are `0xFFEFF`. - > - The `Relationships` container grants Creator `0xF00FE` and admin `0xFFFFF` in both, so a relationship - > gets Users `0xF00FE`, admin `0xFFFFF`. + > name: the **Creator** account's grant becomes the grant to the object's **owner** — the user who created + > it, the admin user for everything LibRed writes — and every other inheritable grant is copied for its own + > account, OR'd into the owner's row when it names the owner too. The owner's row comes first. So the masks + > are the database's, not the object class's: + > - A fresh DAO/ACE database's `Tables` container grants Creator `0xF00FE` and the Users group `0xFFEFF` + > (its admin row, `0x60001`, is not inheritable), so a new table, view or query gets admin `0xF00FE`, + > Users `0xFFEFF`. + > - Northwind's `Tables` container also grants admin `0xFFEFF`, inheritable, so there both rows are `0xFFEFF`. + > - The `Relationships` container grants Creator `0xF00FE` and Users `0xFFFFF` in both, so a relationship + > gets admin `0xF00FE`, Users `0xFFFFF`. > > The route does not matter: ACE's SQL DDL and the Access UI give the same rows in the same database. > @@ -92,9 +94,9 @@ > **The SIDs are per file, and have to be read out of the file being written (verified).** An on-disk SID is > a workgroup account SID XOR'd with a mask that differs per database and is stored nowhere > ([page-00 §2.3](page-00-database.md)). It is recoverable all the same: `MSysObjects` is owned by the - > **Engine** account (`03-03`) in every file, so `mask = MSysObjects.Owner ^ 03-03`, and every other account - > follows from it — a file whose `MSysObjects.Owner` is `680E` has mask `6B-0D`, under which `690C` is - > Users `02-01`, `680C` admin `03-01` and `6809` Creator `03-04`. An object written with a **different** + > **Engine** account (`02-03`) in every file, so `mask = MSysObjects.Owner ^ 02-03`, and every other account + > follows from it — a file whose `MSysObjects.Owner` is `680E` has mask `6A-0D`, under which `690C` is + > admin `03-01`, `680C` Users `02-01` and `6809` Creator `02-04`. An object written with a **different** > file's SIDs carries an owner that names no account in the file it sits in, so a writer adding an object to > a database it did not create must take the mask from that database. diff --git a/test/LibRed.Core.AccessTests/JetPasswordSidAccessTests.cs b/test/LibRed.Core.AccessTests/JetPasswordSidAccessTests.cs new file mode 100644 index 00000000..b703f895 --- /dev/null +++ b/test/LibRed.Core.AccessTests/JetPasswordSidAccessTests.cs @@ -0,0 +1,93 @@ +using System.Reflection; +using LibRed.Crypto; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Core.Tests; + +/// +/// The legacy Jet database password is half of the header region the SID keystream is folded from (page-00 +/// §2.3), so setting one re-masks every stored SID. LibRed's has +/// to write exactly what Access's own password change does: DAO's NewPassword on one copy, LibRed on +/// another, and every MSysObjects.Owner and MSysACEs.SID — and the password field — must agree. +/// +[Collection(AceCollection.Name)] +public class JetPasswordSidAccessTests +{ + private const string Password = "Test1"; + + [Fact] + public void A_jet_password_remasks_a_dao_databases_sids_as_access_does() + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + + string source = TemporaryDatabase.CreatePath("jetpw_dao_", ".mdb"); + try + { + object database = Invoke(engine!, "CreateDatabase", source, ";LANGID=0x0409;CP=1252;COUNTRY=0", 64)!; // dbVersion40 + Invoke(database, "Close"); + Compare(engine!, source); + } + finally { TemporaryDatabase.Delete(source); } + } + + [Fact] + public void A_jet_password_remasks_a_libred_databases_sids_as_access_does() + { + object? engine = AceTestDatabase.CreateDaoEngine(); + Assert.SkipWhen(engine is null, "DAO is not registered in this bitness."); + + string source = TemporaryDatabase.CreatePath("jetpw_libred_", ".mdb"); + try + { + DatabaseCreator.CreateEmpty(source, version: 0x01); + Compare(engine!, source); + } + finally { TemporaryDatabase.Delete(source); } + } + + private static void Compare(object engine, string source) + { + string byAccess = TemporaryDatabase.CopyPath(source, "jetpw_access_"); + string byLibRed = TemporaryDatabase.CopyPath(source, "jetpw_ours_"); + try + { + object database = Invoke(engine, "OpenDatabase", byAccess, true, false, "")!; + Invoke(database, "NewPassword", "", Password); + Invoke(database, "Close"); + AceTestDatabase.ReleaseAbandonedComObjects(); + + using (var db = JetDatabase.Open(byLibRed, readOnly: false, exclusive: true)) + DatabaseEncryption.SetJetPassword(db, Password); + + List access = StoredSids(byAccess), ours = StoredSids(byLibRed); + Assert.NotEqual(StoredSids(source, null)[0], access[0]); // Access did re-mask them + Assert.Equal(access, ours); + Assert.Equal(File.ReadAllBytes(byAccess)[0x42..0x6A], File.ReadAllBytes(byLibRed)[0x42..0x6A]); + } + finally + { + TemporaryDatabase.Delete(byAccess); + TemporaryDatabase.Delete(byLibRed); + } + } + + /// Every MSysObjects.Owner and MSysACEs.SID, in stored order. + private static List StoredSids(string path, string? password = Password) + { + using var db = JetDatabase.Open(path, password: password); + var sids = new List(); + foreach ((string table, string column) in new[] { ("MSysObjects", "Owner"), ("MSysACEs", "SID") }) + { + var t = db.OpenTable(table); + int index = t.Definition.RequireColumn(column).Index; + foreach (object?[] row in t.Rows()) + if (row[index] is byte[] sid) sids.Add(sid); + } + return sids; + } + + private static object? Invoke(object target, string member, params object?[] args) => + target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); +} diff --git a/test/LibRed.Core.Tests/DatabaseCreatorTests.cs b/test/LibRed.Core.Tests/DatabaseCreatorTests.cs index a1d9782e..28f70b53 100644 --- a/test/LibRed.Core.Tests/DatabaseCreatorTests.cs +++ b/test/LibRed.Core.Tests/DatabaseCreatorTests.cs @@ -203,6 +203,52 @@ public void Synthesized_page0_pairs_the_identifier_with_the_version(byte version Assert.Equal(minor, page[0x15]); } + // A new database is dated when it is made, and its SIDs are the default workgroup's, masked with the keystream + // that date and the rest of its page 0 give (page-00 §2.3): Engine owns the system tables, admin owns MSysDb, + // and every grant names admin, the Users group or the Creator placeholder — in an .mdb and an .accdb alike. + [Theory] + [InlineData((byte)0x01, ".mdb")] + [InlineData((byte)0x02, ".accdb")] + [InlineData((byte)0x03, ".accdb")] + public void A_created_database_is_dated_now_and_its_sids_follow_its_own_page0(byte version, string extension) + { + string path = TemporaryDatabase.CreatePath("libred_sids_", extension); + try + { + DateTime before = DateTime.Now; + DatabaseCreator.CreateEmpty(path, version); + byte[] page0 = File.ReadAllBytes(path)[..4096]; + + using var db = JetDatabase.Open(path); + Assert.InRange(db.DefinitionPage.DatabaseCreationDate, before.AddSeconds(-1), DateTime.Now.AddSeconds(1)); + + // The default workgroup's accounts, as System.mdw's MSysAccounts holds them. + byte[] engine = LibRed.Crypto.SidKeystream.MaskAccount(page0, [0x02, 0x03]); + byte[][] grantees = + [ + LibRed.Crypto.SidKeystream.MaskAccount(page0, [0x03, 0x01]), // admin user + LibRed.Crypto.SidKeystream.MaskAccount(page0, [0x02, 0x01]), // Users group + LibRed.Crypto.SidKeystream.MaskAccount(page0, [0x02, 0x04]), // Creator + ]; + + Table objects = db.OpenTable("MSysObjects"); + int name = objects.Definition.RequireColumn("Name").Index, owner = objects.Definition.RequireColumn("Owner").Index; + foreach (object?[] row in objects.Rows()) + if (row[name] is string table && table.StartsWith("MSys", StringComparison.Ordinal)) + Assert.Equal(table == "MSysDb" ? grantees[0] : engine, row[owner]); + + Table aces = db.OpenTable("MSysACEs"); + int sid = aces.Definition.RequireColumn("SID").Index; + Assert.All(aces.Rows(), row => Assert.Contains(grantees, g => g.SequenceEqual((byte[])row[sid]!))); + + var (admin, users, creator) = db.Catalog.SecuritySids; + Assert.Equal(grantees[0], admin); + Assert.Equal(grantees[1], users); + Assert.Equal(grantees[2], creator); + } + finally { TemporaryDatabase.Delete(path); } + } + [Fact] public void Synthesized_page0_round_trips_through_the_reader() { diff --git a/test/LibRed.Core.Tests/LegacyJetPasswordTests.cs b/test/LibRed.Core.Tests/LegacyJetPasswordTests.cs index e6934126..6b6c0c76 100644 --- a/test/LibRed.Core.Tests/LegacyJetPasswordTests.cs +++ b/test/LibRed.Core.Tests/LegacyJetPasswordTests.cs @@ -93,6 +93,103 @@ public void RemoveJetPassword_matches_plain() finally { TemporaryDatabase.Delete(tmp); } } + // The password field is half of what the SID keystream is folded from (page-00 §2.3), so setting a password + // must re-mask every stored SID to the new keystream — the system tables' owner still has to be Engine under + // it — and removing the password must give back the very bytes the file started with. + [Fact] + public void A_password_change_remasks_every_stored_sid() + { + string tmp = CreateJet4(); + try + { + List original = StoredSids(tmp, null); + + using (JetDatabase db = OpenExclusive(tmp)) + DatabaseEncryption.SetJetPassword(db, "Test1"); + + byte[] page0 = File.ReadAllBytes(tmp)[..4096]; + List masked = StoredSids(tmp, "Test1"); + Assert.Equal(original.Count, masked.Count); + Assert.NotEqual(original[0], masked[0]); + using (var db = JetDatabase.Open(tmp, password: "Test1")) + Assert.Equal(SidKeystream.MaskAccount(page0, [0x02, 0x03]), OwnerOf(db, "MSysObjects")); // Engine + + using (JetDatabase db = OpenExclusive(tmp, "Test1")) + DatabaseEncryption.RemoveJetPassword(db); + + Assert.Equal(original, StoredSids(tmp, null)); + } + finally { TemporaryDatabase.Delete(tmp); } + } + + // An .accdb's whole-file encryption writes the same 0x42 field, as Access does: 40 copies of the low byte of the + // new database key at 0x3E (zero once the password is removed). That moves the SID keystream just as a Jet + // password does, so every set, change and removal re-masks the SIDs, and removal gives back the original bytes. + [Fact] + public void An_accdb_password_change_writes_the_key_field_and_remasks_every_stored_sid() + { + string tmp = TemporaryDatabase.CreatePath("libred_accdbpw_", ".accdb"); + DatabaseCreator.CreateEmpty(tmp); + try + { + byte[] originalField = File.ReadAllBytes(tmp)[0x42..(0x42 + 40)]; + List original = StoredSids(tmp, null); + + using (JetDatabase db = OpenExclusive(tmp)) + DatabaseEncryption.SetPassword(db, "Test1", AccessEncryption.Agile); + AssertKeyFieldAndEngineOwner(tmp, "Test1"); + + using (JetDatabase db = OpenExclusive(tmp, "Test1")) + DatabaseEncryption.ChangePassword(db, "Ab", AccessEncryption.OfficeStandardAes); + AssertKeyFieldAndEngineOwner(tmp, "Ab"); + + using (JetDatabase db = OpenExclusive(tmp, "Ab")) + DatabaseEncryption.RemovePassword(db); + Assert.Equal(originalField, File.ReadAllBytes(tmp)[0x42..(0x42 + 40)]); + Assert.Equal(original, StoredSids(tmp, null)); + } + finally { TemporaryDatabase.Delete(tmp); } + } + + private static void AssertKeyFieldAndEngineOwner(string path, string password) + { + byte[] page0 = File.ReadAllBytes(path)[..4096]; + byte[] header = (byte[])page0.Clone(); + ReadOnlySpan mask = Formats.JetFormatBase.PageZeroHeaderMask; + for (int i = 0; i < mask.Length; i++) header[Formats.JetFormatBase.PageZeroHeaderMaskStart + i] ^= mask[i]; + byte[] dateMask = BitConverter.GetBytes((int)BitConverter.ToDouble(header, 0x72)); + byte[] field = [.. Enumerable.Range(0, 40).Select(i => (byte)(header[0x42 + i] ^ dateMask[i % 4]))]; + Assert.All(field, b => Assert.Equal(header[0x3E], b)); + + using var db = JetDatabase.Open(path, password: password); + Assert.Equal(SidKeystream.MaskAccount(page0, [0x02, 0x03]), OwnerOf(db, "MSysObjects")); // Engine + } + + private static JetDatabase OpenExclusive(string path, string password) => + JetDatabase.Open(path, readOnly: false, password: password, exclusive: true); + + /// Every MSysObjects.Owner and MSysACEs.SID, in stored order. + private static List StoredSids(string path, string? password) + { + using var db = JetDatabase.Open(path, password: password); + var sids = new List(); + foreach ((string table, string column) in new[] { ("MSysObjects", "Owner"), ("MSysACEs", "SID") }) + { + var t = db.OpenTable(table); + int index = t.Definition.RequireColumn(column).Index; + foreach (object?[] row in t.Rows()) + if (row[index] is byte[] sid) sids.Add(sid); + } + return sids; + } + + private static byte[] OwnerOf(JetDatabase db, string name) + { + var objects = db.OpenTable("MSysObjects"); + int nameIndex = objects.Definition.RequireColumn("Name").Index, owner = objects.Definition.RequireColumn("Owner").Index; + return (byte[])objects.Rows().First(r => r[nameIndex] as string == name)[owner]!; + } + [Fact] public void SetJetEncoding_roundtrips_and_stays_readable() { diff --git a/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs b/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs index fd8b43b2..dc023764 100644 --- a/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs @@ -10,7 +10,7 @@ namespace LibRed.Engine.Tests; /// The MSysACEs rows a new object gets are its container's inheritable grants, the Creator's becoming the /// owner's (system-catalog §11) — so they differ from database to database. Each is checked in two whose Tables /// containers grant differently: a fresh DAO database, whose owner row comes out 0xF00FE, and Northwind, -/// whose Tables container also grants Users 0xFFEFF. +/// whose Tables container also grants the admin user 0xFFEFF. /// [Collection(AceCollection.Name)] public class PermissionRowsAccessTests(ITestOutputHelper output) : TempDatabaseTest From 0d470f9e08d996d17926c580848db93282dd7ae7 Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Sun, 4 Oct 2026 23:54:45 +0800 Subject: [PATCH 88/88] Consolidate LibRed.Core into one class per concern Fold the pieces of the Core that did one job in several places into a single dedicated path each, keeping the code itself as it was: - TableDef, TableDefinitionPage, TdefBuilder, TdefRegions and TdefChainReader become TableDefinition. - TableCreator, CatalogWriter, ViewCreator and DatabaseCreator become SchemaEditor and JetCatalog; creating a database is JetDatabase.Create. - IndexWriter, IndexPageReader and the index-key encoder and decoder become IndexTree and IndexKeyCodec. - The long-value reader, writer and format become LongValueStore. - RowEncoder, RowDecoder, RowLayout and RecordLayout become RowCodec. - UsageMapWriter and UsageMapBits fold into UsageMap; the allocator is the channel's. - The flag, type and attribute enums move into their own files, and the spec records (ColumnSpec, IndexSpec, LongValueColumnSpec) out of the builders. The declaration of a column now means the same thing on every DDL route: - ADD COLUMN stamps a NUMERIC/DECIMAL precision of 0 as ACE's 18 and refuses a precision or scale ACE refuses, as CREATE TABLE does; ALTER COLUMN does the same on both of its entry points. - ADD COLUMN writes the compression-capable flag for a column declared WITH COMPRESSION, which the SQL front end already hands it. Add ColumnDeclarationTests for both, and update the engine, ADO, EF Core and test projects, and the instruction files, to the new names. Co-Authored-By: Claude Sonnet 5.5 --- .claude/settings.json | 10 + AGENTS.md | 16 +- CLAUDE.md | 19 +- src/LibRed/LibRed.Ado/LibRedConnection.cs | 4 +- src/LibRed/LibRed.Ado/LibRedDataReader.cs | 6 +- src/LibRed/LibRed.Core/Catalog/ColumnDef.cs | 4 +- src/LibRed/LibRed.Core/Catalog/ColumnSpec.cs | 77 + .../LibRed.Core/Catalog/ComplexAttachment.cs | 189 ++ .../LibRed.Core/Catalog/ComplexColumn.cs | 167 +- src/LibRed/LibRed.Core/Catalog/IndexDef.cs | 20 +- src/LibRed/LibRed.Core/Catalog/IndexSpec.cs | 11 + src/LibRed/LibRed.Core/Catalog/JetCatalog.cs | 1401 +++++++++---- .../LibRed.Core/Catalog/LogicalIndexDef.cs | 24 +- .../Catalog/LongValueColumnSpec.cs | 7 + src/LibRed/LibRed.Core/Catalog/NameMap.cs | 245 ++- .../LibRed.Core/Catalog/PropertyBlob.cs | 205 +- .../LibRed.Core/Catalog/RecordLayout.cs | 92 - .../LibRed.Core/Catalog/RelationshipSpec.cs | 4 +- .../SchemaEditor.cs} | 1774 +++++------------ src/LibRed/LibRed.Core/Catalog/StoredQuery.cs | 57 + src/LibRed/LibRed.Core/Catalog/TableDef.cs | 90 - .../LibRed.Core/Catalog/TableDefinition.cs | 1698 ++++++++++++++++ src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs | 682 ------- src/LibRed/LibRed.Core/Catalog/ViewSpec.cs | 7 +- .../LibRed.Core/Crypto/AgileEncryption.cs | 209 +- .../LibRed.Core/Crypto/DatabaseEncryption.cs | 173 +- .../LibRed.Core/Crypto/JetLegacyEncryption.cs | 4 +- .../Crypto/OfficeStandardEncryption.cs | 393 ++-- src/LibRed/LibRed.Core/Crypto/SidKeystream.cs | 47 +- .../LibRed.Core/Formats/CatalogFormat.cs | 24 - src/LibRed/LibRed.Core/Formats/ColumnFlags.cs | 52 + .../Formats/EncryptionInfoFlags.cs | 17 + .../LibRed.Core/Formats/ForeignKeyType.cs | 19 + .../{IndexFlags.cs => IndexAttributes.cs} | 21 +- .../LibRed.Core/Formats/IndexBlockFormat.cs | 49 - .../LibRed.Core/Formats/IndexColumnOrder.cs | 8 + .../LibRed.Core/Formats/IndexInfoType.cs | 9 + .../LibRed.Core/Formats/IndexKeyFlags.cs | 49 - src/LibRed/LibRed.Core/Formats/Jet3Format.cs | 9 +- src/LibRed/LibRed.Core/Formats/Jet4Format.cs | 204 +- .../LibRed.Core/Formats/JetFormatBase.cs | 691 +++++-- .../LibRed.Core/Formats/LongValueFormat.cs | 62 - .../LibRed.Core/Formats/ObjectAttributes.cs | 34 + src/LibRed/LibRed.Core/Formats/ObjectType.cs | 49 + .../LibRed.Core/Formats/QueryAttribute.cs | 49 + .../LibRed.Core/Formats/QueryDefType.cs | 18 + .../LibRed.Core/Formats/QueryOperation.cs | 38 + .../LibRed.Core/Formats/QueryOptions.cs | 28 + .../LibRed.Core/Formats/RelationshipAction.cs | 19 + .../LibRed.Core/Formats/RelationshipFlags.cs | 17 +- src/LibRed/LibRed.Core/Formats/RowPointer.cs | 43 - .../LibRed.Core/Formats/RowSlotFlags.cs | 17 + .../LibRed.Core/Formats/StoredQueryFormat.cs | 84 - .../LibRed.Core/Formats/UsageMapType.cs | 11 + src/LibRed/LibRed.Core/IO/PageBuffer.cs | 22 +- src/LibRed/LibRed.Core/IO/PageChannel.cs | 58 +- src/LibRed/LibRed.Core/JetDatabase.cs | 181 +- src/LibRed/LibRed.Core/LibRed.Core.csproj | 3 + src/LibRed/LibRed.Core/Pages/DataPage.cs | 276 ++- .../Pages/DatabaseDefinitionPage.cs | 260 ++- src/LibRed/LibRed.Core/Pages/Page.cs | 2 +- .../LibRed.Core/Pages/TableDefinitionPage.cs | 451 ----- .../LibRed.Core/Pages/TdefChainReader.cs | 88 - src/LibRed/LibRed.Core/Pages/TdefRegions.cs | 67 - src/LibRed/LibRed.Core/Storage/BitmapBits.cs | 79 + .../LibRed.Core/Storage/CalculatedValue.cs | 30 +- .../LibRed.Core/Storage/CatalogWriter.cs | 97 - .../LibRed.Core/Storage/DatabaseCreator.cs | 535 ----- src/LibRed/LibRed.Core/Storage/IndexCursor.cs | 21 +- .../LibRed.Core/Storage/IndexKeyCodec.cs | 690 +++++++ .../LibRed.Core/Storage/IndexKeyDecoder.cs | 136 -- .../LibRed.Core/Storage/IndexKeyEncoder.cs | 367 ---- .../LibRed.Core/Storage/IndexPageReader.cs | 140 -- .../Storage/{IndexWriter.cs => IndexTree.cs} | 405 ++-- .../Storage/JetIndexKeyChecksum.cs | 78 - .../LibRed.Core/Storage/JetKanaSection.cs | 32 +- .../LibRed.Core/Storage/JetLocaleTailoring.cs | 5 +- .../LibRed.Core/Storage/JetTextCollation.cs | 225 ++- .../LibRed.Core/Storage/JetTextCollationV1.cs | 130 +- .../LibRed.Core/Storage/JetTextComparer.cs | 6 +- .../LibRed.Core/Storage/LongValueReader.cs | 153 -- .../LibRed.Core/Storage/LongValueStore.cs | 333 ++++ .../LibRed.Core/Storage/LongValueWriter.cs | 206 -- .../LibRed.Core/Storage/PageAllocator.cs | 424 ++-- src/LibRed/LibRed.Core/Storage/RowCodec.cs | 668 +++++++ src/LibRed/LibRed.Core/Storage/RowDecoder.cs | 226 --- src/LibRed/LibRed.Core/Storage/RowEncoder.cs | 298 --- src/LibRed/LibRed.Core/Storage/RowId.cs | 10 +- src/LibRed/LibRed.Core/Storage/RowInserter.cs | 578 ++---- src/LibRed/LibRed.Core/Storage/RowLayout.cs | 112 -- .../Storage/RowRelocationReader.cs | 66 - src/LibRed/LibRed.Core/Storage/Table.cs | 120 +- src/LibRed/LibRed.Core/Storage/TableCursor.cs | 12 +- .../LibRed.Core/Storage/Types/JetTypeCodec.cs | 117 +- src/LibRed/LibRed.Core/Storage/UsageMap.cs | 602 ++++-- .../LibRed.Core/Storage/UsageMapBits.cs | 48 - .../LibRed.Core/Storage/UsageMapWriter.cs | 346 ---- src/LibRed/LibRed.Core/Storage/ViewCreator.cs | 280 --- .../Internal/LibRedDatabaseModelFactory.cs | 6 +- .../Storage/Internal/LibRedDatabaseCreator.cs | 2 +- .../Execution/AccessTypeMapper.cs | 5 +- .../Execution/ExpressionEvaluator.cs | 4 +- .../LibRed.Engine/Execution/ParameterBag.cs | 9 +- .../Execution/StatementExecutor.cs | 57 +- .../LibRed.Engine/Planning/ColumnPruning.cs | 6 +- .../LibRed.Engine/Planning/IndexSelection.cs | 6 +- .../LibRed.Engine/Planning/QueryPlanner.cs | 10 +- .../LibRed.Engine/Planning/ViewExpander.cs | 51 +- src/LibRed/LibRed.Engine/QueryEngine.cs | 25 +- .../Schema/CatalogSchemaProvider.cs | 6 +- .../LibRed.Engine/Schema/InformationSchema.cs | 12 +- .../LibRed.Engine/Schema/SchemaRowsets.cs | 88 +- src/LibRed/LibRed.Sql/Ast/Statements.cs | 4 +- src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 | 2 +- src/LibRed/README.md | 43 +- src/LibRed/docs/design/index-key-checksum.md | 4 +- src/LibRed/docs/design/transactions.md | 2 +- src/LibRed/docs/format/README.md | 25 +- src/LibRed/docs/format/appendix-structures.md | 19 +- src/LibRed/docs/format/data-types.md | 8 +- src/LibRed/docs/format/long-values.md | 8 +- src/LibRed/docs/format/page-00-database.md | 53 +- .../docs/format/page-01-data-and-rows.md | 4 +- src/LibRed/docs/format/page-02a-tdef.md | 14 +- src/LibRed/docs/format/page-02b-columns.md | 2 +- .../docs/format/page-02d-constraints.md | 8 +- .../format/page-02e-calculated-columns.md | 6 +- .../docs/format/page-03-04-index-btree.md | 25 +- src/LibRed/docs/format/page-05-usage-maps.md | 18 +- .../docs/format/page-08-released-tdef.md | 2 +- .../docs/format/page-09-released-data.md | 2 +- src/LibRed/docs/format/system-catalog.md | 25 +- src/LibRed/docs/functions.md | 2 +- .../Query/NorthwindJoinQueryJetTest.cs | 6 +- .../Query/NorthwindJoinQueryLibRedTest.cs | 6 +- .../Query/NorthwindJoinQueryLibRedTest.cs | 6 +- .../CreateDatabaseDualTableTests.cs | 4 +- test/LibRed.Benchmarks/Harness/Corpus.cs | 6 +- .../Suites/CatalogBenchmarks.cs | 4 +- .../LibRed.Benchmarks/Suites/DdlBenchmarks.cs | 4 +- .../Suites/StorageBenchmarks.cs | 6 +- .../AceCreatedDatabaseTests.cs | 4 +- .../AceDateTime2UpgradeTests.cs | 32 +- .../AceDdlOnLibRedDatabaseProbeTest.cs | 8 +- .../AceDecimalDeclarationProbeTest.cs | 8 +- .../AcePreEpochDateProbeTest.cs | 6 +- .../AceVbaConversionProbeTest.cs | 6 +- .../ActionQueryProcedureAccessTests.cs | 24 +- .../AppendOnlyDropAccessTests.cs | 14 +- .../ArabicShaddaCollationAccessTests.cs | 4 +- .../AstralCollationProbeTest.cs | 18 +- .../BigIntKeyEncodingTests.cs | 17 +- .../CalculatedColumnAccessTests.cs | 38 +- .../ChainedLongValueAccessTests.cs | 26 +- .../CjkSortOrderGeneratorTest.cs | 4 +- .../CollationQueryProbeTests.cs | 8 +- .../CollationSurveyProbeTests.cs | 4 +- .../CollationV1PatchProbeTests.cs | 26 +- .../ColumnRetypeRelayAccessTests.cs | 6 +- .../ColumnWidthLimitAccessTests.cs | 4 +- .../CommitByteTableTests.cs | 29 +- .../ComplexDeleteCascadeProbeTest.cs | 9 +- .../ComplexDuplicateValueProbeTest.cs | 4 +- .../CompositeIndexOrderingAccessTests.cs | 6 +- .../CompressedTextAccessTests.cs | 32 +- .../ContractionProbeTest.cs | 6 +- .../ControlCharacterKeyAccessTests.cs | 6 +- .../CreatedDatabaseCollationAccessTests.cs | 164 +- .../CreatedJet4DatabaseAccessTests.cs | 4 +- .../DatabaseEncryptionTests.cs | 4 +- .../DateTime2KeyEncodingTests.cs | 17 +- .../DecimalKeyEncodingTests.cs | 9 +- .../DeleteAccessTests.cs | 7 +- .../DerivedTableViewAccessTests.cs | 8 +- .../DropColumnConstraintAccessTests.cs | 4 +- .../DropTableParityAccessTests.cs | 26 +- .../DropViewAccessTests.cs | 6 +- .../DuplicateIndexKeyProbeTest.cs | 25 +- .../FixedFlagHonouredAccessTests.cs | 4 +- .../FixedWidthOverflowAccessTests.cs | 6 +- .../FormatVersionRaiseAccessTests.cs | 18 +- .../GeneralV1CollationAccessTests.cs | 10 +- .../GlobalMapPointerAccessTests.cs | 131 +- .../GuidColumnStorageAccessTests.cs | 6 +- .../GuidKeyEncodingTests.cs | 14 +- .../IndexColumnCountAccessTests.cs | 10 +- .../IndexKeyTruncationAccessTests.cs | 6 +- .../IndexMaintenanceAccessTests.cs | 8 +- .../IndexOrderingAccessTests.cs | 10 +- .../IndexSplitAccessTests.cs | 33 +- .../IndexUsageMapTests.cs | 53 +- .../JetPasswordSidAccessTests.cs | 8 +- .../KeyChecksumProbeTest.cs | 42 +- .../Latin1SymbolCollationAccessTests.cs | 8 +- .../LibRed.Core.AccessTests.csproj | 3 +- .../LocaleCollationAccessTests.cs | 226 --- .../LocaleFixtureCollationProbeTest.cs | 12 +- .../LongTextStorageAccessTests.cs | 26 +- .../LongValueLengthAccessTests.cs | 6 +- .../LongValuePackingAccessTests.cs | 23 +- .../MemoKeyEncodingTests.cs | 6 +- .../MixedCompressionAccessTests.cs | 15 +- .../MultiPageTdefParityAccessTests.cs | 17 +- .../OwnedMapRecycleAccessTests.cs | 19 +- .../PackedLongValueReleaseAccessTests.cs | 19 +- .../RecordSizeAccessTests.cs | 14 +- .../RelationshipLinkageAccessTests.cs | 4 +- .../ReleaseAtCloseAccessTests.cs | 87 +- .../RenameNameValidationAccessTests.cs | 6 +- .../ReverseDiacriticProbeTest.cs | 14 +- .../RowInserterTests.cs | 4 +- .../SortKeyComparisonProbeTest.cs | 8 +- .../SortKeyTableV1OverrideGeneratorTest.cs | 6 +- .../SpanishCollationProbeTest.cs | 4 +- .../StoredQueryKindAccessTests.cs | 15 +- .../TailoringGeneratorProbeTest.cs | 29 +- .../TdefReserveSpillAccessTests.cs | 9 +- .../TextKeyEncodingTests.cs | 10 +- .../ThaiReorderProbeTest.cs | 4 +- .../TransactionPhysicalRollbackAccessTests.cs | 3 +- ...actionSavepointAndEncryptionAccessTests.cs | 3 +- .../UpdateAccessTests.cs | 3 +- .../UsageMapGrowthTests.cs | 25 +- .../V1TailoringProbeTest.cs | 6 +- .../VariableColumnHighWaterAccessTests.cs | 87 +- .../WideMemoUsageMapAccessTests.cs | 28 +- .../WideTableUsageMapTests.cs | 46 +- .../LibRed.Core.Tests/AgileEncryptionTests.cs | 21 +- .../AllocatorAndLvalOwnershipTests.cs | 59 +- .../BinaryIndexKeyAccessTests.cs | 12 +- test/LibRed.Core.Tests/CatalogDumpTests.cs | 4 +- test/LibRed.Core.Tests/CollationTests.cs | 28 +- .../ColumnCollationInheritanceTests.cs | 6 +- .../ColumnDeclarationTests.cs | 169 ++ .../ColumnDescriptorFlagTests.cs | 20 +- .../ComplexAutoNumberRoundTripTests.cs | 14 +- test/LibRed.Core.Tests/ComplexColumnTests.cs | 48 +- .../CorruptFileSweepProbeTests.cs | 10 +- .../CorruptUsageMapPointerTests.cs | 10 +- test/LibRed.Core.Tests/DataPageTests.cs | 19 +- .../LibRed.Core.Tests/DatabaseCreatorTests.cs | 69 +- .../DatabaseDefinitionPageTests.cs | 27 +- .../DropTableReclamationTests.cs | 39 +- .../ForeignKeyLinkageTests.cs | 123 +- .../FunctionalTestsSmokeTest.cs | 4 +- .../GeneralV1CollationTests.cs | 8 +- .../LibRed.Core.Tests/GlobalMapGrowthTests.cs | 55 +- .../GlobalMapPointerTests.cs | 185 +- .../GlobalReferenceFreeMapTests.cs | 46 +- ...yEncoderTests.cs => IndexKeyCodecTests.cs} | 55 +- .../IndexKeyTrailerComparisonTests.cs | 29 +- .../IndexLeafParseCacheTests.cs | 6 +- test/LibRed.Core.Tests/IndexTests.cs | 6 +- .../IndexTraversalCorruptionTests.cs | 63 +- .../JetDatabaseLifetimeTests.cs | 19 +- .../JetLegacyEncryptionTests.cs | 5 +- .../JetTypeCodecBoundaryTests.cs | 21 +- .../LegacyJetPasswordTests.cs | 67 +- .../LibRed.Core.Tests.csproj | 3 +- .../LongValueCorruptionTests.cs | 45 +- .../MultiPageDefinitionTests.cs | 12 +- .../MultiPageTableDefinitionTests.cs | 5 +- test/LibRed.Core.Tests/NameMapTests.cs | 27 +- .../OfficeStandardEncryptionTests.cs | 33 +- .../OfficeStandardVariantReadTests.cs | 11 +- .../PageAndRowCorruptionTests.cs | 27 +- test/LibRed.Core.Tests/PageCacheTests.cs | 5 +- test/LibRed.Core.Tests/PageChannelTests.cs | 4 +- .../PageChannelWriteTests.cs | 4 +- .../PropertyBlobRoundTripTests.cs | 4 +- .../PublicTableWriteTests.cs | 42 + test/LibRed.Core.Tests/ReleaseAtCloseTests.cs | 113 +- .../RelocationSlotWidthTests.cs | 41 +- .../RowCodecGappedIdTests.cs | 15 +- test/LibRed.Core.Tests/RowDecoderTests.cs | 4 +- test/LibRed.Core.Tests/RowEncoderTests.cs | 10 +- .../RowRelocationCorruptionTests.cs | 49 +- .../SortOrderProvenanceProbeTest.cs | 4 +- test/LibRed.Core.Tests/TableCreatorTests.cs | 37 +- .../TableDefinitionPageTests.cs | 21 +- test/LibRed.Core.Tests/TdefBuilderTests.cs | 40 +- .../TdefVariableRegionTests.cs | 104 +- .../TransactionFailureRecoveryTests.cs | 14 +- test/LibRed.Core.Tests/UsageMapTests.cs | 39 +- .../VersionByteDetectTests.cs | 6 +- test/LibRed.EFCore.Tests/RoundTripTests.cs | 4 +- .../AceCoreApiShapeProbeTests.cs | 8 +- .../AceValidityLadder.cs | 6 +- .../AceWriteValiditySweepProbeTests.cs | 8 +- .../BigIntCreatedDatabaseAccessTests.cs | 14 +- .../CalculatedColumnSqlAccessTests.cs | 14 +- .../CatalogRowParityAccessTests.cs | 6 +- .../ColumnDescriptorByteParityAccessTests.cs | 27 +- .../ColumnLengthAccessTests.cs | 6 +- .../ColumnNumberingAccessTests.cs | 7 +- .../ColumnRetypeParityAccessTests.cs | 30 +- .../ComplexAttachmentWriteAccessTests.cs | 4 +- .../ComplexDeleteByteParityProbeTest.cs | 115 +- .../ComplexWriteAceReadbackTests.cs | 10 +- .../CreateTableUsageMapOrderAccessTests.cs | 29 +- .../DateTime2CreatedDatabaseAccessTests.cs | 14 +- .../DateTime2LocaleAccessTests.cs | 50 +- .../DeleteByteParityTests.cs | 34 +- .../DeletedRowSpaceAccessTests.cs | 16 +- .../GuidColumnStorageAccessTests.cs | 4 +- .../IndexRootSplitByteParityTests.cs | 62 +- .../IndexSplitByteParityTests.cs | 35 +- .../IndexSplitPackingAccessTests.cs | 13 +- .../IndexSplitPointProbeTest.cs | 64 +- .../IndexUniqueEntryCountAccessTests.cs | 15 +- .../LikeCandidates.cs | 6 +- .../LikeCollationProbeTests.cs | 6 +- .../LongValueColumnLayoutAccessTests.cs | 8 +- .../LongValueFreeMapAccessTests.cs | 7 +- .../LongValueOrderProbeTest.cs | 52 +- .../LvPropByteParityAccessTests.cs | 4 +- .../LvPropStorageAccessTests.cs | 6 +- .../MemoCompressionAccessTests.cs | 49 +- .../PageGroupProbeTest.cs | 139 +- .../PermissionRowsAccessTests.cs | 4 +- .../QueryNamesAccessTests.cs | 8 +- .../ReferencesAndIdentityAccessTests.cs | 4 +- .../RelationshipByteParityAccessTests.cs | 13 +- .../RelocatedRowDeleteAccessTests.cs | 16 +- .../RelocatedRowGrowthAccessTests.cs | 16 +- .../RowByteParityAccessTests.cs | 16 +- .../SelectIntoAccessTests.cs | 4 +- .../SelectIntoShapeProbeTest.cs | 4 +- .../StoredActionQueryWriteAccessTests.cs | 16 +- .../StoredParameterNameAccessTests.cs | 4 +- .../TdefByteParityAccessTests.cs | 113 +- .../TransactionalDdlRollbackAccessTests.cs | 10 +- .../WholeFileParityProbeTest.cs | 72 +- .../CollationComparisonTests.cs | 6 +- test/LibRed.Engine.Tests/CreateIndexTests.cs | 4 +- .../CreateProcedureTests.cs | 6 +- test/LibRed.Engine.Tests/CreateViewTests.cs | 6 +- test/LibRed.Engine.Tests/DdlDmlTests.cs | 38 +- .../DeleteCompactionTests.cs | 8 +- .../DropViewProcedureTests.cs | 11 +- test/LibRed.Engine.Tests/IndexSplitTests.cs | 7 +- test/LibRed.Engine.Tests/MemoIndexTests.cs | 4 +- .../PreEpochDateOrderingTests.cs | 4 +- test/LibRed.Engine.Tests/QueryNamesTests.cs | 4 +- .../ReferencesAndIdentityTests.cs | 4 +- .../SchemaVisibilityTests.cs | 4 +- test/LibRed.Engine.Tests/SelectIntoTests.cs | 8 +- test/LibRed.Shared/AceTestDatabase.cs | 4 +- test/LibRed.Shared/TestDatabases.cs | 91 +- 349 files changed, 12134 insertions(+), 11834 deletions(-) create mode 100644 src/LibRed/LibRed.Core/Catalog/ColumnSpec.cs create mode 100644 src/LibRed/LibRed.Core/Catalog/ComplexAttachment.cs create mode 100644 src/LibRed/LibRed.Core/Catalog/IndexSpec.cs create mode 100644 src/LibRed/LibRed.Core/Catalog/LongValueColumnSpec.cs delete mode 100644 src/LibRed/LibRed.Core/Catalog/RecordLayout.cs rename src/LibRed/LibRed.Core/{Storage/TableCreator.cs => Catalog/SchemaEditor.cs} (63%) create mode 100644 src/LibRed/LibRed.Core/Catalog/StoredQuery.cs delete mode 100644 src/LibRed/LibRed.Core/Catalog/TableDef.cs create mode 100644 src/LibRed/LibRed.Core/Catalog/TableDefinition.cs delete mode 100644 src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs delete mode 100644 src/LibRed/LibRed.Core/Formats/CatalogFormat.cs create mode 100644 src/LibRed/LibRed.Core/Formats/ColumnFlags.cs create mode 100644 src/LibRed/LibRed.Core/Formats/EncryptionInfoFlags.cs create mode 100644 src/LibRed/LibRed.Core/Formats/ForeignKeyType.cs rename src/LibRed/LibRed.Core/Formats/{IndexFlags.cs => IndexAttributes.cs} (55%) delete mode 100644 src/LibRed/LibRed.Core/Formats/IndexBlockFormat.cs create mode 100644 src/LibRed/LibRed.Core/Formats/IndexColumnOrder.cs create mode 100644 src/LibRed/LibRed.Core/Formats/IndexInfoType.cs delete mode 100644 src/LibRed/LibRed.Core/Formats/IndexKeyFlags.cs delete mode 100644 src/LibRed/LibRed.Core/Formats/LongValueFormat.cs create mode 100644 src/LibRed/LibRed.Core/Formats/ObjectAttributes.cs create mode 100644 src/LibRed/LibRed.Core/Formats/ObjectType.cs create mode 100644 src/LibRed/LibRed.Core/Formats/QueryAttribute.cs create mode 100644 src/LibRed/LibRed.Core/Formats/QueryDefType.cs create mode 100644 src/LibRed/LibRed.Core/Formats/QueryOperation.cs create mode 100644 src/LibRed/LibRed.Core/Formats/QueryOptions.cs create mode 100644 src/LibRed/LibRed.Core/Formats/RelationshipAction.cs delete mode 100644 src/LibRed/LibRed.Core/Formats/RowPointer.cs create mode 100644 src/LibRed/LibRed.Core/Formats/RowSlotFlags.cs delete mode 100644 src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs create mode 100644 src/LibRed/LibRed.Core/Formats/UsageMapType.cs delete mode 100644 src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs delete mode 100644 src/LibRed/LibRed.Core/Pages/TdefChainReader.cs delete mode 100644 src/LibRed/LibRed.Core/Pages/TdefRegions.cs create mode 100644 src/LibRed/LibRed.Core/Storage/BitmapBits.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/CatalogWriter.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs create mode 100644 src/LibRed/LibRed.Core/Storage/IndexKeyCodec.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/IndexKeyDecoder.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/IndexPageReader.cs rename src/LibRed/LibRed.Core/Storage/{IndexWriter.cs => IndexTree.cs} (77%) delete mode 100644 src/LibRed/LibRed.Core/Storage/JetIndexKeyChecksum.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/LongValueReader.cs create mode 100644 src/LibRed/LibRed.Core/Storage/LongValueStore.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/LongValueWriter.cs create mode 100644 src/LibRed/LibRed.Core/Storage/RowCodec.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/RowDecoder.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/RowEncoder.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/RowLayout.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/RowRelocationReader.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/UsageMapBits.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/UsageMapWriter.cs delete mode 100644 src/LibRed/LibRed.Core/Storage/ViewCreator.cs delete mode 100644 test/LibRed.Core.AccessTests/LocaleCollationAccessTests.cs create mode 100644 test/LibRed.Core.Tests/ColumnDeclarationTests.cs rename test/LibRed.Core.Tests/{IndexKeyEncoderTests.cs => IndexKeyCodecTests.cs} (79%) create mode 100644 test/LibRed.Core.Tests/PublicTableWriteTests.cs diff --git a/.claude/settings.json b/.claude/settings.json index d3afe08f..4541a720 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -45,6 +45,16 @@ } ] } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "in=$(cat); printf '%s' \"$in\" | grep -qE '\"stop_hook_active\"[[:space:]]*:[[:space:]]*true' && exit 0; cd \"$CLAUDE_PROJECT_DIR\" || exit 0; [ -z \"$(git status --porcelain -- src ':(glob,exclude)src/**/docs/**')\" ] && exit 0; echo '{\"decision\":\"block\",\"reason\":\"src/ has changes. Before finishing, go through your diff: for every method, overload, wrapper, helper or constant you added, name the existing code that already does that job. If one exists, change or split that code instead and remove yours. See Change the existing path in CLAUDE.md.\"}'" + } + ] + } ] } } diff --git a/AGENTS.md b/AGENTS.md index 134ac1f6..388a4819 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -271,8 +271,8 @@ in `EFCore.Jet.Data`. ``` src/LibRed/ LibRed.Core/ File format: IO (PageChannel/PageBuffer), Formats (version offsets), - Pages, Catalog (MSysObjects → TableDef/ColumnDef/IndexDef), Storage - (Table/TableCursor/RowDecoder/UsageMap), Crypto; JetDatabase entry point + Pages, Catalog (MSysObjects → TableDefinition/ColumnDef/IndexDef), Storage + (Table/TableCursor/RowCodec/UsageMap), Crypto; JetDatabase entry point LibRed.Sql/ SQL front end: ANTLR grammar (AccessSql.g4), AST, parser, binder. NO Jet dependency — binds via the ISchemaProvider abstraction LibRed.Engine/ Logical Plan nodes, QueryPlanner, CatalogSchemaProvider (bridges the @@ -298,7 +298,7 @@ the names are `JetVersion` / `RequiredVersion` / `EnsureFormatAtLeast`: - `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. That guard is on the SQL path; the same rule is enforced again in Core over `JetDataType` (`JetDataTypeVersions.EnsureStorable`, called - from `TdefBuilder` and `TableCreator`), because every `JetDatabase` method that defines a column takes a raw `ColumnSpec` and + from `TableDefinition` and `SchemaEditor`), because every `JetDatabase` method that defines a column takes a raw `ColumnSpec` and would otherwise write the descriptor with nothing objecting. - `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 @@ -365,6 +365,16 @@ here, because it sends the work off in a direction that has to be unwound later. verify against the code rather than quoting the list; and a **memory or summary records what was true when it was written**, not what is true now. +**Change the existing path; don't add one beside it.** A fix reshapes the code that already does the job: split +a method at the point where its callers differ, change what it takes, move a check to where every caller passes +through. It does not add an overload, wrapper, helper, constant or parallel method next to it. Before writing +any new member, find the code that already does that job — by what it does, not by its name: a decode, a lookup, +a layout, a check, a value — and use or reshape it. A second copy is how this codebase got its bugs: two readers +of one field that disagreed, a merged path that lost a check only one copy had, hard-coded page numbers restating +a pointer, test helpers re-implementing Core and drifting from it. A genuinely new member is fine when nothing +existing does the job; say what you checked. (Claude Code backs this with a `Stop` hook in +`.claude/settings.json` that asks for the review while `src/` has uncommitted changes.) + **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 diff --git a/CLAUDE.md b/CLAUDE.md index 9291b971..23e37356 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -275,8 +275,8 @@ in `EFCore.Jet.Data`. ``` src/LibRed/ LibRed.Core/ File format: IO (PageChannel/PageBuffer), Formats (version offsets), - Pages, Catalog (MSysObjects → TableDef/ColumnDef/IndexDef), Storage - (Table/TableCursor/RowDecoder/UsageMap), Crypto; JetDatabase entry point + Pages, Catalog (MSysObjects → TableDefinition/ColumnDef/IndexDef), Storage + (Table/TableCursor/RowCodec/UsageMap), Crypto; JetDatabase entry point LibRed.Sql/ SQL front end: ANTLR grammar (AccessSql.g4), AST, parser, binder. NO Jet dependency — binds via the ISchemaProvider abstraction LibRed.Engine/ Logical Plan nodes, QueryPlanner, CatalogSchemaProvider (bridges the @@ -302,7 +302,7 @@ the names are `JetVersion` / `RequiredVersion` / `EnsureFormatAtLeast`: - `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. That guard is on the SQL path; the same rule is enforced again in Core over `JetDataType` (`JetDataTypeVersions.EnsureStorable`, called - from `TdefBuilder` and `TableCreator`), because every `JetDatabase` method that defines a column takes a raw `ColumnSpec` and + from `TableDefinition` and `SchemaEditor`), because every `JetDatabase` method that defines a column takes a raw `ColumnSpec` and would otherwise write the descriptor with nothing objecting. - `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 @@ -369,6 +369,15 @@ here, because it sends the work off in a direction that has to be unwound later. verify against the code rather than quoting the list; and a **memory or summary records what was true when it was written**, not what is true now. +**Change the existing path; don't add one beside it.** A fix reshapes the code that already does the job: split +a method at the point where its callers differ, change what it takes, move a check to where every caller passes +through. It does not add an overload, wrapper, helper, constant or parallel method next to it. Before writing +any new member, find the code that already does that job — by what it does, not by its name: a decode, a lookup, +a layout, a check, a value — and use or reshape it. A second copy is how this codebase got its bugs: two readers +of one field that disagreed, a merged path that lost a check only one copy had, hard-coded page numbers restating +a pointer, test helpers re-implementing Core and drifting from it. A genuinely new member is fine when nothing +existing does the job; say what you checked. + `.claude/settings.json` installs `PreToolUse` hooks that **deny** three things in `Bash`/`PowerShell`: - **Reading files through the shell** (`cat`, `head`, `grep`, `ls`, `find`, `Get-Content`, `Select-String`, …) — @@ -385,7 +394,9 @@ was written**, not what is true now. `dotnet build`/`test`/`restore` and, in Bash, `git status`/`diff`/`log`/`show`/`branch`/`add`/`commit`/`push` are pre-allowed, and PowerShell is allowed wholesale — so don't work around the hooks; the denial message is telling you which tool to use, not that the action is forbidden. A `PostToolUse` hook also rewrites every file -the `Write` tool writes to CRLF line endings. +the `Write` tool writes to CRLF line endings. A `Stop` hook blocks the end of a turn once while `src/` (its +`docs` folders aside) has uncommitted changes, asking for the review in "Change the existing path" above; the +next stop goes through. ## CI diff --git a/src/LibRed/LibRed.Ado/LibRedConnection.cs b/src/LibRed/LibRed.Ado/LibRedConnection.cs index c5e94947..3ad3d2dd 100644 --- a/src/LibRed/LibRed.Ado/LibRedConnection.cs +++ b/src/LibRed/LibRed.Ado/LibRedConnection.cs @@ -120,7 +120,7 @@ public static string GetConnectionString(string fileNameOrConnectionString) /// — **natively, no DAO/ADOX, cross-platform**. /// /// - /// synthesises the file from scratch (page 0, + /// synthesises the file from scratch (page 0, /// the free map, and the bootstrap system catalog), then LibRed's ordinary writers populate it. /// Produces an .accdb that LibRed reads and writes fully; the remaining Access-compatibility /// system tables are still being filled in. @@ -144,7 +144,7 @@ public static void CreateDatabase( if (string.IsNullOrEmpty(path)) throw new ArgumentException("The connection string is missing a Data Source.", nameof(connectionString)); - Storage.DatabaseCreator.CreateEmpty(path, (byte)version, collation); + JetDatabase.Create(path, (byte)version, collation); CreateDualTable(path); } diff --git a/src/LibRed/LibRed.Ado/LibRedDataReader.cs b/src/LibRed/LibRed.Ado/LibRedDataReader.cs index 706f9059..c78341c8 100644 --- a/src/LibRed/LibRed.Ado/LibRedDataReader.cs +++ b/src/LibRed/LibRed.Ado/LibRedDataReader.cs @@ -1,4 +1,5 @@ using LibRed.Engine.Execution; +using LibRed.Storage.Types; using System.Collections; using System.Collections.ObjectModel; using System.Data; @@ -93,9 +94,6 @@ public override int GetOrdinal(string name) public override object GetValue(int ordinal) => _current[ordinal] ?? DBNull.Value; - /// The OLE epoch (1899-12-30) — Jet stores TimeSpan/TimeOnly as an offset from it. - private static readonly DateTime OleEpoch = new(1899, 12, 30); - /// /// Typed accessor EF Core uses. Jet has no TimeSpan/DateOnly/TimeOnly/DateTimeOffset type — they are /// all stored in a DateTime column — so convert a stored back when one of those @@ -106,7 +104,7 @@ public override T GetFieldValue(int ordinal) { if (_current[ordinal] is DateTime dt) { - if (typeof(T) == typeof(TimeSpan)) return (T)(object)(dt - OleEpoch); + if (typeof(T) == typeof(TimeSpan)) return (T)(object)(dt - JetTypeCodec.OleEpoch); if (typeof(T) == typeof(DateOnly)) return (T)(object)DateOnly.FromDateTime(dt); if (typeof(T) == typeof(TimeOnly)) return (T)(object)TimeOnly.FromDateTime(dt); if (typeof(T) == typeof(DateTimeOffset)) return (T)(object)new DateTimeOffset(dt, TimeSpan.Zero); diff --git a/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs b/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs index 5b85a024..a14713b1 100644 --- a/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs +++ b/src/LibRed/LibRed.Core/Catalog/ColumnDef.cs @@ -2,7 +2,7 @@ namespace LibRed.Catalog; /// /// Describes a single column of a table: its name, type, physical layout and flags. -/// Decoded from the column descriptors in a . +/// Decoded from the column descriptors in a . /// public sealed class ColumnDef { @@ -121,7 +121,7 @@ public sealed class ColumnDef /// the reserved words at 0x03 and 0x11 and the undocumented flag bits included, which no /// modelled property carries. Nothing writes from it: every DDL edits the descriptor in place, so those /// bytes are never re-emitted. Null for a freshly-built (never-read) column. - public byte[]? RawDescriptor { get; init; } + internal byte[]? RawDescriptor { get; init; } /// Catalog flag bits (byte 0x0F) to force-set on a created column — the system-catalog column marker /// (0x10) and security-identifier marker (0x20) Access sets on the columns of its own catalog tables. 0 for diff --git a/src/LibRed/LibRed.Core/Catalog/ColumnSpec.cs b/src/LibRed/LibRed.Core/Catalog/ColumnSpec.cs new file mode 100644 index 00000000..a4c2e6e5 --- /dev/null +++ b/src/LibRed/LibRed.Core/Catalog/ColumnSpec.cs @@ -0,0 +1,77 @@ +namespace LibRed.Catalog; + +/// A column to create: its name, type, and (for fixed/text) byte length. +// AutoNumber (COUNTER) seed and increment — the first generated id is Seed, then +Increment each row. +// Default 1/1 (a plain COUNTER). Stored in the TDEF header: last-value 0x14 = Seed-Increment, 0x18 = Increment. +// An explicit column id, else the column's position is used. Access's own catalog tables declare their +// columns in an order that is not their id order (JetDatabase); null for an ordinary CREATE/ADD column. +// The catalog flag bits (0x0F) Access sets on the columns of its own catalog — MSysObjects, MSysACEs, +// MSysQueries, MSysRelationships and MSysComplexColumns: 0x10 on every one, and 0x20 as well on the two that +// hold a security identifier (MSysObjects.Owner, MSysACEs.SID). Every other column leaves them clear, the +// other MSys* tables' included (verified; see page-02b-columns.md). +// WITH COMPRESSION on a Text/Memo column: the 0x10 extended flag bit 0x01. Off unless asked for, which +// is what ACE does for a column declared without it (LongTextStorageAccessTests). +// A calculated column (§3.4a): the expression, and the type the caller DECLARED for it. Both null for an +// ordinary column. They are separate from Type because Type carries the descriptor's *promoted* storage +// type, which need not be the result type at all — build one with Calculated() rather than by hand. +// A column of a table the engine creates for itself, which leaves 0x09 — the second copy of the column id — +// zero. A catalog column (SystemFlags) is one already; this marks the rest, such as the MSysComplexType_* +// templates, which carry no catalog flag. +// Extended flag bits (0x10) LibRed does not model, to set on a created column: 0x10 on an attachment value +// column (FileData, FileFlags, FileName, FileTimeStamp, FileType, FileURL). +public sealed record ColumnSpec( + string Name, + JetDataType Type, + int Length, + bool IsFixedLength, + bool IsAutoNumber = false, + byte Precision = 0, + byte Scale = 0, + bool IsNullable = true, + int Seed = 1, + int Increment = 1, + int? ColumnId = null, + byte SystemFlags = 0, + bool SupportsCompressedUnicode = false, + string? CalculatedExpression = null, + JetDataType? CalculatedResultType = null, + bool IsEngineColumn = false, + byte ExtendedFlags = 0) +{ + /// A calculated column of computing . + /// ACE stores one **always variable-length**, with the descriptor carrying the *promoted* type and a + /// constant declared length, and the real result type living in the table's property blob — get any of + /// those wrong and Access reads the payload at the wrong width, so this is the only supported way to + /// build one. + // `Backtick` quoting is SQL's, not the expression service's, and ACE cannot read it — normalise + // here so every route in (SQL, or a direct caller) stores something ACE can evaluate. + public static ColumnSpec Calculated(string name, JetDataType resultType, string expression) => + new(name, PromotedType(resultType), CalculatedLength(resultType), IsFixedLength: false, + CalculatedExpression: Storage.Calculated.CalculatedExpression.NormaliseIdentifierQuoting(expression), + CalculatedResultType: resultType); + + /// The descriptor's storage type: ACE widens the result type to the next in its family, which is + /// why the payload's own length is what says how to decode it (§3.4a). + internal static JetDataType PromotedType(JetDataType resultType) => resultType switch + { + JetDataType.Boolean => JetDataType.Int16, + JetDataType.Byte or JetDataType.Int16 => JetDataType.Int32, + JetDataType.Single => JetDataType.Double, + // Memo, Guid and Binary all land on Text — only their declared LENGTH tells them apart. + JetDataType.Memo or JetDataType.Guid or JetDataType.Binary => JetDataType.Text, + _ => resultType, + }; + + /// ACE's constant declared length for a calculated column, measured across every type DAO will + /// create: 0 for a Memo result, whose value lives on long-value pages; 509 for Text + /// whatever size was asked for; 510 for Binary; and 39 for everything else, GUID included. + /// None is a payload ceiling — the requested size is discarded and ACE does not truncate — and nothing + /// moves them: not the requested size, not the length of the expression. + internal static int CalculatedLength(JetDataType resultType) => resultType switch + { + JetDataType.Memo => 0, + JetDataType.Text => 509, + JetDataType.Binary => 510, + _ => 39, + }; +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/ComplexAttachment.cs b/src/LibRed/LibRed.Core/Catalog/ComplexAttachment.cs new file mode 100644 index 00000000..ed9b99b3 --- /dev/null +++ b/src/LibRed/LibRed.Core/Catalog/ComplexAttachment.cs @@ -0,0 +1,189 @@ +using System.Buffers.Binary; +using System.Text; + +namespace LibRed.Catalog; + +/// An attachment's stored file: the extension Access recorded, and the bytes themselves. +/// The extension from the payload's inner header (pdf, png, …), without +/// a dot. This is the payload's own copy, which need not equal the FileType column. +/// The file's bytes, decompressed where Access compressed them. +public readonly record struct ComplexAttachment(string Extension, byte[] Content) +{ + /// + /// Unwraps the FileData column of an attachment row. The stored blob is an 8-byte header — a + /// compression flag (1 = a zlib stream follows, 0 = raw bytes) and the body's decompressed + /// length — then the body, which itself opens with a 20-byte header carrying the extension as + /// null-terminated UTF-16. Verified against a pdf, an mp3 and a png; see + /// docs/format/system-catalog.md. + /// + /// The blob is too short, or its headers do not describe it. + public static ComplexAttachment Unwrap(ReadOnlySpan fileData) + { + if (fileData.Length < OuterHeaderSize) + throw new InvalidDataException( + $"Attachment data is {fileData.Length} bytes; the header alone is {OuterHeaderSize}."); + + var compression = (CompressionKind)BinaryPrimitives.ReadUInt32LittleEndian( + fileData[CompressionOffset..]); + uint bodyLength = BinaryPrimitives.ReadUInt32LittleEndian(fileData[BodyLengthOffset..]); + + byte[] body = compression switch + { + CompressionKind.None => fileData[OuterHeaderSize..].ToArray(), + CompressionKind.ZLib => Inflate(fileData[OuterHeaderSize..]), + _ => throw new InvalidDataException($"Attachment data has compression flag {(uint)compression}; expected 0 or 1."), + }; + if (body.Length != bodyLength) + throw new InvalidDataException($"Attachment body is {body.Length} bytes; its header declares {bodyLength}."); + if (body.Length < MinInnerHeaderSize) + throw new InvalidDataException( + $"Attachment body is {body.Length} bytes; its inner header alone is {MinInnerHeaderSize}."); + + int innerLength = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(InnerLengthOffset)); + if (innerLength < ExtensionOffset || innerLength > body.Length) + throw new InvalidDataException($"Attachment inner header declares {innerLength} bytes, which its {body.Length}-byte body cannot hold."); + + string extension = Encoding.Unicode + .GetString(body.AsSpan(ExtensionOffset, innerLength - ExtensionOffset)).TrimEnd('\0'); + return new ComplexAttachment(extension, body[innerLength..]); + } + + /// + /// The extensions Access leaves uncompressed because the format already is. From the documented list + /// (Attachment object, "Types of files that Access compresses"), which is explicitly partial — anything + /// absent is compressed, so this set is the exception rather than the rule. + /// + // The comparer is explicit because an extension matches case-insensitively (.PNG is still a png), which + // IDE0028's collection expression would leave to the default. +#pragma warning disable IDE0028 + private static readonly HashSet NativelyCompressed = new(StringComparer.OrdinalIgnoreCase) + { + "jpg", "jpeg", "gif", "png", "zip", "cab", "docx", "xlsx", "xlsb", "pptx", + }; +#pragma warning restore IDE0028 + + /// + /// The largest attachment Access accepts — "Individual files cannot exceed 256 megabytes". This is + /// Access's policy, not the format's capability: a long value's stored length runs to 0x3FFFFFFF, + /// which ACE itself accepts and reads back, so the file format holds four times this. + /// + public const int MaxAccessAttachmentBytes = 256 * 1024 * 1024; + + /// + /// Whether Access would deflate a file of this extension rather than store it as it is — its + /// convention, not a rule of the format. Both forms are valid on disk and ACE reads either, so this only + /// decides whether a file LibRed writes looks like one Access wrote. + /// + public static bool Compresses(string extension) => + !NativelyCompressed.Contains((extension ?? "").TrimStart('.')); + + /// + /// Throws when or breaks one of Access's + /// documented attachment limits — the 256 MB per file, the 255-character name, and the characters a name + /// may not contain. None of these is a format limit; they are what the Access UI enforces, and a file + /// breaking them is one Access would not have created and may not open. + /// + public static void ValidateAccessLimits(string fileName, ReadOnlySpan content) + { + ArgumentException.ThrowIfNullOrEmpty(fileName); + if (content.Length > MaxAccessAttachmentBytes) + throw new ArgumentOutOfRangeException(nameof(content), + $"An attachment is {content.Length} bytes; an attachment holds at most {MaxAccessAttachmentBytes} bytes."); + if (fileName.Length > 255) + throw new ArgumentException( + $"An attachment name is at most 255 characters including the extension; '{fileName}' is {fileName.Length}.", + nameof(fileName)); + if (fileName.IndexOfAny(['?', '"', '/', '\\', '<', '>', '*', '|', ':']) >= 0) + throw new ArgumentException( + $"An attachment name cannot contain any of ? \" / \\ < > * | : — '{fileName}' does.", + nameof(fileName)); + } + + /// + /// Builds the FileData blob for an attachment — the inverse of . + /// + /// + /// Both storage forms are valid and ACE reads either, so compression is a choice, not a + /// correctness question. Left to it follows Access's own convention — + /// "Access will compress your attached files unless those files are compressed natively" — which matches + /// what ACE wrote in the sample file: a png raw, a pdf and an mp3 deflated. + /// + /// The file's extension without a dot, as the inner header records it. + /// The file's bytes. + /// Force the stored form, or to follow Access's convention + /// for the extension. + public static byte[] Pack(string extension, ReadOnlySpan content, bool? compress = null) + { + ArgumentNullException.ThrowIfNull(extension); + bool deflate = compress ?? Compresses(extension); + + // The extension is null-terminated UTF-16, and the unit count is its length INCLUDING that terminator — + // "pdf" gives 4 and a 20-byte inner header, "thmx" 5 and 22. + int units = extension.Length + 1; + int innerLength = ExtensionOffset + units * sizeof(char); + + // The body is the inner header followed by the file; the outer header's length is the body's + // UNCOMPRESSED size, whether or not the stream that follows is deflated. + byte[] body = new byte[innerLength + content.Length]; + BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(InnerLengthOffset), innerLength); + BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(InnerConstantOffset), InnerHeaderConstant); + BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(ExtensionUnitsOffset), units); + Encoding.Unicode.GetBytes(extension, body.AsSpan(ExtensionOffset)); // terminator stays 0 + content.CopyTo(body.AsSpan(innerLength)); + + byte[] stored = deflate ? Deflate(body) : body; + byte[] blob = new byte[OuterHeaderSize + stored.Length]; + BinaryPrimitives.WriteUInt32LittleEndian(blob.AsSpan(CompressionOffset), + (uint)(deflate ? CompressionKind.ZLib : CompressionKind.None)); + BinaryPrimitives.WriteUInt32LittleEndian(blob.AsSpan(BodyLengthOffset), (uint)body.Length); + stored.CopyTo(blob.AsSpan(OuterHeaderSize)); + return blob; + } + + private static byte[] Deflate(byte[] body) + { + using var output = new MemoryStream(); + using (var zlib = new System.IO.Compression.ZLibStream( + output, System.IO.Compression.CompressionLevel.Optimal, leaveOpen: true)) + zlib.Write(body); + return output.ToArray(); + } + + private static byte[] Inflate(ReadOnlySpan stream) + { + using var input = new MemoryStream(stream.ToArray()); + using var zlib = new System.IO.Compression.ZLibStream(input, System.IO.Compression.CompressionMode.Decompress); + using var output = new MemoryStream(); + zlib.CopyTo(output); + return output.ToArray(); + } + + // --- Outer header --- + + internal const int CompressionOffset = 0; + internal const int BodyLengthOffset = 4; + internal const int OuterHeaderSize = 8; + + // --- Inner header, from the body's start --- + + internal const int InnerLengthOffset = 0; + + /// A word every attachment measured carries as ; its meaning is + /// not established. + internal const int InnerConstantOffset = 4; + internal const int InnerHeaderConstant = 1; + + /// The extension's length in UTF-16 units, its terminator included — "pdf" gives 4. + internal const int ExtensionUnitsOffset = 8; + internal const int ExtensionOffset = 12; + + /// The inner header of a three-letter extension, the shortest measured. + internal const int MinInnerHeaderSize = 20; + + private enum CompressionKind : uint + { + None = 0, + ZLib = 1, + } + +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/ComplexColumn.cs b/src/LibRed/LibRed.Core/Catalog/ComplexColumn.cs index 5a6e8dfb..c8921378 100644 --- a/src/LibRed/LibRed.Core/Catalog/ComplexColumn.cs +++ b/src/LibRed/LibRed.Core/Catalog/ComplexColumn.cs @@ -1,6 +1,3 @@ -using System.Buffers.Binary; -using System.Text; - namespace LibRed.Catalog; /// @@ -21,7 +18,7 @@ namespace LibRed.Catalog; public sealed class ComplexColumn { internal ComplexColumn( - string columnName, int complexId, TableDef ownerTable, TableDef flatTable, + string columnName, int complexId, TableDefinition ownerTable, TableDefinition flatTable, ColumnDef ownerLink, ColumnDef valueId, string? elementTypeName) { ColumnName = columnName; @@ -31,7 +28,7 @@ internal ComplexColumn( OwnerLink = ownerLink; ValueId = valueId; ElementTypeName = elementTypeName; - ValueColumns = [.. flatTable.Columns.Where(c => c != ownerLink && c != valueId)]; + ValueColumns = flatTable.Columns.Where(c => c != ownerLink && c != valueId).ToList().AsReadOnly(); } /// The complex column's name on . @@ -42,10 +39,10 @@ internal ComplexColumn( public int ComplexId { get; } /// The table holding the complex column. - public TableDef OwnerTable { get; } + public TableDefinition OwnerTable { get; } /// The per-column table holding the values, one row each. - public TableDef FlatTable { get; } + public TableDefinition FlatTable { get; } /// The flat table's link back to the owning record: it holds that record's complex id and /// repeats once per value, which is what makes the column multi-valued. Indexed, not unique. @@ -67,158 +64,4 @@ internal ComplexColumn( /// FileData/FileName shape, so applies. public bool IsAttachment => FlatTable.FindColumn("FileData") is not null && FlatTable.FindColumn("FileName") is not null; -} - -/// An attachment's stored file: the extension Access recorded, and the bytes themselves. -/// The extension from the payload's inner header (pdf, png, …), without -/// a dot. This is the payload's own copy, which need not equal the FileType column. -/// The file's bytes, decompressed where Access compressed them. -public readonly record struct ComplexAttachment(string Extension, byte[] Content) -{ - /// - /// Unwraps the FileData column of an attachment row. The stored blob is an 8-byte header — a - /// compression flag (1 = a zlib stream follows, 0 = raw bytes) and the body's decompressed - /// length — then the body, which itself opens with a 20-byte header carrying the extension as - /// null-terminated UTF-16. Verified against a pdf, an mp3 and a png; see - /// docs/format/system-catalog.md. - /// - /// The blob is too short, or its headers do not describe it. - public static ComplexAttachment Unwrap(ReadOnlySpan fileData) - { - const int OuterHeader = 8, InnerHeaderLength = 20, ExtensionOffset = 12; - if (fileData.Length < OuterHeader) - throw new InvalidDataException($"Attachment data is {fileData.Length} bytes; the header alone is {OuterHeader}."); - - uint compression = BinaryPrimitives.ReadUInt32LittleEndian(fileData); - uint bodyLength = BinaryPrimitives.ReadUInt32LittleEndian(fileData[4..]); - - byte[] body = compression switch - { - 0 => fileData[OuterHeader..].ToArray(), - 1 => Inflate(fileData[OuterHeader..]), - _ => throw new InvalidDataException($"Attachment data has compression flag {compression}; expected 0 or 1."), - }; - if (body.Length != bodyLength) - throw new InvalidDataException($"Attachment body is {body.Length} bytes; its header declares {bodyLength}."); - if (body.Length < InnerHeaderLength) - throw new InvalidDataException($"Attachment body is {body.Length} bytes; its inner header alone is {InnerHeaderLength}."); - - int innerLength = BinaryPrimitives.ReadInt32LittleEndian(body); - if (innerLength < ExtensionOffset || innerLength > body.Length) - throw new InvalidDataException($"Attachment inner header declares {innerLength} bytes, which its {body.Length}-byte body cannot hold."); - - string extension = Encoding.Unicode - .GetString(body.AsSpan(ExtensionOffset, innerLength - ExtensionOffset)).TrimEnd('\0'); - return new ComplexAttachment(extension, body[innerLength..]); - } - - /// - /// The extensions Access leaves uncompressed because the format already is. From the documented list - /// (Attachment object, "Types of files that Access compresses"), which is explicitly partial — anything - /// absent is compressed, so this set is the exception rather than the rule. - /// - // The comparer is explicit because an extension matches case-insensitively (.PNG is still a png), which - // IDE0028's collection expression would leave to the default. -#pragma warning disable IDE0028 - private static readonly HashSet NativelyCompressed = new(StringComparer.OrdinalIgnoreCase) - { - "jpg", "jpeg", "gif", "png", "zip", "cab", "docx", "xlsx", "xlsb", "pptx", - }; -#pragma warning restore IDE0028 - - /// - /// The largest attachment Access accepts — "Individual files cannot exceed 256 megabytes". This is - /// Access's policy, not the format's capability: a long value's stored length runs to 0x3FFFFFFF, - /// which ACE itself accepts and reads back, so the file format holds four times this. - /// - public const int MaxAccessAttachmentBytes = 256 * 1024 * 1024; - - /// - /// Whether Access would deflate a file of this extension rather than store it as it is — its - /// convention, not a rule of the format. Both forms are valid on disk and ACE reads either, so this only - /// decides whether a file LibRed writes looks like one Access wrote. - /// - public static bool Compresses(string extension) => - !NativelyCompressed.Contains((extension ?? "").TrimStart('.')); - - /// - /// Throws when or breaks one of Access's - /// documented attachment limits — the 256 MB per file, the 255-character name, and the characters a name - /// may not contain. None of these is a format limit; they are what the Access UI enforces, and a file - /// breaking them is one Access would not have created and may not open. - /// - public static void ValidateAccessLimits(string fileName, ReadOnlySpan content) - { - ArgumentException.ThrowIfNullOrEmpty(fileName); - if (content.Length > MaxAccessAttachmentBytes) - throw new ArgumentOutOfRangeException(nameof(content), - $"An attachment is {content.Length} bytes; an attachment holds at most {MaxAccessAttachmentBytes} bytes."); - if (fileName.Length > 255) - throw new ArgumentException( - $"An attachment name is at most 255 characters including the extension; '{fileName}' is {fileName.Length}.", - nameof(fileName)); - if (fileName.IndexOfAny(['?', '"', '/', '\\', '<', '>', '*', '|', ':']) >= 0) - throw new ArgumentException( - $"An attachment name cannot contain any of ? \" / \\ < > * | : — '{fileName}' does.", - nameof(fileName)); - } - - /// - /// Builds the FileData blob for an attachment — the inverse of . - /// - /// - /// Both storage forms are valid and ACE reads either, so compression is a choice, not a - /// correctness question. Left to it follows Access's own convention — - /// "Access will compress your attached files unless those files are compressed natively" — which matches - /// what ACE wrote in the sample file: a png raw, a pdf and an mp3 deflated. - /// - /// The file's extension without a dot, as the inner header records it. - /// The file's bytes. - /// Force the stored form, or to follow Access's convention - /// for the extension. - public static byte[] Pack(string extension, ReadOnlySpan content, bool? compress = null) - { - ArgumentNullException.ThrowIfNull(extension); - bool deflate = compress ?? Compresses(extension); - const int OuterHeader = 8, ExtensionOffset = 12; - - // The extension is null-terminated UTF-16, and the count at [8] is its length in UTF-16 units - // INCLUDING that terminator — "pdf" gives 4 and a 20-byte inner header, "thmx" 5 and 22. - int units = extension.Length + 1; - int innerLength = ExtensionOffset + units * 2; - - // The body is the inner header followed by the file; the outer header's length is the body's - // UNCOMPRESSED size, whether or not the stream that follows is deflated. - byte[] body = new byte[innerLength + content.Length]; - BinaryPrimitives.WriteInt32LittleEndian(body, innerLength); - BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(4), 1); - BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(8), units); - Encoding.Unicode.GetBytes(extension, body.AsSpan(ExtensionOffset)); // terminator stays 0 - content.CopyTo(body.AsSpan(innerLength)); - - byte[] stored = deflate ? Deflate(body) : body; - byte[] blob = new byte[OuterHeader + stored.Length]; - BinaryPrimitives.WriteUInt32LittleEndian(blob, deflate ? 1u : 0u); - BinaryPrimitives.WriteUInt32LittleEndian(blob.AsSpan(4), (uint)body.Length); - stored.CopyTo(blob.AsSpan(OuterHeader)); - return blob; - } - - private static byte[] Deflate(byte[] body) - { - using var output = new MemoryStream(); - using (var zlib = new System.IO.Compression.ZLibStream( - output, System.IO.Compression.CompressionLevel.Optimal, leaveOpen: true)) - zlib.Write(body); - return output.ToArray(); - } - - private static byte[] Inflate(ReadOnlySpan stream) - { - using var input = new MemoryStream(stream.ToArray()); - using var zlib = new System.IO.Compression.ZLibStream(input, System.IO.Compression.CompressionMode.Decompress); - using var output = new MemoryStream(); - zlib.CopyTo(output); - return output.ToArray(); - } -} +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/IndexDef.cs b/src/LibRed/LibRed.Core/Catalog/IndexDef.cs index 8e9c50db..9c936585 100644 --- a/src/LibRed/LibRed.Core/Catalog/IndexDef.cs +++ b/src/LibRed/LibRed.Core/Catalog/IndexDef.cs @@ -1,3 +1,5 @@ +using LibRed.Formats; + namespace LibRed.Catalog; /// Describes an index: its columns (with sort direction), uniqueness and root page. @@ -6,7 +8,13 @@ public sealed record IndexDef public required string Name { get; init; } /// Indexed columns in key order; true = ascending. - public IReadOnlyList<(ColumnDef Column, bool Ascending)> Columns { get; init; } = []; + public IReadOnlyList<(ColumnDef Column, bool Ascending)> Columns + { + get => _columns; + init => _columns = Array.AsReadOnly(value.ToArray()); + } + + private readonly IReadOnlyList<(ColumnDef Column, bool Ascending)> _columns = []; public bool IsUnique { get; init; } public bool IsPrimaryKey { get; init; } @@ -19,9 +27,9 @@ public sealed record IndexDef /// required (index flag 0x08). Surfaced by scaffolding as the index DISALLOW NULL filter. public bool Required { get; init; } - /// The whole flags word at the index-data block's 0x2E — the bits above included, and - /// 0x0200 on an index over a complex column. - public ushort Flags { get; init; } + /// The whole flags word of the index-data block — the bits above included, and + /// on an index over a complex column. + public IndexAttributes Flags { get; init; } /// /// The index's unique-entry count from the TDEF statistics block. This is a cumulative @@ -34,6 +42,10 @@ public sealed record IndexDef /// Page number of the index B-tree root. Neither a split nor a bulk build moves it; a rebuild onto a fresh root does. public int RootPage { get; internal set; } + /// The (row, page) pointer to the index's own pages usage map, which records every page of its B-tree; + /// a zero page when the index has none. + public (int Row, int Page) UsageMap { get; init; } + /// /// This index's position among the table's real indexes — the ordinal of its 12-byte statistics /// block (at TdefRealIndexBlockOffset + ordinal × RealIndexEntrySize) and its index-data diff --git a/src/LibRed/LibRed.Core/Catalog/IndexSpec.cs b/src/LibRed/LibRed.Core/Catalog/IndexSpec.cs new file mode 100644 index 00000000..123af739 --- /dev/null +++ b/src/LibRed/LibRed.Core/Catalog/IndexSpec.cs @@ -0,0 +1,11 @@ +namespace LibRed.Catalog; + +/// An index to create over the named columns, anchored at an already-allocated root page. +public sealed record IndexSpec( + string Name, + IReadOnlyList Columns, + bool IsPrimaryKey, + bool IsUnique, + int RootPage, + int UsageMapRow = 0, + int UsageMapPage = 0); \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs index 496eccb5..4b826352 100644 --- a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs +++ b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs @@ -10,98 +10,411 @@ namespace LibRed.Catalog; /// Reads the system catalog (MSysObjects) to enumerate the tables in a database. /// /// -/// Bootstrap: MSysObjects' own TDEF is at a fixed page (), -/// so we build a for it from that page and read its rows like any -/// other table. For a table object, the row's Id is its TDEF page number. +/// Bootstrap: MSysObjects' own TDEF is the page page 0's catalog root pointer names +/// (), so we build a for it +/// from that page and read its rows like any other table. For a table object, the row's Id is its TDEF +/// page number. /// -public sealed class JetCatalog(PageChannel channel, int catalogPage = 2) +public sealed class JetCatalog { - // MSysObjects TDEF page — the catalog root. Normally read from the page-0 bootstrap pointer (0x20) - // and passed in by JetDatabase; defaults to Access's canonical page 2 for direct construction. - private readonly int _catalogPage = catalogPage; - - /// MSysObjects.Type value for a table object. - private const short ObjectTypeTable = 1; - - /// MSysObjects.Flags bits marking a system object (`0x80000000` system, `0x00000002` - /// system attribute). - private const uint SystemObjectFlags = 0x80000002; - - /// MSysObjects.Flags bit marking a hidden object (`0x08`, observed on Access's - /// nav-pane tables and on EFCore.Jet's `#Dual` helper). Access excludes hidden objects from its - /// user-table list, so we treat them as non-user too. - private const uint HiddenObjectFlags = 0x00000008; - - private readonly PageChannel _channel = channel; - private List? _tables; - private List? _relationships; - private Dictionary? _views; - private Dictionary? _actionQueries; - private Dictionary>? _queryParameters; - private List? _complexColumns; - private (byte[] Admin, byte[] Users, byte[] Creator)? _securitySids; - private long _seenSchemaGeneration = channel.SchemaGeneration; + // The catalog column bits, and the attachment value bit, as ColumnSpec takes them (raw bytes). + private const byte Sys = (byte)ColumnFlags.SystemCatalog; + private const byte SysSid = (byte)(ColumnFlags.SystemCatalog | ColumnFlags.SecurityId); + private const byte AttachmentValue = (byte)ColumnExtendedFlags.AttachmentValue; + + // MSysObjects — the system catalog. Declared in the real physical (alphabetical) descriptor order with + // explicit canonical ColumnIds and system flags, exactly as an Access-created file stores it. + private static readonly ColumnSpec[] MSysObjectsColumns = + [ + new("Connect", JetDataType.Memo, 0, false, ColumnId: 9, SystemFlags: Sys), + new("Database", JetDataType.Memo, 0, false, ColumnId: 8, SystemFlags: Sys), + new("DateCreate", JetDataType.DateTime, 8, true, ColumnId: 4, SystemFlags: Sys), + new("DateUpdate", JetDataType.DateTime, 8, true, ColumnId: 5, SystemFlags: Sys), + new("Flags", JetDataType.Int32, 4, true, ColumnId: 7, SystemFlags: Sys), + new("ForeignName", JetDataType.Text, 510, false, ColumnId: 10, SystemFlags: Sys), + new("Id", JetDataType.Int32, 4, true, ColumnId: 0, SystemFlags: Sys), + new("Lv", JetDataType.Ole, 0, false, ColumnId: 13, SystemFlags: Sys), + new("LvExtra", JetDataType.Ole, 0, false, ColumnId: 16, SystemFlags: Sys), + new("LvModule", JetDataType.Ole, 0, false, ColumnId: 15, SystemFlags: Sys), + new("LvProp", JetDataType.Ole, 0, false, ColumnId: 14, SystemFlags: Sys), + new("Name", JetDataType.Text, 510, false, ColumnId: 2, SystemFlags: Sys), + new("Owner", JetDataType.Binary, 510, false, ColumnId: 6, SystemFlags: SysSid), + new("ParentId", JetDataType.Int32, 4, true, ColumnId: 1, SystemFlags: Sys), + new("RmtInfoLong", JetDataType.Ole, 0, false, ColumnId: 12, SystemFlags: Sys), + new("RmtInfoShort", JetDataType.Binary, 510, false, ColumnId: 11, SystemFlags: Sys), + new("Type", JetDataType.Int16, 2, true, ColumnId: 3, SystemFlags: Sys), + ]; + + // MSysACEs — per-object access-control rows. Real physical order + ColumnIds (ObjectId at fixed offset 0). + private static readonly ColumnSpec[] MSysAcesColumns = + [ + new("ACM", JetDataType.Int32, 4, true, ColumnId: 2, SystemFlags: Sys), + new("FInheritable", JetDataType.Boolean, 1, true, ColumnId: 3, SystemFlags: Sys), + new("ObjectId", JetDataType.Int32, 4, true, ColumnId: 0, SystemFlags: Sys), + new("SID", JetDataType.Binary, 510, false, ColumnId: 1, SystemFlags: SysSid), + ]; + + // MSysQueries — stored query/view definitions (empty in a fresh database). + private static readonly ColumnSpec[] MSysQueriesColumns = + [ + new("Attribute", JetDataType.Byte, 1, true, ColumnId: 1, SystemFlags: Sys), + new("Expression", JetDataType.Memo, 0, false, ColumnId: 5, SystemFlags: Sys), + new("Flag", JetDataType.Int16, 2, true, ColumnId: 6, SystemFlags: Sys), + new("LvExtra", JetDataType.Int32, 4, true, ColumnId: 7, SystemFlags: Sys), + new("Name1", JetDataType.Text, 510, false, ColumnId: 3, SystemFlags: Sys), + new("Name2", JetDataType.Text, 510, false, ColumnId: 4, SystemFlags: Sys), + new("ObjectId", JetDataType.Int32, 4, true, ColumnId: 0, SystemFlags: Sys), + new("Order", JetDataType.Binary, 510, false, ColumnId: 2, SystemFlags: Sys), + ]; + + // MSysRelationships — relationship (foreign key) definitions (empty in a fresh database). + private static readonly ColumnSpec[] MSysRelationshipsColumns = + [ + new("ccolumn", JetDataType.Int32, 4, true, ColumnId: 2, SystemFlags: Sys), + new("grbit", JetDataType.Int32, 4, true, ColumnId: 1, SystemFlags: Sys), + new("icolumn", JetDataType.Int32, 4, true, ColumnId: 3, SystemFlags: Sys), + new("szColumn", JetDataType.Text, 510, false, ColumnId: 5, SystemFlags: Sys), + new("szObject", JetDataType.Text, 510, false, ColumnId: 4, SystemFlags: Sys), + new("szReferencedColumn", JetDataType.Text, 510, false, ColumnId: 7, SystemFlags: Sys), + new("szReferencedObject", JetDataType.Text, 510, false, ColumnId: 6, SystemFlags: Sys), + 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. + /// Engine tables, but not part of the catalog, so their columns carry no catalog flag. + private static readonly (string Name, ColumnSpec[] Columns)[] MSysComplexTypeTables = + [ + ("MSysComplexType_UnsignedByte", [new("Value", JetDataType.Byte, 1, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_Short", [new("Value", JetDataType.Int16, 2, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_Long", [new("Value", JetDataType.Int32, 4, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_IEEESingle", [new("Value", JetDataType.Single, 4, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_IEEEDouble", [new("Value", JetDataType.Double, 8, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_GUID", [new("Value", JetDataType.Guid, 16, true, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_Decimal", [new("Value", JetDataType.FixedPoint, 9, false, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_Text", [new("Value", JetDataType.Text, 510, false, ColumnId: 0, IsEngineColumn: true)]), + ("MSysComplexType_Attachment", + [ + new("FileData", JetDataType.Ole, 0, false, ColumnId: 3, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + new("FileFlags", JetDataType.Int32, 4, true, ColumnId: 5, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + new("FileName", JetDataType.Text, 510, false, ColumnId: 1, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + new("FileTimeStamp", JetDataType.DateTime, 8, true, ColumnId: 4, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + new("FileType", JetDataType.Text, 510, false, ColumnId: 2, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + new("FileURL", JetDataType.Memo, 0, false, ColumnId: 0, IsEngineColumn: true, ExtendedFlags: AttachmentValue), + ]), + ]; + + /// Builds the initial catalog definitions and usage maps in their stored page order. + internal static (List Pages, int GlobalMapPage, int ObjectsPage, int AcesPage, int QueriesPage, int RelationshipsPage) + BuildBootstrap(JetFormatBase format, Collation sortOrder) + { + var pages = new List { null }; + int Place() { pages.Add(null); return pages.Count - 1; } + + int globalMapPage = Place(); + ColumnSpec[][] systemTables = [MSysObjectsColumns, MSysAcesColumns, MSysQueriesColumns, MSysRelationshipsColumns]; + // Every TDEF first, then every usage map, as ACE lays them out. + var tdefPages = new int[systemTables.Length]; + for (int table = 0; table < systemTables.Length; table++) + tdefPages[table] = Place(); + + // 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. + for (int table = 0; table < systemTables.Length; table++) + { + int usageMapPage = Place(); + var (tdef, usageMap) = BuildSystemTable(format, systemTables[table], usageMapPage, sortOrder); + pages[tdefPages[table]] = tdef; + pages[usageMapPage] = usageMap; + } + pages[globalMapPage] = UsageMap.BuildGlobalMapPage(format, pages.Count); + int objPage = tdefPages[0], acesPage = tdefPages[1], queriesPage = tdefPages[2], relPage = tdefPages[3]; + return (pages, globalMapPage, objPage, acesPage, queriesPage, relPage); + } + + /// Registers and indexes the initial system catalog through the normal database writers. + internal static void InitializeBootstrap(JetDatabase db, byte[] sidUsers, byte[] sidAdmin, byte[] sidEngine, byte[] sidCreator) + { + int objPage = db.DefinitionPage.CatalogRootPage; + int acesPage = db.DefinitionPage.AcesRootPage; + int queriesPage = db.DefinitionPage.QueriesRootPage; + int relPage = db.DefinitionPage.RelationshipsRootPage; + // The DAO catalog hierarchy Access navigates: a root (0x0F000000) parents the three containers + // (Tables/Databases/Relationships); tables live under Tables, MSysDb under Databases. Access looks + // objects up by (ParentId, Name), so these parents must be exact. + Table msysObjects = db.OpenTableAt(objPage, "MSysObjects"); + TableDefinition aces = db.OpenTableAt(acesPage, "MSysACEs").Definition; + void ObjectRow(int id, string name, ObjectType type, ObjectAttributes flags, int parentId, byte[]? owner) => + JetCatalog.InsertObjectRow(db.Channel, msysObjects.Definition, name, id, type, parentId, + unchecked((int)flags), owner, properties: null); + void Ace(int objectId, byte[] sid, int acm, bool inherit = false) => + JetCatalog.InsertAceRow(db.Channel, aces, objectId, sid, acm, inherit); + + // Catalog rows in the real stored order: DAO containers + MSysDb first, then the system tables, then + // the DAO "SingleRecord" pseudo-object. Access's catalog bootstrap walks MSysObjects in this order. + ObjectRow(JetCatalog.ObjectContainerParentId, "Tables", ObjectType.Container, + ObjectAttributes.System, parentId: JetCatalog.ContainerParentId, owner: sidEngine); + ObjectRow(JetCatalog.DatabaseContainerParentId, "Databases", ObjectType.Container, + ObjectAttributes.System, parentId: JetCatalog.ContainerParentId, owner: sidEngine); + ObjectRow(JetCatalog.RelationshipContainerParentId, "Relationships", ObjectType.Container, + ObjectAttributes.System, parentId: JetCatalog.ContainerParentId, owner: sidEngine); + ObjectRow(JetCatalog.DatabaseObjectId, "MSysDb", ObjectType.Database, + ObjectAttributes.System, parentId: JetCatalog.DatabaseContainerParentId, owner: sidAdmin); + ObjectRow(objPage, "MSysObjects", ObjectType.Table, + ObjectAttributes.System, parentId: JetCatalog.ObjectContainerParentId, owner: sidEngine); + ObjectRow(acesPage, "MSysACEs", ObjectType.Table, + ObjectAttributes.System, parentId: JetCatalog.ObjectContainerParentId, owner: sidEngine); + ObjectRow(queriesPage, "MSysQueries", ObjectType.Table, + ObjectAttributes.System, parentId: JetCatalog.ObjectContainerParentId, owner: sidEngine); + ObjectRow(relPage, "MSysRelationships", ObjectType.Table, + ObjectAttributes.System, parentId: JetCatalog.ObjectContainerParentId, owner: sidEngine); + ObjectRow(JetCatalog.FirstPagelessObjectId, "SingleRecord", ObjectType.SingleRecord, + ObjectAttributes.QueryDef, parentId: JetCatalog.RelationshipContainerParentId, owner: null); + + // Access-control rows (verified per-object-class masks) in the same object order as the catalog rows. + Ace(JetCatalog.ObjectContainerParentId, sidCreator, 0x0F00FE, inherit: true); + Ace(JetCatalog.ObjectContainerParentId, sidAdmin, 0x060001); + Ace(JetCatalog.ObjectContainerParentId, sidUsers, 0x0FFEFF, inherit: true); + Ace(JetCatalog.DatabaseContainerParentId, sidAdmin, 0x060000); + Ace(JetCatalog.RelationshipContainerParentId, sidCreator, 0x0F00FE, inherit: true); + Ace(JetCatalog.RelationshipContainerParentId, sidAdmin, 0x060001); + Ace(JetCatalog.RelationshipContainerParentId, sidUsers, 0x0FFFFF, inherit: true); + Ace(JetCatalog.DatabaseObjectId, sidAdmin, 0x06000E); Ace(JetCatalog.DatabaseObjectId, sidUsers, 0x00000E); // MSysDb + Ace(objPage, sidAdmin, 0x060000); Ace(objPage, sidUsers, 0x000014); // MSysObjects + Ace(acesPage, sidAdmin, 0x060000); // MSysACEs (admin only) + Ace(queriesPage, sidAdmin, 0x060000); Ace(queriesPage, sidUsers, 0x000014); // MSysQueries + Ace(relPage, sidAdmin, 0x0E0000); Ace(relPage, sidUsers, 0x000014); // MSysRelationships + + // The system tables carry the indexes Access uses to navigate the catalog. ParentIdName is the first + // real index, Id (the PK) second — matching real files. + // Its first index must use the known definition: catalog name lookup needs this index populated. + new SchemaEditor(msysObjects.Channel, db.Catalog, db.Collation).AddIndex( + msysObjects.Definition, "ParentIdName", [("ParentId", false), ("Name", false)], + isUnique: true, isPrimary: false, disallowNull: false, ignoreNulls: false); + db.CreateIndex("MSysObjects", "Id", [("Id", false)], isUnique: true, isPrimary: true); + db.CreateIndex("MSysACEs", "ObjectId", [("ObjectId", false)], disallowNull: true); + db.CreateIndex("MSysQueries", "ObjectIdAttribute", [("ObjectId", false), ("Attribute", false), ("Order", false)], isUnique: true, isPrimary: true); + db.CreateIndex("MSysRelationships", "szRelationship", [("szRelationship", 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 (db.Format.IsAccdb) CreateComplexSystemTables(db, sidEngine); + + // 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 + // a table Access's compact rejected ("-1206 Unrecognized database format"). A faithful native file mirrors + // 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, byte[] sidEngine) + { + db.CreateTable("MSysComplexColumns", MSysComplexColumnsColumns, tableType: TableType.System); + // 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); + // The registry carries the plain system flag, the flat storage tables ComplexStorage as well — as the real + // engine writes them. + MarkAsSystemTable(db, "MSysComplexColumns", ObjectAttributes.System, sidEngine); + + foreach ((string name, ColumnSpec[] columns) in MSysComplexTypeTables) + { + db.CreateTable(name, columns, tableType: TableType.System); + MarkAsSystemTable(db, name, ObjectAttributes.System | ObjectAttributes.ComplexStorage, sidEngine); + } + } + + /// Corrects the MSysObjects row the ordinary writers gave a system table to what the engine writes: + /// the system flags and the engine's owner. + private static void MarkAsSystemTable(JetDatabase db, string name, ObjectAttributes flags, byte[] sidEngine) + { + TableDefinition definition = db.Catalog.FindTable(name) + ?? throw new InvalidOperationException($"'{name}' was not found after creating it."); + + new SchemaEditor(db.Channel, db.Catalog, db.Collation).UpdateCatalogRows("MSysObjects", "Id", + definition.DefinitionPage, required: true, ("Flags", unchecked((int)flags)), ("Owner", sidEngine)); + db.Catalog.Invalidate(); + } + + /// Builds a system table's TDEF page and its owned/free usage-map page. Long-value (Memo/Ole) + /// columns each get an owned+free map on that page (rows 2 onward, after the two data-page maps), so a + /// 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, 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(); + var longValueSpecs = new List(longValueCols.Count); + 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 = TableDefinition.Build(format, TableType.System, columns, collation, + longValueColumns: longValueSpecs, usageMapPage: usageMapPage).Page; + var tdefPage = new byte[format.PageSize]; + Array.Copy(tdef, tdefPage, format.PageSize); // these system TDEFs fit one page + + byte[] usageMap = UsageMap.NewMapPage(format, 2 + longValueCols.Count * 2); + return (tdefPage, usageMap); + } + + /// The MSysObjects.ParentId of a top-level user object — the database's object container, + /// a constant 0x0F000001. Used for both table and query/view objects (verified vs Northwind). + internal const int ObjectContainerParentId = 0x0F000001; + + /// The MSysObjects.ParentId of a relationship object — the Relationships container, + /// 0x0F000003 (verified vs ACE). A relationship may share its name with a table or a query but not with + /// another relationship (verified vs ACE). + internal const int RelationshipContainerParentId = 0x0F000003; + + /// The MSysObjects.ParentId of MSysDb — the Databases container, 0x0F000002. + internal const int DatabaseContainerParentId = 0x0F000002; + + /// The MSysObjects.ParentId of every container — Tables, Forms, Reports and the + /// rest, each a row of its own whose Id its objects carry as their ParentId. 0x0F000000 itself + /// has no row (system-catalog §11). + internal const int ContainerParentId = 0x0F000000; + + /// The MSysObjects.Id of the database object, MSysDb. + internal const int DatabaseObjectId = 0x10000000; + + /// The first MSysObjects.Id of an object with no TDEF page to take its id from — a query, a + /// relationship, the SingleRecord pseudo-object. They count up from here, so they are negative. + internal const int FirstPagelessObjectId = unchecked((int)0x80000000); + + + // MSysObjects TDEF page — the catalog root, from the page-0 bootstrap pointer. + private readonly int _catalogPage; + + private static int CatalogRoot(PageChannel channel) + { + var page0 = new DatabaseDefinitionPage(); + page0.Read(channel.ReadPage(0), channel.Format); + return page0.CatalogRootPage; + } + + private readonly PageChannel _channel; + private IReadOnlyList? _tables; + private TableDefinition? _catalogDef; + // The objects of the Tables container looked up by name (FindObject) — a table's TableDefinition, a StoredQuery, null for + // neither. Dropped whenever the catalog is invalidated. + private readonly Dictionary _objects = [with(StringComparer.OrdinalIgnoreCase)]; + private IReadOnlyList? _relationships; + private IReadOnlyList? _complexColumns; + private long _seenSchemaGeneration; + + internal JetCatalog(PageChannel channel) + { + _catalogPage = CatalogRoot(channel); + _channel = channel; + _seenSchemaGeneration = channel.SchemaGeneration; + } + + /// MSysObjects' own definition, read from the TDEF page page 0's catalog root names — what every + /// catalog lookup reads it by. + private TableDefinition MSysObjects => _catalogDef ??= ReadTableDefinition(_catalogPage, "MSysObjects", isSystem: true); /// All tables in the database (user and system). - public IReadOnlyList Tables { get { EnsureFresh(); return _tables ??= LoadTables(); } } + public IReadOnlyList Tables + { + get + { + EnsureFresh(); + return _tables ??= ObjectNames(ObjectType.Table).Select(name => FindTable(name) + ?? throw new InvalidDataException($"Catalog table '{name}' could not be resolved through its name index.")) + .ToList().AsReadOnly(); + } + } /// /// The account SIDs as this database masks them: the admin user, who owns an object LibRed creates /// (LibRed acts as the default workgroup's admin), the Users group, and the Creator placeholder a container's /// inheritable grant names in place of whoever creates an object in it. Every on-disk SID is a workgroup - /// account SID XOR'd with a keystream that differs per file and is stored nowhere (page-00 §2.3) — but - /// MSysObjects is owned by the Engine account in every file, ACE-written and LibRed-written alike, so - /// its own catalog row gives the keystream's first bytes away. An object written into a file has to carry that - /// file's SIDs, or its owner decodes to no account at all. + /// account SID XOR'd with a keystream that differs per file and is stored nowhere, but which page 0 determines + /// (page-00 §2.3). An object written into a file has to carry that file's SIDs, or its owner decodes to no + /// account at all. /// - /// When that row cannot be read, the keystream is worked out from page 0 instead, which determines - /// it (page-00 §2.3). + /// From page 0 rather than from MSysObjects' own Engine-owned row, which gives the same mask + /// away: an Access-encrypted .accdb can leave that row's owner un-re-masked, and only page 0 follows the + /// key the file has now. public (byte[] Admin, byte[] Users, byte[] Creator) SecuritySids { get { - _ = Tables; // reading the catalog captures the keystream from MSysObjects' own row - if (_securitySids is { } sids) return sids; - byte[] engine = Crypto.SidKeystream.MaskAccount(_channel.ReadPageShared(0).Span, EngineAccount); - return (AsMaskedInThisFile(engine, AdminAccount), AsMaskedInThisFile(engine, UsersAccount), - AsMaskedInThisFile(engine, CreatorAccount)); + ReadOnlySpan page0 = _channel.ReadPageShared(0).Span; + return (Crypto.SidKeystream.MaskAccount(page0, Crypto.SidKeystream.AdminAccount, _channel.Format), + Crypto.SidKeystream.MaskAccount(page0, Crypto.SidKeystream.UsersAccount, _channel.Format), + Crypto.SidKeystream.MaskAccount(page0, Crypto.SidKeystream.CreatorAccount, _channel.Format)); } } - // The default workgroup's account SIDs, as a stock System.mdw's MSysAccounts holds them (page-00 §2.3). - private static readonly byte[] EngineAccount = [0x02, 0x03]; - private static readonly byte[] AdminAccount = [0x03, 0x01]; - private static readonly byte[] UsersAccount = [0x02, 0x01]; - private static readonly byte[] CreatorAccount = [0x02, 0x04]; - - private static byte[] AsMaskedInThisFile(byte[] engineOnDisk, byte[] account) => - [ - (byte)(engineOnDisk[0] ^ EngineAccount[0] ^ account[0]), - (byte)(engineOnDisk[1] ^ EngineAccount[1] ^ account[1]), - ]; - - /// 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 { 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 { 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 { EnsureFresh(); EnsureStoredQueries(); return _queryParameters!; } } + /// Every stored query in the database, by name, each read by . + public IReadOnlyDictionary Queries + { + get + { + Dictionary queries = [with(StringComparer.OrdinalIgnoreCase)]; + foreach (string name in ObjectNames(ObjectType.Query)) + if (FindQuery(name) is { } query) + queries[name] = query; + return queries; + } + } /// Drops the cached catalog so a freshly created table is picked up on next read. - public void Invalidate(bool markChanged = true) + internal void Invalidate(bool markChanged = true) { _tables = null; + _catalogDef = null; + _objects.Clear(); _relationships = null; - _views = null; - _actionQueries = null; - _queryParameters = null; _complexColumns = null; _seenSchemaGeneration = _channel.SchemaGeneration; if (markChanged) _channel.MarkSchemaChanged(); @@ -111,7 +424,7 @@ public void Invalidate(bool markChanged = true) /// values live in. Empty when the file has no MSysComplexColumns — a Jet 4 database has none. public IReadOnlyList ComplexColumns { - get { EnsureFresh(); return _complexColumns ??= LoadComplexColumns(); } + get { EnsureFresh(); return _complexColumns ??= LoadComplexColumns().AsReadOnly(); } } /// The complex column of , or null when that @@ -131,12 +444,13 @@ private List LoadComplexColumns() var resolved = new List(); if (FindTable("MSysComplexColumns") is not { } definition) return resolved; - int name = ColumnIndex(definition, "ColumnName"), id = ColumnIndex(definition, "ComplexID"), - elementType = ColumnIndex(definition, "ComplexTypeObjectID"), - owner = ColumnIndex(definition, "ConceptualTableID"), flat = ColumnIndex(definition, "FlatTableID"); - if (name < 0 || id < 0 || owner < 0 || flat < 0) return resolved; + if (definition.FindColumn("ColumnName") is not { Index: var name } || definition.FindColumn("ComplexID") is not { Index: var id } + || definition.FindColumn("ConceptualTableID") is not { Index: var owner } + || definition.FindColumn("FlatTableID") is not { Index: var flat }) + return resolved; + int? elementType = definition.FindColumn("ComplexTypeObjectID")?.Index; - foreach (object?[] row in new Storage.Table(_channel, definition).Rows()) + foreach (object?[] row in new Storage.Table(_channel, definition, this).Rows()) { if (row[name] is not string columnName) continue; if (TableWithId(row[owner]) is not { } ownerTable || TableWithId(row[flat]) is not { } flatTable) continue; @@ -152,15 +466,13 @@ private List LoadComplexColumns() resolved.Add(new ComplexColumn( columnName, row[id] is null ? 0 : Convert.ToInt32(row[id], CultureInfo.InvariantCulture), ownerTable, flatTable, ownerLink, valueId, - elementType < 0 ? null : TableWithId(row[elementType])?.Name)); + elementType is { } element ? TableWithId(row[element])?.Name : null)); } return resolved; - - static int ColumnIndex(TableDef t, string column) => t.FindColumn(column)?.Index ?? -1; } /// The table whose MSysObjects id (its TDEF page) is . - private TableDef? TableWithId(object? id) => + internal TableDefinition? TableWithId(object? id) => id is null ? null : Tables.FirstOrDefault(t => t.DefinitionPage == Convert.ToInt32(id, CultureInfo.InvariantCulture)); @@ -177,121 +489,236 @@ private void EnsureFresh() } /// All relationships (foreign keys) defined in the database. - public IReadOnlyList Relationships { get { EnsureFresh(); return _relationships ??= LoadRelationships(); } } + public IReadOnlyList Relationships { get { EnsureFresh(); return _relationships ??= LoadRelationships().AsReadOnly(); } } /// Relationships for which is the referencing (child) table. public IEnumerable ForeignKeysOf(string table) => Relationships.Where(r => string.Equals(r.Table, table, StringComparison.OrdinalIgnoreCase)); /// User (non-system) tables only. - public IEnumerable UserTables => Tables.Where(t => !t.IsSystem); + public IEnumerable UserTables => Tables.Where(t => !t.IsSystem); - public TableDef? FindTable(string name) => - Tables.FirstOrDefault(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase)); + /// The table called , or null when that name is not a table's. + public TableDefinition? FindTable(string name) => FindObject(name) as TableDefinition; - /// A catalog table the caller cannot proceed without — MSysObjects, MSysACEs and - /// the rest. Their absence is a broken database rather than a case to handle, so this throws where - /// returns null. - public TableDef RequireTable(string name) => - FindTable(name) ?? throw new InvalidOperationException($"{name} catalog table was not found."); + /// The stored query called , or null when that name is not a query's. + public StoredQuery? FindQuery(string name) => FindObject(name) as StoredQuery; - private List LoadTables() + /// + /// The object called in the Tables container, found by one seek in MSysObjects and + /// returned as what its Type makes it: a table's , a , or null + /// for any other kind of object, or none. Kept until the catalog is next invalidated. + /// + private object? FindObject(string name) { - // Build a TableDef for MSysObjects from its own (fixed) TDEF page, then scan its rows. - TableDef catalogDef = ReadTableDefinition(_catalogPage, "MSysObjects", isSystem: true); - var columns = catalogDef.Columns; - - int idIndex = ColumnIndex(columns, "Id"); - int typeIndex = ColumnIndex(columns, "Type"); - int nameIndex = ColumnIndex(columns, "Name"); - int flagsIndex = ColumnIndex(columns, "Flags"); - int lvpropIndex = ColumnIndex(columns, "LvProp"); - int ownerIndex = ColumnIndex(columns, "Owner"); + EnsureFresh(); + if (_objects.TryGetValue(name, out object? found)) return found; - var catalog = new Table(_channel, catalogDef); - var tables = new List(); + if (FindObjectRow(JetCatalog.ObjectContainerParentId, name) is not { } row) return _objects[name] = null; + return _objects[name] = row[MSysObjects.RequireColumn("Type").Index] switch + { + short t when (ObjectType)t == ObjectType.Table => ReadTable(row), + short t when (ObjectType)t == ObjectType.Query => ReadQuery(row), + _ => null, + }; + } - foreach (object?[] row in catalog.Rows()) + /// A table's definition from its MSysObjects row: its TDEF — the row's Id is that page — with + /// what the row adds, its flags and the column and table properties in its LvProp. + private TableDefinition ReadTable(object?[] row) + { + TableDefinition mo = MSysObjects; + int definitionPage = (int)row[mo.RequireColumn("Id").Index]!; + string name = (string)row[mo.RequireColumn("Name").Index]!; + var flags = (ObjectAttributes)unchecked((uint)(int)row[mo.RequireColumn("Flags").Index]!); + + // A table is "system" (excluded from the user-table list, as Access's own schema view + // does) if it is flagged system or hidden, or is named as engine/temporary infrastructure: + // MSys*, a leading '~' (temp), or a leading '#' (e.g. EFCore.Jet's hidden #Dual helper). + bool isSystem = (flags & (ObjectAttributes.System | ObjectAttributes.SystemAttribute | ObjectAttributes.Hidden)) != 0 + || name.StartsWith("MSys", StringComparison.Ordinal) + || name.StartsWith('~') + || name.StartsWith('#'); + + TableDefinition definition = ReadTableDefinition(definitionPage, name, isSystem); + definition.ObjectFlags = flags; + // Attach column DefaultValue and table CHECK properties from the extended-properties (LvProp) blob. + if (row[mo.RequireColumn("LvProp").Index] is byte[] { Length: > 0 } blob) { - if (row[typeIndex] is not short type || type != ObjectTypeTable) continue; - - int definitionPage = (int)row[idIndex]!; - string name = (string)row[nameIndex]!; - - // MSysObjects' own row carries the Engine account's SID as this file masks it — the one anchor - // for every SID a new object in this file must be written with (see SecuritySids). - if (definitionPage == _catalogPage && row[ownerIndex] is byte[] { Length: 2 } engine) - _securitySids = (AsMaskedInThisFile(engine, AdminAccount), AsMaskedInThisFile(engine, UsersAccount), - AsMaskedInThisFile(engine, CreatorAccount)); - - uint flags = unchecked((uint)(int)row[flagsIndex]!); - // A table is "system" (excluded from the user-table list, as Access's own schema view - // does) if it is flagged system or hidden, or is named as engine/temporary infrastructure: - // MSys*, a leading '~' (temp), or a leading '#' (e.g. EFCore.Jet's hidden #Dual helper). - bool isSystem = (flags & (SystemObjectFlags | HiddenObjectFlags)) != 0 - || name.StartsWith("MSys", StringComparison.Ordinal) - || name.StartsWith('~') - || name.StartsWith('#'); - - TableDef definition = ReadTableDefinition(definitionPage, name, isSystem); - definition.ObjectFlags = flags; - // Attach column DefaultValue and table CHECK properties from the extended-properties (LvProp) blob. - if (row[lvpropIndex] is byte[] { Length: > 0 } blob) + IReadOnlyList properties = PropertyBlob.Read(blob); + var defaults = PropertyBlob.ReadColumnDefaults(properties); + var required = PropertyBlob.ReadRequiredColumns(properties); + foreach (ColumnDef column in definition.Columns) { - IReadOnlyList properties = PropertyBlob.Read(blob); - var defaults = PropertyBlob.ReadColumnDefaults(properties); - var required = PropertyBlob.ReadRequiredColumns(properties); - foreach (ColumnDef column in definition.Columns) - { - if (defaults.TryGetValue(column.Name, out string? value)) - column.DefaultValue = value; - if (required.Contains(column.Name)) - column.IsNullable = false; - (column.ValidationRule, column.ValidationText) = PropertyBlob.ReadValidation(properties, column.Name); - // A calculated column's expression and REAL result type live here, not in the descriptor - // (§3.4a). Without them the stored payload can only be guessed from its width, which is - // wrong whenever the declared and expression types differ. - if (column.IsCalculated) - (column.CalculatedExpression, column.CalculatedResultType) = - PropertyBlob.ReadCalculated(properties, column.Name); - } + if (defaults.TryGetValue(column.Name, out string? value)) + column.DefaultValue = value; + if (required.Contains(column.Name)) + column.IsNullable = false; + (column.ValidationRule, column.ValidationText) = PropertyBlob.ReadValidation(properties, column.Name); + // A calculated column's expression and REAL result type live here, not in the descriptor + // (§3.4a). Without them the stored payload can only be guessed from its width, which is + // wrong whenever the declared and expression types differ. + if (column.IsCalculated) + (column.CalculatedExpression, column.CalculatedResultType) = + PropertyBlob.ReadCalculated(properties, column.Name); + } - var checks = PropertyBlob.ReadCheckConstraints(properties); - if (checks.Count > 0) definition.CheckConstraints = checks; + var checks = PropertyBlob.ReadCheckConstraints(properties); + if (checks.Count > 0) definition.CheckConstraints = checks; - (definition.ValidationRule, definition.ValidationText) = PropertyBlob.ReadValidation(properties, ""); - } - tables.Add(definition); + (definition.ValidationRule, definition.ValidationText) = PropertyBlob.ReadValidation(properties, ""); } + return definition; + } + + /// A stored query from its MSysObjects row: rebuilt from its own MSysQueries rows, keyed by the + /// row's Id. Null when it has none to rebuild from. + private StoredQuery? ReadQuery(object?[] row) + { + int id = (int)row[MSysObjects.RequireColumn("Id").Index]!; + if (FindTable("MSysQueries") is not { } mq) return null; + + int oid = mq.RequireColumn("ObjectId").Index, attr = mq.RequireColumn("Attribute").Index, + expr = mq.RequireColumn("Expression").Index, flag = mq.RequireColumn("Flag").Index, + n1 = mq.RequireColumn("Name1").Index, n2 = mq.RequireColumn("Name2").Index, + order = mq.RequireColumn("Order").Index, lvExtra = mq.RequireColumn("LvExtra").Index; + List queryRows = [.. new Table(_channel, mq, this).RowsWhere([oid], r => r[oid] is int o && o == id) + .Select(r => r.Values)]; + if (queryRows.Count == 0) return null; + + // Reconstruct it: an action query → its readback, otherwise a SELECT → its view SQL. + // The Attribute=1 row is the OPERATION row and its Flag is the query KIND, of which SELECT (1) is one + // value -- so the row's presence does not make a query an action query. Access writes it on plain + // SELECTs as well as on action queries, and it writes no such row at all for others (both shapes are + // common in the wild), which is why the kind has to be read rather than inferred from the row. + object?[]? operation = queryRows.FirstOrDefault(r => IsAttribute(r[attr], QueryAttribute.Operation)); + QueryOperation kind = operation?[flag] is short k ? (QueryOperation)k : QueryOperation.Select; + StoredQuery query = operation is not null && kind != QueryOperation.Select + ? ReconstructAction(queryRows, attr, expr, flag, n1, n2, order, lvExtra) + : new StoredQuery(Reconstruct(queryRows, attr, expr, flag, n1, n2, order, lvExtra), null, IsAction: false); + + // The declared parameters (Attribute=2 rows), in declaration order, for EXECUTE positional binding. + // Each row's Flag is the parameter's Jet type code (0 for Access's untyped parameter). + var parameters = RowsOf(queryRows, attr, order, QueryAttribute.Parameter) + .Where(r => r[n1] is string) + .Select(r => + { + JetDataType? parameterType = r[flag] is short f and not 0 ? (JetDataType)(byte)f : null; + // The declared facets ride in LvExtra: a length for text, precision and scale packed + // together for a decimal. + var (size, precision, scale) = parameterType is { } t + ? StoredQuery.UnpackParameterFacets(t, r[lvExtra] as int?) + : (null, null, null); + return new StoredQueryParameter(DeclaredParameterName((string)r[n1]!), parameterType, size, precision, scale); + }) + .ToList(); + return query with { Parameters = parameters }; + } + + /// The names of the Tables container's objects of , read from MSysObjects — + /// what the whole-catalog lists (, ) go through + /// with. + private List ObjectNames(ObjectType type) + { + EnsureFresh(); + int nameIndex = MSysObjects.RequireColumn("Name").Index, typeIndex = MSysObjects.RequireColumn("Type").Index; + var objects = new Table(_channel, MSysObjects, this); + return [.. objects.Rows(objects.DecodeOnly([nameIndex, typeIndex])) + .Where(r => r[typeIndex] is short t && (ObjectType)t == type) + .Select(r => (string)r[nameIndex]!)]; + } - return tables; + /// The MSysObjects id of the object of named , + /// or null when there is none. + /// Every object is the row of that name in its kind's container (system-catalog §11), so it is sought + /// there (). Tables and queries share the Tables container, relationships have the + /// Relationships one and the database objects the Databases one, all constants; any other container is itself a + /// row under the root, found by its name. + internal int? FindObjectId(string name, ObjectType type) + { + EnsureFresh(); + int idIdx = MSysObjects.RequireColumn("Id").Index, typeIdx = MSysObjects.RequireColumn("Type").Index; + + int? container = type switch + { + ObjectType.Table or ObjectType.Query or ObjectType.LinkedTable => JetCatalog.ObjectContainerParentId, + ObjectType.Relationship => JetCatalog.RelationshipContainerParentId, + ObjectType.Database or ObjectType.DatabaseDocument => JetCatalog.DatabaseContainerParentId, + ObjectType.Container => JetCatalog.ContainerParentId, + _ => (type switch + { + ObjectType.User => "SysRel", + ObjectType.Module => "Modules", + ObjectType.Report => "Reports", + ObjectType.Macro => "Scripts", + ObjectType.Form => "Forms", + _ => null, + }) is { } containerName + && FindObjectRow(JetCatalog.ContainerParentId, containerName) is { } containerRow + ? Convert.ToInt32(containerRow[idIdx], CultureInfo.InvariantCulture) + : null, + }; + return container is { } parent && FindObjectRow(parent, name) is { } row && row[typeIdx] is short t && (ObjectType)t == type + ? Convert.ToInt32(row[idIdx], CultureInfo.InvariantCulture) + : null; + } + + /// The MSysObjects row named in , or null. + /// Sought through MSysObjects' unique (ParentId, Name) index when its name collation can be encoded; + /// otherwise scanned through the same parent/name predicate. The seek can over-return on a text key, so the + /// name is checked again on what it finds. + internal object?[]? FindObjectRow(int container, string name) + { + EnsureFresh(); + TableDefinition mo = MSysObjects; + int parentIndex = mo.RequireColumn("ParentId").Index, nameIndex = mo.RequireColumn("Name").Index; + IndexDef byName = mo.Indexes.FirstOrDefault(i => + i.Columns is [{ Column.Index: var parent }, { Column.Index: var named }] + && parent == parentIndex && named == nameIndex) + ?? throw new InvalidDataException("MSysObjects has no (ParentId, Name) index."); + + var key = new object?[mo.Columns.Count]; + key[parentIndex] = container; + key[nameIndex] = name; + var objects = new Table(_channel, mo, this); + return mo.Columns[nameIndex].Collation.IsIndexKeyEncodable + ? objects.SeekRows(byName, key).FirstOrDefault(Matches) + : objects.RowsWhere([parentIndex, nameIndex], Matches).Select(r => r.Values).FirstOrDefault(); + + bool Matches(object?[] row) => row[parentIndex] is int p && p == container + && string.Equals(row[nameIndex] as string, name, StringComparison.OrdinalIgnoreCase); } + /// A catalog table the caller cannot proceed without — MSysObjects, MSysACEs and + /// the rest. Their absence is a broken database rather than a case to handle, so this throws where + /// returns null. + public TableDefinition RequireTable(string name) => + FindTable(name) ?? throw new InvalidOperationException($"{name} catalog table was not found."); + private List LoadRelationships() { - TableDef? def = FindTable("MSysRelationships"); + TableDefinition? def = FindTable("MSysRelationships"); if (def is null) return []; - var c = def.Columns; - int nameIdx = ColumnIndex(c, "szRelationship"); - int childTableIdx = ColumnIndex(c, "szObject"); - int childColumnIdx = ColumnIndex(c, "szColumn"); - int parentTableIdx = ColumnIndex(c, "szReferencedObject"); - int parentColumnIdx = ColumnIndex(c, "szReferencedColumn"); - int orderIdx = ColumnIndex(c, "icolumn"); - int flagsIdx = ColumnIndex(c, "grbit"); + int nameIdx = def.RequireColumn("szRelationship").Index; + int childTableIdx = def.RequireColumn("szObject").Index; + int childColumnIdx = def.RequireColumn("szColumn").Index; + int parentTableIdx = def.RequireColumn("szReferencedObject").Index; + int parentColumnIdx = def.RequireColumn("szReferencedColumn").Index; + int orderIdx = def.RequireColumn("icolumn").Index; + int flagsIdx = def.RequireColumn("grbit").Index; // One row per column; group by relationship name and order columns by icolumn. - var groups = new Dictionary Columns)>(); - foreach (object?[] row in new Table(_channel, def).Rows()) + foreach (object?[] row in new Table(_channel, def, this).Rows()) { string name = (string)row[nameIdx]!; if (!groups.TryGetValue(name, out var g)) { g = ((string)row[childTableIdx]!, (string)row[parentTableIdx]!, - (int)row[flagsIdx]!, []); + (RelationshipFlags)(int)row[flagsIdx]!, []); groups[name] = g; } g.Columns.Add(((int)row[orderIdx]!, (string)row[childColumnIdx]!, (string)row[parentColumnIdx]!)); @@ -306,105 +733,40 @@ private List LoadRelationships() LogicalIndexDef? block = FindTable(kvp.Value.Child)?.LogicalIndexes.FirstOrDefault(l => l.IsRelationship && !l.IsIncomingRelationship && string.Equals(l.Name, kvp.Key, StringComparison.OrdinalIgnoreCase)); - int flags = kvp.Value.Flags; + RelationshipFlags flags = kvp.Value.Flags; return new ForeignKey( kvp.Key, kvp.Value.Child, kvp.Value.Parent, - kvp.Value.Columns.OrderBy(x => x.Order).Select(x => (x.Column, x.ReferencedColumn)).ToList(), - IsEnforced: (flags & RelationshipFlags.DontEnforce) == 0, + kvp.Value.Columns.OrderBy(x => x.Order).Select(x => (x.Column, x.ReferencedColumn)).ToList().AsReadOnly(), + IsEnforced: !flags.HasFlag(RelationshipFlags.DontEnforce), CascadeUpdate: block is null - ? (flags & RelationshipFlags.UpdateCascade) != 0 - : block.UpdateAction == IndexBlockFormat.CascadeAction, + ? flags.HasFlag(RelationshipFlags.UpdateCascade) + : block.UpdateAction == RelationshipAction.Cascade, CascadeDelete: block is null - ? (flags & RelationshipFlags.DeleteCascade) != 0 - : block.DeleteAction == IndexBlockFormat.CascadeAction, + ? flags.HasFlag(RelationshipFlags.DeleteCascade) + : block.DeleteAction == RelationshipAction.Cascade, DeleteSetNull: block is null - ? (flags & RelationshipFlags.DeleteSetNull) != 0 - : block.DeleteAction == IndexBlockFormat.SetNullAction, - IsOneToOne: (flags & RelationshipFlags.OneToOne) != 0); + ? flags.HasFlag(RelationshipFlags.DeleteSetNull) + : block.DeleteAction == RelationshipAction.SetNull, + IsOneToOne: flags.HasFlag(RelationshipFlags.OneToOne)); }) .ToList(); } - private void EnsureStoredQueries() - { - if (_views is not null) return; - _views = [with(StringComparer.OrdinalIgnoreCase)]; - _actionQueries = [with(StringComparer.OrdinalIgnoreCase)]; - _queryParameters = [with(StringComparer.OrdinalIgnoreCase)]; - - TableDef? mqDef = FindTable("MSysQueries"); - TableDef? objDef = FindTable("MSysObjects"); - if (mqDef is null || objDef is null) return; - - // Group MSysQueries rows by ObjectId. - var mq = mqDef.Columns; - int oid = ColumnIndex(mq, "ObjectId"), attr = ColumnIndex(mq, "Attribute"), expr = ColumnIndex(mq, "Expression"), - flag = ColumnIndex(mq, "Flag"), n1 = ColumnIndex(mq, "Name1"), n2 = ColumnIndex(mq, "Name2"), order = ColumnIndex(mq, "Order"), - lvExtra = ColumnIndex(mq, "LvExtra"); - var byObject = new Dictionary>(); - foreach (object?[] row in new Table(_channel, mqDef).Rows()) - if (row[oid] is int id) - (byObject.TryGetValue(id, out var list) ? list : byObject[id] = []).Add(row); - - // For each query object, reconstruct it: an action query → ActionQueries, otherwise a SELECT → Views. - // The Attribute=1 row is the OPERATION row and its Flag is the query KIND, of which SELECT (1) is one - // value -- so the row's presence does not make a query an action query. Access writes it on plain - // SELECTs as well as on action queries, and it writes no such row at all for others (both shapes are - // common in the wild), which is why the kind has to be read rather than inferred from the row. - var oc = objDef.Columns; - int objId = ColumnIndex(oc, "Id"), objType = ColumnIndex(oc, "Type"), objName = ColumnIndex(oc, "Name"); - foreach (object?[] row in new Table(_channel, objDef).Rows()) - { - if (row[objType] is not short type || type != StoredQueryFormat.ObjectTypeQuery) continue; - if (row[objId] is not int id || row[objName] is not string name) continue; - if (!byObject.TryGetValue(id, out var rows)) continue; - - object?[]? operation = rows.FirstOrDefault(r => r[attr] is byte b && b == StoredQueryFormat.AttrOperation); - short kind = operation?[flag] is short k ? k : StoredQueryFormat.OperationSelect; - if (operation is not null && kind != StoredQueryFormat.OperationSelect) - _actionQueries[name] = ReconstructAction(rows, attr, expr, flag, n1, n2, order, lvExtra); - else if (Reconstruct(rows, attr, expr, flag, n1, n2, order, lvExtra) is { } sql) - _views[name] = sql; - - // The declared parameters (Attribute=2 rows), in declaration order, for EXECUTE positional binding. - // Each row's Flag is the parameter's Jet type code (0 for Access's untyped parameter). - var parameters = rows.Where(r => r[attr] is byte b && b == StoredQueryFormat.AttrParameter) - .OrderBy(r => r[order] is byte[] ob && ob.Length >= 4 - ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(ob) : 0) - .Where(r => r[n1] is string) - .Select(r => - { - JetDataType? type = r[flag] is short f and not 0 ? (JetDataType)(byte)f : null; - // The declared facets ride in LvExtra: a length for text, precision and scale packed - // together for a decimal. - var (size, precision, scale) = type is { } t - ? StoredQueryFormat.UnpackParameterFacets(t, r[lvExtra] as int?) - : (null, null, null); - return new StoredQueryParameter(DeclaredParameterName((string)r[n1]!), type, size, precision, scale); - }) - .ToList(); - if (parameters.Count > 0) _queryParameters[name] = parameters; - } - } - /// Rebuilds a stored action query's executable SQL from its MSysQueries rows. Handles every kind /// whose statement LibRed's engine can run — CREATE/DROP TABLE, INSERT (from VALUES or from a SELECT), /// UPDATE, DELETE and make-table; the rest return an unsupported reason. - private static StoredActionQuery ReconstructAction( + private static StoredQuery ReconstructAction( List rows, int attr, int expr, int flag, int n1, int n2, int order, int lvExtra) { - static int Ord(object? v) => v is byte[] b && b.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(b) : 0; - List OfAttr(byte a) => rows.Where(r => r[attr] is byte b && b == a).OrderBy(r => Ord(r[order])).ToList(); + object?[]? action = rows.FirstOrDefault(r => IsAttribute(r[attr], QueryAttribute.Operation)); + if (action is null) return new StoredQuery(null, "The stored action query has no action row."); - object?[]? action = rows.FirstOrDefault(r => r[attr] is byte b && b == StoredQueryFormat.AttrOperation); - if (action is null) return new StoredActionQuery(null, "The stored action query has no action row."); - - short kind = action[flag] is short f ? f : (short)0; - if (kind == StoredQueryFormat.ActionDdl) + var kind = (QueryOperation)(action[flag] is short f ? f : (short)0); + if (kind == QueryOperation.Ddl) // The whole DDL statement is in Expression (Access stored it with a leading space). - return new StoredActionQuery((action[expr] as string)?.TrimStart(), null); + return new StoredQuery((action[expr] as string)?.TrimStart(), null); // Every remaining kind keeps its sources, predicate and declared parameters where a SELECT keeps them, // so they are read the same way. A query with no FROM source at all is an INSERT … VALUES, which has @@ -412,84 +774,70 @@ private static StoredActionQuery ReconstructAction( var source = BuildFromClause(rows, attr, expr, flag, n1, n2, order); string? where = source is { } s ? WhereClause(rows, attr, expr, order, s.Extra) : null; string Where() => where is null ? "" : $" WHERE {where}"; - var columns = OfAttr(StoredQueryFormat.AttrColumn); + var columns = RowsOf(rows, attr, order, QueryAttribute.Column); string declared = ParametersClause(rows, attr, flag, n1, order, lvExtra); - // The SELECT of a make-table or an append query keeps its DISTINCT, DISTINCTROW and TOP on the same option row a - // view does, and reads back the same way. - short options = 0; - foreach (object?[] r in OfAttr(StoredQueryFormat.AttrOption)) - if (r[flag] is short o) options |= o; - // WITH OWNERACCESS OPTION ends every kind of statement, as it ends a SELECT. - string owner = (options & StoredQueryFormat.FlagOwnerAccess) != 0 ? " WITH OWNERACCESS OPTION" : ""; - string? topCount = OfAttr(StoredQueryFormat.AttrOption) - .Where(r => r[flag] is short tf && (tf & StoredQueryFormat.FlagTop) != 0) - .Select(r => r[n1] as string).FirstOrDefault(); - string selecting = ((options & StoredQueryFormat.FlagDistinctRow) != 0 ? "DISTINCTROW " - : (options & StoredQueryFormat.FlagDistinct) != 0 ? "DISTINCT " : "") - + (topCount is null ? "" : $"TOP {topCount} {((options & StoredQueryFormat.FlagPercent) != 0 ? "PERCENT " : "")}"); + // The SELECT of a make-table or an append query keeps its DISTINCT, DISTINCTROW and TOP on the same option + // row a view does, and reads back the same way; WITH OWNERACCESS OPTION ends every kind of statement, as + // it ends a SELECT. + (string selecting, string owner) = SelectOptions(rows, attr, flag, n1, order); switch (kind) { - case StoredQueryFormat.ActionAppend: + case QueryOperation.Append: { string target = Quote(action[n1] as string ?? ""); // A literal-value column (Flag 0x8000) is an INSERT … VALUES; a Flag-0 one reads its value // from the query's own FROM source, which makes it an INSERT … SELECT. Both name the target // column in Name2 and hold the value's text in Expression. if (columns.Count == 0) - return new StoredActionQuery(null, "An append query with no columns is not executed by LibRed."); + return new StoredQuery(null, "An append query with no columns is not executed by LibRed."); string targetColumns = string.Join(", ", columns.Select(r => Quote(r[n2] as string ?? ""))); string values = string.Join(", ", columns.Select(r => r[expr] as string ?? "NULL")); - if (columns.All(r => r[flag] is short cf && cf == StoredQueryFormat.AppendValueFlag)) + if (columns.All(r => r[flag] is short cf && cf == StoredQuery.AppendValueFlag)) return source is null - ? new StoredActionQuery($"{declared}INSERT INTO {target} ({targetColumns}) VALUES ({values}){owner}", null) - : new StoredActionQuery(null, "An append query cannot take both literal values and a source."); + ? new StoredQuery($"{declared}INSERT INTO {target} ({targetColumns}) VALUES ({values}){owner}", null) + : new StoredQuery(null, "An append query cannot take both literal values and a source."); return source is { } appendSource - ? new StoredActionQuery( + ? new StoredQuery( $"{declared}INSERT INTO {target} ({targetColumns}) SELECT {selecting}{values} FROM {appendSource.From}{Where()}{owner}", null) - : new StoredActionQuery(null, "An append query with no values and no source is not executed by LibRed."); + : new StoredQuery(null, "An append query with no values and no source is not executed by LibRed."); } - case StoredQueryFormat.ActionUpdate when source is { } updateSource: + case QueryOperation.Update when source is { } updateSource: { // One column row per assignment: Name2 is the target column — qualified when the update runs // over a join — and Expression is the new value. if (columns.Count == 0) - return new StoredActionQuery(null, "An update query with no assignments is not executed by LibRed."); + return new StoredQuery(null, "An update query with no assignments is not executed by LibRed."); string assignments = string.Join(", ", columns.Select(r => $"{Qualified(r[n2] as string ?? "")} = {r[expr] as string ?? "NULL"}")); - return new StoredActionQuery($"{declared}UPDATE {updateSource.From} SET {assignments}{Where()}{owner}", null); + return new StoredQuery($"{declared}UPDATE {updateSource.From} SET {assignments}{Where()}{owner}", null); } - case StoredQueryFormat.ActionDelete when source is { } deleteSource: + case QueryOperation.Delete when source is { } deleteSource: { // Access writes `DELETE
.* FROM …` when the query names the table's columns and // `DELETE * FROM …` when it doesn't; the column row holds that `
.*` verbatim. string what = columns.Count > 0 ? columns[0][expr] as string ?? "*" : "*"; - return new StoredActionQuery($"{declared}DELETE {what} FROM {deleteSource.From}{Where()}{owner}", null); + return new StoredQuery($"{declared}DELETE {what} FROM {deleteSource.From}{Where()}{owner}", null); } - case StoredQueryFormat.ActionMakeTable when source is { } intoSource: + case QueryOperation.MakeTable when source is { } intoSource: { // The target is on the action row; a target in ANOTHER database file (Name2) is a shape // LibRed has no statement for. if (action[n2] is string external && external.Length > 0) - return new StoredActionQuery(null, $"A make-table query writing into '{external}' is not executed by LibRed."); + return new StoredQuery(null, $"A make-table query writing into '{external}' is not executed by LibRed."); string selected = columns.Count == 0 ? "*" : string.Join(", ", columns.Select(r => (r[n1] as string) is { } alias ? $"{r[expr] as string} AS {Quote(alias)}" : r[expr] as string ?? "")); - var groupBy = OfAttr(StoredQueryFormat.AttrGroupBy).Select(r => r[expr] as string ?? "").ToList(); - string grouping = groupBy.Count > 0 ? $" GROUP BY {string.Join(", ", groupBy)}" : ""; - string having = OfAttr(StoredQueryFormat.AttrHaving) - .Select(r => r[expr] as string).FirstOrDefault(s => !string.IsNullOrEmpty(s)) is { } h - ? $" HAVING {h}" : ""; - return new StoredActionQuery( - $"{declared}SELECT {selecting}{selected} INTO {Quote(action[n1] as string ?? "")} FROM {intoSource.From}{Where()}{grouping}{having}{owner}", null); + return new StoredQuery( + $"{declared}SELECT {selecting}{selected} INTO {Quote(action[n1] as string ?? "")} FROM {intoSource.From}{Where()}{GroupingClauses(rows, attr, expr, order)}{owner}", null); } } @@ -497,14 +845,56 @@ private static StoredActionQuery ReconstructAction( // is leaves a caller no way to tell an unimplemented feature from an unreadable file. string reason = kind switch { - StoredQueryFormat.ActionUpdate or StoredQueryFormat.ActionDelete or StoredQueryFormat.ActionMakeTable => + QueryOperation.Update or QueryOperation.Delete or QueryOperation.MakeTable => "The stored action query names no table.", - StoredQueryFormat.ActionCrosstab => "Crosstab (TRANSFORM) stored queries are not executed by LibRed yet.", - StoredQueryFormat.ActionPassThrough => "Pass-through stored queries are not executed by LibRed yet.", - StoredQueryFormat.ActionUnion => "UNION stored queries are not executed by LibRed yet.", - _ => $"Kind-{kind} stored queries are not executed by LibRed yet.", + QueryOperation.Crosstab => "Crosstab (TRANSFORM) stored queries are not executed by LibRed yet.", + QueryOperation.PassThrough => "Pass-through stored queries are not executed by LibRed yet.", + QueryOperation.Union => "UNION stored queries are not executed by LibRed yet.", + _ => $"Kind-{(short)kind} stored queries are not executed by LibRed yet.", }; - return new StoredActionQuery(null, reason); + return new StoredQuery(null, reason); + } + + /// Whether an MSysQueries.Attribute value is . + private static bool IsAttribute(object? value, QueryAttribute attribute) => + value is byte b && (QueryAttribute)b == attribute; + + /// A stored query's rows of one , in their stored order. + private static List RowsOf(List rows, int attr, int order, QueryAttribute attribute) => + [.. rows.Where(r => IsAttribute(r[attr], attribute)).OrderBy(r => StoredQuery.UnpackOrder(r[order]))]; + + /// The keywords a query's option rows put after SELECT — DISTINCTROW or DISTINCT, then TOP n + /// [PERCENT] — and the WITH OWNERACCESS OPTION that ends the statement, or empty strings. + private static (string Selecting, string OwnerAccess) SelectOptions( + List rows, int attr, int flag, int n1, int order) + { + // The option bits are cumulative and can share one row, so test each as a bit across all of them. + List optionRows = RowsOf(rows, attr, order, QueryAttribute.Option); + var options = QueryOptions.None; + foreach (object?[] r in optionRows) + if (r[flag] is short o) options |= (QueryOptions)o; + + // DISTINCT and DISTINCTROW are separate bits and separate keywords: DISTINCT dedupes output rows, + // DISTINCTROW dedupes by the underlying contributing rows. Emitting one for the other changes results. + // TOP n: an option row with the TOP bit; the count is in Name1. The PERCENT bit rides alongside it (48 = + // TOP PERCENT), and dropping it turns "TOP 10 PERCENT" into "TOP 10" -- silently wrong. + string? top = optionRows.Where(r => r[flag] is short f && ((QueryOptions)f & QueryOptions.Top) != 0) + .Select(r => r[n1] as string).FirstOrDefault(); + string selecting = ((options & QueryOptions.DistinctRow) != 0 ? "DISTINCTROW " + : (options & QueryOptions.Distinct) != 0 ? "DISTINCT " : "") + + (top is null ? "" : $"TOP {top} {((options & QueryOptions.Percent) != 0 ? "PERCENT " : "")}"); + return (selecting, (options & QueryOptions.OwnerAccess) != 0 ? " WITH OWNERACCESS OPTION" : ""); + } + + /// A query's GROUP BY … and HAVING … clauses, each empty when it has none. HAVING is a + /// single row holding the predicate verbatim, carrying no flag of its own. + private static string GroupingClauses(List rows, int attr, int expr, int order) + { + var groupBy = RowsOf(rows, attr, order, QueryAttribute.GroupBy).Select(r => r[expr] as string ?? "").ToList(); + string? having = RowsOf(rows, attr, order, QueryAttribute.Having) + .Select(r => r[expr] as string).FirstOrDefault(s => !string.IsNullOrEmpty(s)); + return (groupBy.Count > 0 ? $" GROUP BY {string.Join(", ", groupBy)}" : "") + + (having is null ? "" : $" HAVING {having}"); } /// Bracket-quotes a stored name, so one with a space in it survives. @@ -525,15 +915,12 @@ private static string Qualified(string name) => private static (string From, List Extra)? BuildFromClause( List rows, int attr, int expr, int flag, int n1, int n2, int order) { - static int Ord(object? v) => v is byte[] b && b.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(b) : 0; - IEnumerable OfAttr(byte a) => rows.Where(r => r[attr] is byte b && b == a).OrderBy(r => Ord(r[order])); - // A derived-table source has its subquery SQL in Expression and no Name1; a named table uses Name1. - var tables = OfAttr(StoredQueryFormat.AttrTable) + var tables = RowsOf(rows, attr, order, QueryAttribute.Table) .Select(r => (Table: r[n1] as string ?? "", Alias: r[n2] as string, Sub: r[n1] is null ? r[expr] as string : null)).ToList(); if (tables.Count == 0) return null; - var joins = OfAttr(StoredQueryFormat.AttrJoin) - .Select(r => (Cond: r[expr] as string ?? "", Kind: r[flag] is short f ? f : (short)1, + var joins = RowsOf(rows, attr, order, QueryAttribute.Join) + .Select(r => (Cond: r[expr] as string ?? "", Kind: r[flag] is short f ? (ViewJoinType)f : ViewJoinType.Inner, Left: r[n1] as string ?? "", Right: r[n2] as string ?? "")).ToList(); static string Ident(string s) => $"[{s}]"; @@ -561,8 +948,13 @@ static string Render((string Table, string? Alias, string? Sub) t) => int ti = tables.FindIndex(t => string.Equals(Key(t), newKey, StringComparison.OrdinalIgnoreCase)); if (ti < 0) continue; - short kind = !leftIn && j.Kind is 2 or 3 ? (short)(j.Kind == 2 ? 3 : 2) : j.Kind; // flip on reversed order - string kw = kind switch { 2 => "LEFT", 3 => "RIGHT", _ => "INNER" }; + ViewJoinType kind = leftIn ? j.Kind : j.Kind switch // flip on reversed order + { + ViewJoinType.Left => ViewJoinType.Right, + ViewJoinType.Right => ViewJoinType.Left, + _ => j.Kind, + }; + string kw = kind switch { ViewJoinType.Left => "LEFT", ViewJoinType.Right => "RIGHT", _ => "INNER" }; from.Append(CultureInfo.InvariantCulture, $" {kw} JOIN {Render(tables[ti])} ON {j.Cond}"); used.Add(newKey); pending.RemoveAt(i); @@ -609,10 +1001,7 @@ private static string DeclaredParameterName(string declared) /// declares them exactly as a SELECT does. private static string ParametersClause(List rows, int attr, int flag, int n1, int order, int lvExtra) { - static int Ord(object? v) => v is byte[] b && b.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(b) : 0; - var parameters = rows - .Where(r => r[attr] is byte b && b == StoredQueryFormat.AttrParameter) - .OrderBy(r => Ord(r[order])) + var parameters = RowsOf(rows, attr, order, QueryAttribute.Parameter) .Select(r => (Name: r[n1] as string, Code: r[flag] is short f ? (byte)f : (byte)0, Extra: r[lvExtra] as int?)) .Where(p => p.Name is not null) // ACE stores the name as declared, its brackets included ('[@firstName]'), and renders it as it stands; @@ -629,7 +1018,7 @@ private static string ParametersClause(List rows, int attr, int flag, private static string Facets(byte code, int? lvExtra) { if (code == 0) return ""; // the untyped parameter, rendered as the keyword Value - var (size, precision, scale) = StoredQueryFormat.UnpackParameterFacets((JetDataType)code, lvExtra); + var (size, precision, scale) = StoredQuery.UnpackParameterFacets((JetDataType)code, lvExtra); return size is { } length ? $"({length})" : precision is { } p ? $"({p},{scale ?? 0})" : ""; @@ -641,10 +1030,7 @@ private static string Facets(byte code, int? lvExtra) /// Access never wrote, which is a needless difference from its own SQL. private static string? WhereClause(List rows, int attr, int expr, int order, List extra) { - static int Ord(object? v) => v is byte[] b && b.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(b) : 0; - string? where = rows - .Where(r => r[attr] is byte b && b == StoredQueryFormat.AttrWhere) - .OrderBy(r => Ord(r[order])) + string? where = RowsOf(rows, attr, order, QueryAttribute.Where) .Select(r => r[expr] as string).FirstOrDefault(); return where is null ? (extra.Count > 0 ? string.Join(" AND ", extra) : null) @@ -657,18 +1043,16 @@ private static string Facets(byte code, int? lvExtra) private static string? Reconstruct( List rows, int attr, int expr, int flag, int n1, int n2, int order, int lvExtra) { - static int Ord(object? v) => v is byte[] b && b.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(b) : 0; - IEnumerable OfAttr(byte a) => rows.Where(r => r[attr] is byte b && b == a).OrderBy(r => Ord(r[order])); - - // Bail out if the query uses attributes beyond a simple SELECT: a pass-through connection string - // (0x04) or complex-type data (0x0C) mean this is not a shape we can render. - // AttrOperation is in the list because a SELECT may carry one — the caller has already checked that - // its kind IS SELECT, so reaching here with any other kind is impossible. - var known = new byte[] { StoredQueryFormat.AttrType, StoredQueryFormat.AttrOperation, StoredQueryFormat.AttrParameter, StoredQueryFormat.AttrOption, StoredQueryFormat.AttrTable, StoredQueryFormat.AttrColumn, StoredQueryFormat.AttrJoin, StoredQueryFormat.AttrWhere, StoredQueryFormat.AttrGroupBy, StoredQueryFormat.AttrHaving, StoredQueryFormat.AttrOrderBy, 0xFF }; - if (rows.Any(r => r[attr] is byte b && !known.Contains(b))) return null; + // Bail out if the query uses attributes beyond a simple SELECT: a pass-through connection string or + // complex-type data mean this is not a shape we can render. The operation row is allowed because a SELECT + // may carry one — the caller has already checked that its kind IS SELECT, so reaching here with any other + // kind is impossible. + if (rows.Any(r => r[attr] is byte b + && ((QueryAttribute)b is QueryAttribute.Connect or QueryAttribute.Complex || !Enum.IsDefined((QueryAttribute)b)))) + return null; // A column row's Name1 (when present) is its output alias. - var columns = OfAttr(StoredQueryFormat.AttrColumn) + var columns = RowsOf(rows, attr, order, QueryAttribute.Column) .Select(r => (r[n1] as string) is { } a ? $"{r[expr] as string} AS [{a}]" : r[expr] as string ?? "").ToList(); // A query with no table rows has no FROM clause, which Access allows and stores this way (`SELECT 1 // AS n`). One with neither tables nor columns says nothing at all, and is not a query we can rebuild. @@ -678,43 +1062,21 @@ private static string Facets(byte code, int? lvExtra) // record-source query takes. Treating it as unreconstructable dropped those queries silently. if (columns.Count == 0) columns.Add("*"); - // The option bits are cumulative and can share one row, so test each as a bit across all of them. - short options = 0; - foreach (object?[] r in OfAttr(StoredQueryFormat.AttrOption)) - if (r[flag] is short f) options |= f; - - // DISTINCT and DISTINCTROW are separate bits and separate keywords: DISTINCT dedupes output rows, - // DISTINCTROW dedupes by the underlying contributing rows. Emitting one for the other changes results. - bool distinct = (options & StoredQueryFormat.FlagDistinct) != 0; - bool distinctRow = (options & StoredQueryFormat.FlagDistinctRow) != 0; - // TOP n: an AttrOption row with the TOP bit; the count is in Name1. The PERCENT bit rides alongside - // it (48 = TOP PERCENT), and dropping it turns "TOP 10 PERCENT" into "TOP 10" -- silently wrong. - string? top = OfAttr(StoredQueryFormat.AttrOption) - .Where(r => r[flag] is short f && (f & StoredQueryFormat.FlagTop) != 0) - .Select(r => r[n1] as string).FirstOrDefault(); - bool percent = (options & StoredQueryFormat.FlagPercent) != 0; - // ORDER BY: one AttrOrderBy row per key (Expression = column, Name1 = "d" for descending). - var orderBy = OfAttr(StoredQueryFormat.AttrOrderBy) + (string selecting, string owner) = SelectOptions(rows, attr, flag, n1, order); + // ORDER BY: one row per key (Expression = column, Name1 = "d" for descending). + var orderBy = RowsOf(rows, attr, order, QueryAttribute.OrderBy) .Select(r => (r[expr] as string ?? "") + (string.Equals(r[n1] as string, "d", StringComparison.OrdinalIgnoreCase) ? " DESC" : "")) .Where(s => s.Length > 0).ToList(); - var groupBy = OfAttr(StoredQueryFormat.AttrGroupBy).Select(r => r[expr] as string ?? "").ToList(); - // HAVING: a single AttrHaving row holding the predicate verbatim, carrying no flag of its own. - string? having = OfAttr(StoredQueryFormat.AttrHaving) - .Select(r => r[expr] as string).FirstOrDefault(s => !string.IsNullOrEmpty(s)); string? whereClause = WhereClause(rows, attr, expr, order, source?.Extra ?? []); var sql = new System.Text.StringBuilder(ParametersClause(rows, attr, flag, n1, order, lvExtra)); - sql.Append("SELECT "); - if (distinctRow) sql.Append("DISTINCTROW "); - else if (distinct) sql.Append("DISTINCT "); - if (top is not null) sql.Append("TOP ").Append(top).Append(percent ? " PERCENT " : " "); + sql.Append("SELECT ").Append(selecting); sql.Append(string.Join(", ", columns)); if (source is { } from) sql.Append(" FROM ").Append(from.From); if (whereClause is not null) sql.Append(" WHERE ").Append(whereClause); - if (groupBy.Count > 0) sql.Append(" GROUP BY ").Append(string.Join(", ", groupBy)); - if (having is not null) sql.Append(" HAVING ").Append(having); + sql.Append(GroupingClauses(rows, attr, expr, order)); if (orderBy.Count > 0) sql.Append(" ORDER BY ").Append(string.Join(", ", orderBy)); - if ((options & StoredQueryFormat.FlagOwnerAccess) != 0) sql.Append(" WITH OWNERACCESS OPTION"); + sql.Append(owner); return sql.ToString(); } @@ -741,36 +1103,353 @@ private static string Facets(byte code, int? lvExtra) _ => "TEXT", }; - /// Builds a straight from a TDEF page, bypassing the catalog — used - /// during database creation to seed the system tables before they are self-registered. - internal TableDef ReadTableDefinitionAt(int definitionPage, string name, bool isSystem) => - ReadTableDefinition(definitionPage, name, isSystem); + /// Builds a straight from a TDEF page, without its catalog row — what every + /// catalog lookup builds on, and what database creation and an index back-fill use to read a table they + /// already know the page of. + internal TableDefinition ReadTableDefinition(int definitionPage, string name, bool isSystem) + { + var definition = new TableDefinition { Name = name, IsSystem = isSystem }; + definition.Read(_channel, definitionPage); + return definition; + } - private TableDef ReadTableDefinition(int definitionPage, string name, bool isSystem) + + /// Inserts the object's MSysObjects row — owner admin, both dates now — maintaining the + /// table's indexes, including the (ParentId, Name) one Access resolves names through. + /// The object's name, unique within its container. + /// A table's is its TDEF page; every other object takes a synthetic negative id. + /// A table, a query or a relationship. + /// The container the object belongs to. + /// Object-class specific; a query's encodes its kind. + /// The object's extended properties, if it has any: a table's per-column + /// DefaultValue/Required and its CHECK constraints all live in this one blob. + internal void AddObjectRow( + string name, int objectId, ObjectType type, int parentId, int flags, + IReadOnlyList? properties = null) => + InsertObjectRow(_channel, RequireTable("MSysObjects"), name, objectId, type, parentId, flags, + SecuritySids.Admin, properties is { Count: > 0 } ? PropertyBlob.Write(properties) : null); + + /// Inserts one MSysObjects row into — both dates now, and + /// , when given, stored as any long value is: inline up to 64 bytes, else packed + /// onto a shared LvProp page as Access does — maintaining whatever indexes the table has. + internal static void InsertObjectRow(PageChannel channel, TableDefinition msysObjects, string name, int objectId, + ObjectType type, int parentId, int flags, byte[]? owner, byte[]? properties) { - var tdef = new TableDefinitionPage(); - tdef.Read(_channel, definitionPage); + DateTime now = DateTime.Now; + var values = new object?[msysObjects.Columns.Count]; + Set(msysObjects, values, "Id", objectId); + Set(msysObjects, values, "ParentId", parentId); + Set(msysObjects, values, "Type", (short)type); + Set(msysObjects, values, "Name", name); + Set(msysObjects, values, "Flags", flags); + if (owner is not null) Set(msysObjects, values, "Owner", owner); + Set(msysObjects, values, "DateCreate", now); + Set(msysObjects, values, "DateUpdate", now); + + var inserter = new RowInserter(channel, msysObjects); + if (properties is not null) + Set(msysObjects, values, "LvProp", inserter.StorePackedLongValue( + msysObjects.RequireColumn("LvProp").ColumnId, properties)); + + inserter.Insert(values, updateIndexes: true); + } + + /// Inserts one MSysACEs row into : 's + /// on the object, inheritable by what is created in it or not, maintaining whatever + /// indexes the table has. + internal static void InsertAceRow(PageChannel channel, TableDefinition msysAces, int objectId, byte[] sid, int acm, + bool inheritable) + { + var values = new object?[msysAces.Columns.Count]; + Set(msysAces, values, "ACM", acm); + Set(msysAces, values, "FInheritable", inheritable); + Set(msysAces, values, "ObjectId", objectId); + Set(msysAces, values, "SID", sid); + new RowInserter(channel, msysAces).Insert(values, updateIndexes: true); + } - return new TableDef + /// Inserts the object's MSysACEs rows, maintaining the ObjectId index so Access's security + /// check finds them — without them it warns about permissions on opening the object. + /// The grants are the ones the object's container passes down, derived as ACE derives them + /// (verified): the Creator account's inheritable grant becomes the owner's — the admin user, as whom LibRed + /// creates every object — and every other inheritable grant is copied for its own account, OR'd into the + /// owner's row when it names the owner's account. The owner's row comes first, the rest in the container's + /// order. So the masks follow the database: a table's owner gets 0xF00FE where the Tables container grants the + /// Creator that alone, and 0xFFEFF where it also grants admin 0xFFEFF, as Northwind's does. + internal void AddPermissionRows(int objectId, int containerId) + { + TableDefinition msysAces = RequireTable("MSysACEs"); + int idIndex = msysAces.RequireColumn("ObjectId").Index, sidIndex = msysAces.RequireColumn("SID").Index; + int acmIndex = msysAces.RequireColumn("ACM").Index, inheritIndex = msysAces.RequireColumn("FInheritable").Index; + (byte[] owner, _, byte[] creator) = SecuritySids; + + var grants = new List<(byte[] Sid, int Acm)>(); + var aces = new Table(_channel, msysAces, this); + foreach (object?[] row in aces.Rows(aces.DecodeOnly([idIndex, sidIndex, acmIndex, inheritIndex]))) + { + if (row[idIndex] is not int id || id != containerId || row[inheritIndex] is not true) continue; + byte[] sid = (byte[])row[sidIndex]!; + if (sid.AsSpan().SequenceEqual(creator)) sid = owner; + int at = grants.FindIndex(g => g.Sid.AsSpan().SequenceEqual(sid)); + if (at < 0) grants.Add((sid, (int)row[acmIndex]!)); + else grants[at] = (grants[at].Sid, grants[at].Acm | (int)row[acmIndex]!); + } + + foreach ((byte[] sid, int acm) in grants.OrderBy(g => g.Sid.AsSpan().SequenceEqual(owner) ? 0 : 1)) + InsertAceRow(_channel, msysAces, objectId, sid, acm, inheritable: false); + } + + /// Puts in the row slot occupies. + internal static void Set(TableDefinition table, object?[] values, string column, object? value) => + values[table.RequireColumn(column).Index] = value; + + + + internal void CreateView(string name, ViewSpec spec) + { + int objectId = AllocateQueryObject(name, QueryDefType.Select); + AddQueryRows(objectId, spec); + } + + /// Persists a stored action query (a non-SELECT CREATE PROCEDURE body) byte-faithfully. + internal void CreateActionQuery(string name, ActionQuerySpec spec) + { + QueryDefType type = spec.Kind switch { - Name = name, - DefinitionPage = definitionPage, - Columns = tdef.Columns, - Indexes = tdef.Indexes, - LogicalIndexes = tdef.LogicalIndexes, - RowCount = tdef.RowCount, - VariableColumnCount = tdef.VariableColumnCount, - ColumnIdHighWater = tdef.ColumnIdHighWater, - ComplexAutoNumber = tdef.ComplexAutoNumber, - IsSystem = isSystem, + ActionQueryKind.DataDefinition => QueryDefType.DataDefinition, + ActionQueryKind.Append => QueryDefType.Append, + ActionQueryKind.Update => QueryDefType.Update, + ActionQueryKind.Delete => QueryDefType.Delete, + ActionQueryKind.MakeTable => QueryDefType.MakeTable, + _ => throw new NotSupportedException($"Action query kind {spec.Kind} is not stored yet."), }; + int objectId = AllocateQueryObject(name, type); + AddActionRows(objectId, spec); } - private static int ColumnIndex(IReadOnlyList columns, string name) + /// + /// Records a relationship as ACE does (verified): a type-8 MSysObjects object in the Relationships + /// container, named after it, flags 0, with the next high-bit id — the sequence queries draw from, so the two + /// interleave, and a dropped one's id is taken again — and its two MSysACEs rows. Refuses a name + /// another relationship has, as ACE does; a table or query may share it. + /// + internal void CreateRelationshipObject(string name) => + AllocateObject(name, ObjectType.Relationship, JetCatalog.RelationshipContainerParentId, flags: 0); + + /// A stored query's object: its MSysObjects.Flags are + /// plus its DAO . + private int AllocateQueryObject(string name, QueryDefType type) + { + JetName.Validate(name, "query name"); + return AllocateObject(name, ObjectType.Query, JetCatalog.ObjectContainerParentId, + (int)ObjectAttributes.QueryDef | (int)type); + } + + /// Reserves the next free high-bit object id, checks the name is free, and writes + /// the MSysObjects row and the MSysACEs rows its container grants it. For a query the + /// distinguish view / append / data-definition. + private int AllocateObject(string name, ObjectType type, int parentId, int flags) + { + // A name must be free within the object's own container — the rule MSysObjects' unique (ParentId, Name) + // index states, and the one ACE applies (measured): a query collides with a table, a query or a linked + // table, all in the Tables container, and not with a form, report, macro, module, relationship, database + // document or container; a relationship only with another relationship. + if (FindObjectRow(parentId, name) is not null) + throw parentId == JetCatalog.RelationshipContainerParentId + ? SchemaEditor.RelationshipNameTaken(name) + : new SchemaObjectExistsException($"Object '{name}' already exists.", name); + + // The next free negative id: one past the highest in use. + TableDefinition msysObjects = RequireTable("MSysObjects"); + int idIndex = msysObjects.RequireColumn("Id").Index; + int nextId = JetCatalog.FirstPagelessObjectId; + var objects = new Table(_channel, msysObjects, this); + foreach (object?[] row in objects.Rows(objects.DecodeOnly([idIndex]))) + if (row[idIndex] is int id && id < 0 && id >= nextId) nextId = id + 1; + + AddObjectRow(name, nextId, type, parentId, flags); + AddPermissionRows(nextId, parentId); + return nextId; + } + + private void AddActionRows(int objectId, ActionQuerySpec spec) + { + TableDefinition mq = RequireTable("MSysQueries"); + + AddHeaderRows(mq, objectId, spec.Parameters); + + if (spec.Kind == ActionQueryKind.DataDefinition) + { + // The whole DDL statement is stored verbatim in one row; Access records it with a leading space. + // Nothing is decomposed: a data-definition query has no sources, columns or predicate. + Row(mq, objectId, QueryAttribute.Operation, order: 1, flag: (short)QueryOperation.Ddl, + expression: " " + spec.DdlSql); + return; + } + + // The action row carries the kind, and the target table for the two kinds that write into one. + QueryOperation kind = spec.Kind switch + { + ActionQueryKind.Append => QueryOperation.Append, + ActionQueryKind.Update => QueryOperation.Update, + ActionQueryKind.Delete => QueryOperation.Delete, + ActionQueryKind.MakeTable => QueryOperation.MakeTable, + _ => throw new NotSupportedException($"Action query kind {spec.Kind} is not stored yet."), + }; + Row(mq, objectId, QueryAttribute.Operation, order: 1, flag: (short)kind, + name1: spec.Kind is ActionQueryKind.Append or ActionQueryKind.MakeTable ? spec.TargetTable : null); + + // Sources first: Access processes the rows in order, and a derived-table source defines an alias the + // column expressions reference (the same reason the view path writes tables before columns). + AddSourceRows(mq, objectId, spec.Body); + + var values = spec.Values ?? []; + switch (spec.Kind) + { + case ActionQueryKind.Append: + // Name2 = target column, Expression = the value; the 0x8000 flag marks an INSERT … VALUES, + // where a column fed by the query's own source carries Flag 0. + for (int i = 0; i < values.Count; i++) + Row(mq, objectId, QueryAttribute.Column, order: i + 1, + flag: spec.Body is null ? StoredQuery.AppendValueFlag : (short)0, + expression: values[i].ValueExpression, name2: values[i].Column); + break; + + case ActionQueryKind.Update: + // One row per SET assignment, stored exactly as an append's columns are. + for (int i = 0; i < values.Count; i++) + Row(mq, objectId, QueryAttribute.Column, order: i + 1, flag: 0, + expression: values[i].ValueExpression, name2: values[i].Column); + break; + + case ActionQueryKind.Delete: + // `DELETE t.* FROM …` keeps that target verbatim in a single column row; `DELETE FROM …` + // stores no column row at all, and Access renders it back as `DELETE * FROM …`. + if (spec.DeleteTarget is { } target) + Row(mq, objectId, QueryAttribute.Column, order: 1, flag: 0, expression: target); + break; + + case ActionQueryKind.MakeTable: + AddColumnRows(mq, objectId, spec.Body?.Columns ?? []); + break; + } + + AddJoinAndWhereRows(mq, objectId, spec.Body); + // The option row a view carries: a make-table or append query's DISTINCT and TOP, and any kind's WITH + // OWNERACCESS OPTION. + AddOptionRow(mq, objectId, spec.Body, spec.OwnerAccess); + } + + /// + /// The query's one 0x03 option row, when it has an option: DISTINCT, WITH OWNERACCESS OPTION, TOP and PERCENT + /// are bits of the same row, the TOP count in its Name1 (verified vs ACE: DISTINCT TOP is 18, with the option 22, + /// and with PERCENT too 54; the option alone is 4; a make-table query's DISTINCT is the same row as a view's). + /// + private void AddOptionRow(TableDefinition mq, int objectId, ViewSpec? body, bool ownerAccess) + { + QueryOptions options = (body?.Distinct == true ? QueryOptions.Distinct : QueryOptions.None) + | (ownerAccess ? QueryOptions.OwnerAccess : QueryOptions.None) + | (body?.Top is null ? QueryOptions.None + : QueryOptions.Top | (body.TopPercent ? QueryOptions.Percent : QueryOptions.None)); + if (options != QueryOptions.None) + Row(mq, objectId, QueryAttribute.Option, order: 1, flag: (short)options, + name1: body?.Top?.ToString(System.Globalization.CultureInfo.InvariantCulture)); + } + + /// The rows every stored query opens with — the type row, the end row, then its declared parameters + /// in declaration order — written identically for a view and for an action query. + private void AddHeaderRows(TableDefinition mq, int objectId, IReadOnlyList? parameters) + { + Row(mq, objectId, QueryAttribute.Type, order: 1, flag: StoredQuery.QueryTypeSelect); + Row(mq, objectId, QueryAttribute.End, order: 1); + for (int i = 0; i < (parameters?.Count ?? 0); i++) + { + ViewParameterSpec p = parameters![i]; + Row(mq, objectId, QueryAttribute.Parameter, order: i + 1, + flag: p.TypeCode, name1: p.Name, + lvExtra: StoredQuery.PackParameterFacets((JetDataType)p.TypeCode, p.Size, p.Scale)); + } + } + + /// An ordinary output list, a view's or a make-table query's: Expression = the column, Name1 = its + /// alias. + private void AddColumnRows(TableDefinition mq, int objectId, IReadOnlyList columns) { for (int i = 0; i < columns.Count; i++) - if (string.Equals(columns[i].Name, name, StringComparison.OrdinalIgnoreCase)) - return columns[i].Index; - throw new InvalidOperationException($"MSysObjects is missing the '{name}' column."); + Row(mq, objectId, QueryAttribute.Column, order: i + 1, flag: 0, + expression: columns[i].Expression, name1: columns[i].Alias); + } + + /// The 0x05 FROM rows: a named table in Name1 (alias in Name2), or a derived table whose + /// subquery SQL goes in Expression with Name1 empty. + private void AddSourceRows(TableDefinition mq, int objectId, ViewSpec? body) + { + var tables = body?.Tables ?? []; + for (int i = 0; i < tables.Count; i++) + { + ViewTableSpec t = tables[i]; + if (t.SubquerySql is { } sub) + Row(mq, objectId, QueryAttribute.Table, order: i + 1, expression: sub, name2: t.Alias); + else + Row(mq, objectId, QueryAttribute.Table, order: i + 1, name1: t.Table, name2: t.Alias); + } + } + + /// The 0x07 join rows (condition, kind, and the two tables the condition names) and the + /// single 0x08 WHERE row. + private void AddJoinAndWhereRows(TableDefinition mq, int objectId, ViewSpec? body) + { + var joins = body?.Joins ?? []; + for (int i = 0; i < joins.Count; i++) + { + ViewJoinSpec j = joins[i]; + Row(mq, objectId, QueryAttribute.Join, order: i + 1, flag: (short)j.Kind, + expression: j.Condition, name1: j.LeftAlias, name2: j.RightAlias); + } + if (body?.Where is { } where) + Row(mq, objectId, QueryAttribute.Where, order: 1, expression: where); } + + private void AddQueryRows(int objectId, ViewSpec spec) + { + TableDefinition mq = RequireTable("MSysQueries"); + + // ACE's row order (verified against every Northwind view): type, end, distinct, TABLES, COLUMNS, + // joins, where. Tables must precede columns — a derived-table source defines an alias that the + // column expressions reference, and Access processes the rows in order, so columns-before-tables + // makes it fail to run the view (it opens, but SELECT-from-view errors). Order fields are + // per-attribute counters, independent of this insertion order. + // Declared parameters (CREATE PROCEDURE) come right after the End row, before the tables. + AddHeaderRows(mq, objectId, spec.Parameters); + AddOptionRow(mq, objectId, spec, spec.OwnerAccess); + AddSourceRows(mq, objectId, spec); + AddColumnRows(mq, objectId, spec.Columns); + AddJoinAndWhereRows(mq, objectId, spec); + for (int i = 0; i < (spec.GroupBy?.Count ?? 0); i++) + Row(mq, objectId, QueryAttribute.GroupBy, order: i + 1, flag: 0, expression: spec.GroupBy![i]); + // The group filter carries no flag of its own, exactly as the WHERE row doesn't. + if (spec.Having is { } having) + Row(mq, objectId, QueryAttribute.Having, order: 1, expression: having); + for (int i = 0; i < (spec.OrderBy?.Count ?? 0); i++) + Row(mq, objectId, QueryAttribute.OrderBy, order: i + 1, expression: spec.OrderBy![i].Expression, + name1: spec.OrderBy[i].Descending ? "d" : null); + } + + private void Row(TableDefinition mq, int objectId, QueryAttribute attribute, int order, + short? flag = null, string? expression = null, string? name1 = null, string? name2 = null, + int? lvExtra = null) + { + var values = new object?[mq.Columns.Count]; + Set(mq, values, "ObjectId", objectId); + Set(mq, values, "Attribute", (byte)attribute); + Set(mq, values, "Order", StoredQuery.PackOrder(order)); + if (flag is { } f) Set(mq, values, "Flag", f); + if (expression is not null) Set(mq, values, "Expression", expression); + if (name1 is not null) Set(mq, values, "Name1", name1); + if (name2 is not null) Set(mq, values, "Name2", name2); + // A declared parameter's length; ACE writes it here and renders the PARAMETERS clause from it. + if (lvExtra is { } extra) Set(mq, values, "LvExtra", extra); + new RowInserter(_channel, mq).Insert(values, updateIndexes: true); + } + } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs b/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs index cf59a6c4..c6a38340 100644 --- a/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs +++ b/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs @@ -1,3 +1,5 @@ +using LibRed.Formats; + namespace LibRed.Catalog; /// @@ -11,23 +13,13 @@ namespace LibRed.Catalog; /// Which real index () stores it. /// True when the entry is a foreign-key relationship rather than a named index. /// True when this entry is the table's primary key. -/// The relationship's direction as the info block records it: 0 none, 1 incoming -/// (this table is the parent — the half Access names .r… and keeps out of its schema views), 2 outgoing -/// (this table holds the foreign key). -/// The info block's update action (0x15): 0x04 on a plain index; on a -/// relationship 0x00 no cascade, 0x01 cascade update. Written on both ends of a relationship. -/// The info block's delete action (0x16): 0x04 on a plain index; on a -/// relationship 0x00 no cascade, 0x01 cascade delete, 0x02 ON DELETE SET NULL. +/// The relationship's direction as the info block records it. +/// The info block's update action. +/// The info block's delete action. public sealed record LogicalIndexDef( - string Name, int RealIndexOrdinal, bool IsRelationship, bool IsPrimaryKey, byte ForeignKeyType, - byte UpdateAction, byte DeleteAction) + string Name, int RealIndexOrdinal, bool IsRelationship, bool IsPrimaryKey, ForeignKeyType ForeignKeyType, + RelationshipAction UpdateAction, RelationshipAction DeleteAction) { /// True for the parent side of a relationship, which Access hides. - public bool IsIncomingRelationship => ForeignKeyType == IncomingRelationship; - - /// of the parent side. - public const byte IncomingRelationship = 1; - - /// of the child side, the table holding the foreign key. - public const byte OutgoingRelationship = 2; + public bool IsIncomingRelationship => ForeignKeyType == ForeignKeyType.Incoming; } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/LongValueColumnSpec.cs b/src/LibRed/LibRed.Core/Catalog/LongValueColumnSpec.cs new file mode 100644 index 00000000..9ef9d2fc --- /dev/null +++ b/src/LibRed/LibRed.Core/Catalog/LongValueColumnSpec.cs @@ -0,0 +1,7 @@ +namespace LibRed.Catalog; + +/// +/// The trailing §3.3.2 column-usage-map entry for a long-value (memo/OLE) column: its column id and +/// pointers to the owned- and free-pages usage maps that will track the column's LVAL pages. +/// +public sealed record LongValueColumnSpec(int ColumnId, int UsedRow, int FreeRow, int MapPage); \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/NameMap.cs b/src/LibRed/LibRed.Core/Catalog/NameMap.cs index 076769ca..195c5317 100644 --- a/src/LibRed/LibRed.Core/Catalog/NameMap.cs +++ b/src/LibRed/LibRed.Core/Catalog/NameMap.cs @@ -18,15 +18,101 @@ namespace LibRed.Catalog; /// public sealed record NameMap { + /// One MSysNameMap row (docs/format/system-catalog.md §11). + /// The object's GUID property — what identifies the row. + /// The row's Id. Not the object's MSysObjects.Id, and not decoded. + /// The object's name when the row was written, without the trailing NUL it is stored with. + /// The object's MSysObjects.Type read as unsigned: 1 table, 5 query, 32768 form, + /// 32772 report. + /// The row's map; null when its NameMap column is. + public sealed record CatalogRow(Guid ObjectGuid, int Id, string Name, int Type, NameMap? Map); + + + /// + /// One record of a : an object, or a field of one (docs/format/system-catalog.md §11). + /// + /// The object's or field's GUID — for an object, its GUID property. + /// The record kind. Not decoded: object records carry 0, 1 or 2, field records almost always 7. + /// The object's or field's name, without the terminating NUL. + public sealed record Entry(Guid ItemGuid, int Kind, string Name) + { + /// The 16-byte slot after the kind: in an object record an OLE Automation date then eight zero + /// bytes, in a field record a GUID (in most records the preceding object record's). See + /// and . + public byte[] Slot { get; init; } = new byte[NameMap.SlotSize]; + + /// The record's MSysObjects type (an object record) or data type code (a field record). Only the + /// MSysNameMap layout stores it; a record read from the property layout has zero. + public int TypeCode { get; init; } + + /// The first four bytes of the record, zero in every file examined; carried so they are written back + /// as read. + public int Reserved { get; init; } + + /// The last four fixed bytes of a MSysNameMap record, which Access leaves uninitialised — + /// whatever was in memory. Carried so a map is written back as read; zero for a new record. Not stored in + /// the property layout. + public int Unused { get; init; } + + /// Whether the name is stored with its terminating NUL, as every record but one in the files + /// examined is. Only the MSysNameMap layout can store it without. + public bool NameTerminated { get; init; } = true; + + /// The slot read as an object record's OLE Automation date; null when it holds none. + public DateTime? SlotDate + { + get + { + double days = BinaryPrimitives.ReadDoubleLittleEndian(Slot); + return days != 0 && Storage.Types.JetTypeCodec.TryFromOaDate(days, out DateTime date) ? date : null; + } + } + + /// The slot read as a field record's GUID. + public Guid SlotGuid => new(Slot); + } + + // --- The fields both layouts' records share --- + + internal const int ReservedOffset = 0; + internal const int ItemGuidOffset = 4; + internal const int GuidSize = 16; + internal const int KindOffset = 20; + internal const int SlotOffset = 24; + internal const int SlotSize = 16; + internal const int CommonLength = 40; + + /// The UTF-16 NUL that ends a name. + internal const int NameTerminatorSize = 2; + + // --- Row layout --- + + internal const int RowVersionOffset = 0; + internal const int RowFixedLengthOffset = 4; + internal const int RowHeaderSize = 8; + + /// The length prefix of each row-layout record, counting the fixed bytes and the name. + internal const int RowRecordLengthSize = 4; + + /// A row-layout record's fixed bytes: the common ones, then and + /// . + internal const int RowFixedLength = 48; + internal const int RowTypeCodeOffset = 40; + internal const int RowUnusedOffset = 44; + + // --- Property layout --- + + internal static ReadOnlySpan PropertySignature => [0x0A, 0xCC, 0x0E, 0x55]; + + /// The kind of the record that closes a property-layout map. + internal const int PropertyEndKind = 12; + + /// The MSysNameMap.NameMap version in every file examined. public const int RowVersion = 5; - private const int RowFixedLength = 48; - private const int PropertyEndKind = 12; - private static readonly byte[] PropertySignature = [0x0A, 0xCC, 0x0E, 0x55]; - /// The records, in stored order. - public IReadOnlyList Records { get; init; } = []; + public IReadOnlyList Records { get; init; } = []; /// The map's version: the header value of the row layout (), or the value /// the property layout's closing kind-12 record carries (2 to 5 seen). Null for a property blob that has no @@ -46,62 +132,64 @@ public sealed record NameMap /// The blob is not in this layout. public static NameMap ReadRow(ReadOnlySpan blob) { - if (blob.Length < 8) - throw new InvalidDataException($"A name map needs an 8-byte header; this one is {blob.Length} bytes."); - int fixedLength = BinaryPrimitives.ReadInt32LittleEndian(blob[4..]); + if (blob.Length < RowHeaderSize) + throw new InvalidDataException( + $"A name map needs a {RowHeaderSize}-byte header; this one is {blob.Length} bytes."); + int fixedLength = BinaryPrimitives.ReadInt32LittleEndian(blob[RowFixedLengthOffset..]); if (fixedLength != RowFixedLength) throw new InvalidDataException($"A name map's records have {RowFixedLength} fixed bytes; this one declares {fixedLength}."); - var records = new List(); - int pos = 8; + var records = new List(); + int pos = RowHeaderSize; while (pos < blob.Length) { - int length = pos + 4 <= blob.Length ? BinaryPrimitives.ReadInt32LittleEndian(blob[pos..]) : -1; - if (length < RowFixedLength || length % 2 != 0 || length > blob.Length - pos - 4) + int length = pos + RowRecordLengthSize <= blob.Length ? BinaryPrimitives.ReadInt32LittleEndian(blob[pos..]) : -1; + if (length < RowFixedLength || length % 2 != 0 || length > blob.Length - pos - RowRecordLengthSize) throw new InvalidDataException($"The name map record at offset {pos} overruns the blob."); - ReadOnlySpan record = blob.Slice(pos + 4, length); + ReadOnlySpan record = blob.Slice(pos + RowRecordLengthSize, length); ReadOnlySpan name = record[RowFixedLength..]; - bool terminated = name.Length >= 2 && name[^2] == 0 && name[^1] == 0; - records.Add(ReadCommon(record, Encoding.Unicode.GetString(terminated ? name[..^2] : name)) with + bool terminated = name.Length >= NameTerminatorSize && name[^2] == 0 && name[^1] == 0; + records.Add(ReadCommon(record, Encoding.Unicode.GetString(terminated ? name[..^NameTerminatorSize] : name)) with { - TypeCode = BinaryPrimitives.ReadInt32LittleEndian(record[40..]), - Unused = BinaryPrimitives.ReadInt32LittleEndian(record[44..]), + TypeCode = BinaryPrimitives.ReadInt32LittleEndian(record[RowTypeCodeOffset..]), + Unused = BinaryPrimitives.ReadInt32LittleEndian(record[RowUnusedOffset..]), NameTerminated = terminated, }); - pos += 4 + length; + pos += RowRecordLengthSize + length; } - return new NameMap { Records = records, Version = BinaryPrimitives.ReadInt32LittleEndian(blob) }; + return new NameMap { Records = records, Version = BinaryPrimitives.ReadInt32LittleEndian(blob[RowVersionOffset..]) }; } /// Decodes the NameMap property of an object's LvProp: the signature /// 0A CC 0E 55, then records of [40 fixed bytes][UTF-16 name, NUL-terminated] up to a closing - /// record of kind 12. A property record has no or - /// ; both read as zero. + /// record of kind 12. A property record has no or + /// ; both read as zero. /// The blob is not in this layout. public static NameMap ReadProperty(ReadOnlySpan blob) { - if (blob.Length < 4 || !blob[..4].SequenceEqual(PropertySignature)) + ReadOnlySpan signature = PropertySignature; + if (blob.Length < signature.Length || !blob[..signature.Length].SequenceEqual(signature)) throw new InvalidDataException("A NameMap property begins with the signature 0A CC 0E 55."); - var records = new List(); - int pos = 4; - while (pos + 42 <= blob.Length) + var records = new List(); + int pos = signature.Length; + while (pos + CommonLength + NameTerminatorSize <= blob.Length) { - int end = pos + 40; - while (end + 1 < blob.Length && (blob[end] | blob[end + 1]) != 0) end += 2; + int end = pos + CommonLength; + while (end + 1 < blob.Length && (blob[end] | blob[end + 1]) != 0) end += NameTerminatorSize; if (end + 1 >= blob.Length) break; // no terminating NUL: not a record, left as trailing bytes - ReadOnlySpan record = blob[pos..(end + 2)]; - if (BinaryPrimitives.ReadInt32LittleEndian(record[20..]) == PropertyEndKind) + ReadOnlySpan record = blob[pos..(end + NameTerminatorSize)]; + if (BinaryPrimitives.ReadInt32LittleEndian(record[KindOffset..]) == PropertyEndKind) return new NameMap { Records = records, - Version = BinaryPrimitives.ReadInt32LittleEndian(record[24..]), - _endSlot = record.Slice(24, 16).ToArray(), - _trailing = blob[(end + 2)..].ToArray(), + Version = BinaryPrimitives.ReadInt32LittleEndian(record[SlotOffset..]), + _endSlot = record.Slice(SlotOffset, SlotSize).ToArray(), + _trailing = blob[(end + NameTerminatorSize)..].ToArray(), }; - records.Add(ReadCommon(record, Encoding.Unicode.GetString(blob[(pos + 40)..end]))); - pos = end + 2; + records.Add(ReadCommon(record, Encoding.Unicode.GetString(blob[(pos + CommonLength)..end]))); + pos = end + NameTerminatorSize; } return new NameMap { Records = records, Version = null, _trailing = blob[pos..].ToArray() }; } @@ -113,7 +201,7 @@ public byte[] WriteRow() var blob = new List(); AppendInt32(blob, Version ?? RowVersion); AppendInt32(blob, RowFixedLength); - foreach (NameMapRecord record in Records) + foreach (NameMap.Entry record in Records) { byte[] name = EncodeName(record); AppendInt32(blob, RowFixedLength + name.Length); @@ -130,44 +218,46 @@ public byte[] WriteRow() /// name is always NUL-terminated here, since the NUL is what ends it. public byte[] WriteProperty() { - var blob = new List(PropertySignature); - foreach (NameMapRecord record in Records) + var blob = new List(PropertySignature.ToArray()); + foreach (NameMap.Entry record in Records) { AppendCommon(blob, record); blob.AddRange(Encoding.Unicode.GetBytes(record.Name)); - blob.AddRange([0, 0]); + blob.AddRange(new byte[NameTerminatorSize]); } if (Version is int version) { - byte[] slot = _endSlot is null ? new byte[16] : (byte[])_endSlot.Clone(); + byte[] slot = _endSlot is null ? new byte[SlotSize] : (byte[])_endSlot.Clone(); BinaryPrimitives.WriteInt32LittleEndian(slot, version); - AppendCommon(blob, new NameMapRecord(Guid.Empty, PropertyEndKind, "") { Slot = slot }); - blob.AddRange([0, 0]); + AppendCommon(blob, new NameMap.Entry(Guid.Empty, PropertyEndKind, "") { Slot = slot }); + blob.AddRange(new byte[NameTerminatorSize]); } blob.AddRange(_trailing); return [.. blob]; } /// The 40 bytes both layouts share: [int32 reserved][GUID][int32 kind][16-byte slot]. - private static NameMapRecord ReadCommon(ReadOnlySpan record, string name) => - new(new Guid(record.Slice(4, 16)), BinaryPrimitives.ReadInt32LittleEndian(record[20..]), name) + private static NameMap.Entry ReadCommon(ReadOnlySpan record, string name) => + new(new Guid(record.Slice(ItemGuidOffset, GuidSize)), BinaryPrimitives.ReadInt32LittleEndian(record[KindOffset..]), name) { - Reserved = BinaryPrimitives.ReadInt32LittleEndian(record), - Slot = record.Slice(24, 16).ToArray(), + Reserved = BinaryPrimitives.ReadInt32LittleEndian(record[ReservedOffset..]), + Slot = record.Slice(SlotOffset, SlotSize).ToArray(), }; - private static void AppendCommon(List blob, NameMapRecord record) + /// The common fields, in field order — the inverse of . + private static void AppendCommon(List blob, NameMap.Entry record) { - if (record.Slot.Length != 16) - throw new ArgumentException($"A name map record's slot is 16 bytes; '{record.Name}' has {record.Slot.Length}.", nameof(record)); + if (record.Slot.Length != SlotSize) + throw new ArgumentException( + $"A name map record's slot is {SlotSize} bytes; '{record.Name}' has {record.Slot.Length}.", nameof(record)); AppendInt32(blob, record.Reserved); blob.AddRange(record.ItemGuid.ToByteArray()); AppendInt32(blob, record.Kind); blob.AddRange(record.Slot); } - private static byte[] EncodeName(NameMapRecord record) => - [.. Encoding.Unicode.GetBytes(record.Name), .. record.NameTerminated ? new byte[2] : []]; + private static byte[] EncodeName(NameMap.Entry record) => + [.. Encoding.Unicode.GetBytes(record.Name), .. record.NameTerminated ? new byte[NameTerminatorSize] : []]; private static void AppendInt32(List blob, int value) { @@ -175,57 +265,4 @@ private static void AppendInt32(List blob, int value) BinaryPrimitives.WriteInt32LittleEndian(bytes, value); blob.AddRange(bytes); } -} - -/// One MSysNameMap row (docs/format/system-catalog.md §11). -/// The object's GUID property — what identifies the row. -/// The row's Id. Not the object's MSysObjects.Id, and not decoded. -/// The object's name when the row was written, without the trailing NUL it is stored with. -/// The object's MSysObjects.Type read as unsigned: 1 table, 5 query, 32768 form, -/// 32772 report. -/// The row's map; null when its NameMap column is. -public sealed record NameMapRow(Guid ObjectGuid, int Id, string Name, int Type, NameMap? Map); - -/// -/// One record of a : an object, or a field of one (docs/format/system-catalog.md §11). -/// -/// The object's or field's GUID — for an object, its GUID property. -/// The record kind. Not decoded: object records carry 0, 1 or 2, field records almost always 7. -/// The object's or field's name, without the terminating NUL. -public sealed record NameMapRecord(Guid ItemGuid, int Kind, string Name) -{ - /// The 16-byte slot after the kind: in an object record an OLE Automation date then eight zero - /// bytes, in a field record a GUID (in most records the preceding object record's). See - /// and . - public byte[] Slot { get; init; } = new byte[16]; - - /// The record's MSysObjects type (an object record) or data type code (a field record). Only the - /// MSysNameMap layout stores it; a record read from the property layout has zero. - public int TypeCode { get; init; } - - /// The first four bytes of the record, zero in every file examined; carried so they are written back - /// as read. - public int Reserved { get; init; } - - /// The last four fixed bytes of a MSysNameMap record, which Access leaves uninitialised — - /// whatever was in memory. Carried so a map is written back as read; zero for a new record. Not stored in - /// the property layout. - public int Unused { get; init; } - - /// Whether the name is stored with its terminating NUL, as every record but one in the files - /// examined is. Only the MSysNameMap layout can store it without. - public bool NameTerminated { get; init; } = true; - - /// The slot read as an object record's OLE Automation date; null when it holds none. - public DateTime? SlotDate - { - get - { - double days = BinaryPrimitives.ReadDoubleLittleEndian(Slot); - return days != 0 && double.IsFinite(days) && days is > -657435 and < 2958466 ? DateTime.FromOADate(days) : null; - } - } - - /// The slot read as a field record's GUID. - public Guid SlotGuid => new(Slot); -} +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs b/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs index fe5b456f..0cd06eaa 100644 --- a/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs +++ b/src/LibRed/LibRed.Core/Catalog/PropertyBlob.cs @@ -1,3 +1,4 @@ +using LibRed.Storage.Types; using System.Buffers.Binary; using System.Text; @@ -18,12 +19,46 @@ namespace LibRed.Catalog; /// public static class PropertyBlob { - private static readonly byte[] SignatureAce = "MR2\0"u8.ToArray(); - private static readonly byte[] SignatureMdb = "KKD\0"u8.ToArray(); - private const ushort NameListBlock = 0x0080; - private const ushort TableBlock = 0x0000; // value block owned by the table (empty owner name) - private const ushort ColumnBlock = 0x0001; // value block owned by a column - private const byte DdlFlag = 0x01; + /// The signature an ACE blob opens with; on an older .mdb. + internal static ReadOnlySpan SignatureAce => "MR2\0"u8; + internal static ReadOnlySpan SignatureMdb => "KKD\0"u8; + internal const int SignatureSize = 4; + + // --- Block: [length:4][type:2][body] --- + + internal const int BlockLengthOffset = 0; + internal const int BlockTypeOffset = 4; + internal const int BlockHeaderSize = 6; + + /// The block types: the property-name pool, and the value blocks owned by the table (empty owner + /// name) and by a column. By mdbtools' account an index's block is 0x0002. + internal const ushort NameListBlock = 0x0080; + internal const ushort TableBlock = 0x0000; + internal const ushort ColumnBlock = 0x0001; + + /// A name-pool entry's, and an owner's, UTF-16 byte length ahead of the text. + internal const int NameLengthSize = 2; + + // --- Owner record: [recordLength:2][unmodelled:2][nameLength:2][UTF-16 name] --- + + internal const int OwnerRecordLengthOffset = 0; + internal const int OwnerUnmodelledOffset = 2; + internal const int OwnerNameLengthOffset = 4; + internal const int OwnerHeaderSize = 6; + + // --- Property entry: [entryLength:2][flags:1][dataType:1][nameIndex:2][valueLength:2][value] --- + + internal const int EntryLengthOffset = 0; + internal const int EntryFlagsOffset = 2; + internal const int EntryTypeOffset = 3; + internal const int EntryNameIndexOffset = 4; + internal const int EntryValueLengthOffset = 6; + internal const int EntryHeaderSize = 8; + + /// The entry flag marking a DDL property, protected as part of the object's definition. + internal const byte DdlFlag = 0x01; + + public const string DefaultValueProperty = "DefaultValue"; public const string CheckConstraintsProperty = "CheckConstraints"; public const string RequiredProperty = "Required"; @@ -127,14 +162,14 @@ public static Property Of(string owner, string name, object value, JetDataType? /// signature alone. Omit it only when authoring a blob from nothing, where ACE's signature is the default. public static byte[] Write(IReadOnlyList properties, ReadOnlySpan original = default) { - var names = original.Length > 4 ? Parse(original).Names.ToList() : []; + var names = original.Length > SignatureSize ? Parse(original).Names.ToList() : []; foreach (string name in properties.Select(p => p.Name)) if (!names.Contains(name)) names.Add(name); ValidateForWrite(properties, names); var nameIndex = new Dictionary(); for (int i = 0; i < names.Count; i++) nameIndex.TryAdd(names[i], i); - var blob = new List(original.Length >= 4 ? original[..4].ToArray() : SignatureAce); + var blob = new List(original.Length >= SignatureSize ? original[..SignatureSize].ToArray() : SignatureAce.ToArray()); var namesBody = new List(); foreach (string name in names) AppendString(namesBody, name); @@ -171,7 +206,7 @@ public static byte[] AddColumnProperties(ReadOnlySpan blob, string owner, ValidateNames(names); ValidateOwnerProperties(owner, newProps, nameIndex); - var result = new List(blob[..4].ToArray()); // keep the original signature, MR2\0 or KKD\0 + var result = new List(blob[..SignatureSize].ToArray()); // keep the original signature, MR2\0 or KKD\0 var namesBody = new List(); foreach (string n in names) AppendString(namesBody, n); AppendBlock(result, NameListBlock, namesBody); @@ -188,10 +223,7 @@ private static void AppendOwnerBlock( Property[] propertyArray = props.ToArray(); ValidateOwnerProperties(owner, propertyArray, nameIndex); var body = new List(); - var ownerRec = new List { 0, 0, 0, 0 }; // [recLen placeholder][0x0000] - AppendString(ownerRec, owner); - BinaryPrimitives.WriteUInt16LittleEndian(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(ownerRec), (ushort)ownerRec.Count); - body.AddRange(ownerRec); + AppendOwnerRecord(body, owner, unmodelled: 0); foreach (Property p in propertyArray) { @@ -199,18 +231,30 @@ private static void AppendOwnerBlock( // anything LibRed does not model round-trips byte-for-byte. A LibRed-constructed property has no // RawValue and is encoded from Value/Type (Boolean = one 0/1 byte, else UTF-16). byte[] value = PropertyValue(p); - var entry = new List(); - AppendUInt16(entry, (ushort)(2 + 1 + 1 + 2 + 2 + value.Length)); // entry length - entry.Add(p.Flags); - entry.Add((byte)p.Type); - AppendUInt16(entry, (ushort)nameIndex[p.Name]); - AppendUInt16(entry, (ushort)value.Length); - entry.AddRange(value); - body.AddRange(entry); + // In field order: [entryLength][flags][dataType][nameIndex][valueLength][value]. + AppendUInt16(body, (ushort)(EntryHeaderSize + value.Length)); + body.Add(p.Flags); + body.Add((byte)p.Type); + AppendUInt16(body, (ushort)nameIndex[p.Name]); + AppendUInt16(body, (ushort)value.Length); + body.AddRange(value); } AppendBlock(blob, type, body); } + /// Appends an owner record in field order — its length, the unmodelled word, the owner's name — the + /// inverse of . + private static void AppendOwnerRecord(List body, string owner, ushort unmodelled) + { + int start = body.Count; + AppendUInt16(body, 0); // the record's length, known once the name is in + AppendUInt16(body, unmodelled); + AppendString(body, owner); + BinaryPrimitives.WriteUInt16LittleEndian( + System.Runtime.InteropServices.CollectionsMarshal.AsSpan(body)[(start + OwnerRecordLengthOffset)..], + (ushort)(body.Count - start)); + } + /// /// Removes the property-value block owned by (a column being dropped), keeping /// every other block — including the name pool — verbatim. The name pool is deliberately left untouched @@ -225,7 +269,7 @@ public static byte[] RemoveOwner(ReadOnlySpan blob, string owner) ParsedBlob parsed = Parse(blob); var result = new List(blob.Length); - result.AddRange(blob[..4]); // signature + result.AddRange(blob[..SignatureSize]); foreach (ParsedBlock block in parsed.Blocks) { // Only the column's own block: an index's can carry the same name. @@ -250,7 +294,7 @@ public static byte[] RenameOwner(ReadOnlySpan blob, string oldOwner, strin ParsedBlob parsed = Parse(blob); var result = new List(blob.Length); - result.AddRange(blob[..4]); // signature + result.AddRange(blob[..SignatureSize]); foreach (ParsedBlock block in parsed.Blocks) { if (block.Type != ColumnBlock @@ -260,25 +304,15 @@ public static byte[] RenameOwner(ReadOnlySpan blob, string oldOwner, strin continue; } - // Owner record: [uint16 recordLength][uint16 unmodelled][uint16 ownerLength][UTF-16 name]. - // Everything past it is this owner's property entries, which the rename must not disturb. - int oldRecordLength = BinaryPrimitives.ReadUInt16LittleEndian(block.Body.AsSpan(0, 2)); - byte[] nameBytes = Encoding.Unicode.GetBytes(newOwner); - - var body = new List(block.Body.Length - oldRecordLength + 6 + nameBytes.Length); - byte[] header = new byte[6]; - BinaryPrimitives.WriteUInt16LittleEndian(header.AsSpan(0, 2), (ushort)(6 + nameBytes.Length)); - block.Body.AsSpan(2, 2).CopyTo(header.AsSpan(2, 2)); // preserved verbatim - BinaryPrimitives.WriteUInt16LittleEndian(header.AsSpan(4, 2), (ushort)nameBytes.Length); - body.AddRange(header); - body.AddRange(nameBytes); + // A new owner record, its unmodelled word carried through; everything past the old one is this + // owner's property entries, which the rename must not disturb. + int oldRecordLength = BinaryPrimitives.ReadUInt16LittleEndian( + block.Body.AsSpan(OwnerRecordLengthOffset, sizeof(ushort))); + var body = new List(); + AppendOwnerRecord(body, newOwner, + BinaryPrimitives.ReadUInt16LittleEndian(block.Body.AsSpan(OwnerUnmodelledOffset, sizeof(ushort)))); body.AddRange(block.Body.AsSpan(oldRecordLength).ToArray()); - - byte[] raw = new byte[6 + body.Count]; - BinaryPrimitives.WriteInt32LittleEndian(raw.AsSpan(0, 4), raw.Length); - BinaryPrimitives.WriteUInt16LittleEndian(raw.AsSpan(4, 2), block.Type); - body.CopyTo(raw, 6); - result.AddRange(raw); + AppendBlock(result, block.Type, body); } return [.. result]; @@ -398,23 +432,25 @@ private sealed record ParsedBlob( /// same block, owner-record, entry, and name-index boundaries. private static ParsedBlob Parse(ReadOnlySpan blob) { - if (blob.Length < 4) - throw new InvalidDataException($"Property blob has {blob.Length} bytes; expected a 4-byte signature."); - if (!blob[..4].SequenceEqual(SignatureAce) && !blob[..4].SequenceEqual(SignatureMdb)) + if (blob.Length < SignatureSize) + throw new InvalidDataException( + $"Property blob has {blob.Length} bytes; expected a {SignatureSize}-byte signature."); + if (!blob[..SignatureSize].SequenceEqual(SignatureAce) && !blob[..SignatureSize].SequenceEqual(SignatureMdb)) throw new InvalidDataException("Property blob has an unknown signature."); var blocks = new List(); - int pos = 4; + int pos = SignatureSize; while (pos < blob.Length) { - if (blob.Length - pos < 6) + if (blob.Length - pos < BlockHeaderSize) throw new InvalidDataException($"Property blob has {blob.Length - pos} trailing bytes after its last complete block."); - int length = BinaryPrimitives.ReadInt32LittleEndian(blob.Slice(pos, 4)); - if (length < 6 || length > blob.Length - pos) + int length = BinaryPrimitives.ReadInt32LittleEndian(blob.Slice(pos + BlockLengthOffset, sizeof(int))); + if (length < BlockHeaderSize || length > blob.Length - pos) throw new InvalidDataException( $"Property block at {pos} declares invalid length {length} with {blob.Length - pos} bytes remaining."); - ushort type = BinaryPrimitives.ReadUInt16LittleEndian(blob.Slice(pos + 4, 2)); - blocks.Add(new ParsedBlock(type, blob.Slice(pos + 6, length - 6).ToArray(), blob.Slice(pos, length).ToArray())); + ushort type = BinaryPrimitives.ReadUInt16LittleEndian(blob.Slice(pos + BlockTypeOffset, sizeof(ushort))); + blocks.Add(new ParsedBlock(type, blob.Slice(pos + BlockHeaderSize, length - BlockHeaderSize).ToArray(), + blob.Slice(pos, length).ToArray())); pos += length; } @@ -434,10 +470,10 @@ private static void ReadNames(ReadOnlySpan body, List names) int pos = 0; while (pos < body.Length) { - if (body.Length - pos < 2) + if (body.Length - pos < NameLengthSize) throw new InvalidDataException("Property name pool ends inside a length field."); - int length = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos, 2)); - pos += 2; + int length = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos, NameLengthSize)); + pos += NameLengthSize; if ((length & 1) != 0 || length > body.Length - pos) throw new InvalidDataException( $"Property name declares invalid UTF-16 length {length} with {body.Length - pos} bytes remaining."); @@ -448,39 +484,43 @@ private static void ReadNames(ReadOnlySpan body, List names) private static string ReadOwner(ReadOnlySpan body) { - if (body.Length < 6) - throw new InvalidDataException("Property owner block is shorter than its 6-byte owner header."); - int recordLength = BinaryPrimitives.ReadUInt16LittleEndian(body[..2]); - int ownerLength = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(4, 2)); - if (recordLength < 6 || recordLength > body.Length || (ownerLength & 1) != 0 || ownerLength != recordLength - 6) + if (body.Length < OwnerHeaderSize) + throw new InvalidDataException( + $"Property owner block is shorter than its {OwnerHeaderSize}-byte owner header."); + int recordLength = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(OwnerRecordLengthOffset, sizeof(ushort))); + int ownerLength = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(OwnerNameLengthOffset, sizeof(ushort))); + if (recordLength < OwnerHeaderSize || recordLength > body.Length || (ownerLength & 1) != 0 + || ownerLength != recordLength - OwnerHeaderSize) throw new InvalidDataException( $"Property owner record length {recordLength} and UTF-16 name length {ownerLength} are inconsistent."); - return Encoding.Unicode.GetString(body.Slice(6, ownerLength)); + return Encoding.Unicode.GetString(body.Slice(OwnerHeaderSize, ownerLength)); } private static void ReadProperties(ushort block, ReadOnlySpan body, List names, List properties) { string owner = ReadOwner(body); - int pos = BinaryPrimitives.ReadUInt16LittleEndian(body[..2]); + int pos = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(OwnerRecordLengthOffset, sizeof(ushort))); while (pos < body.Length) { - if (body.Length - pos < 8) - throw new InvalidDataException("Property value block ends inside an 8-byte entry header."); - int entryLength = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos, 2)); - int nameIndex = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 4, 2)); - int valueLength = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos + 6, 2)); - if (entryLength < 8 || entryLength > body.Length - pos || valueLength != entryLength - 8) + if (body.Length - pos < EntryHeaderSize) + throw new InvalidDataException( + $"Property value block ends inside an {EntryHeaderSize}-byte entry header."); + ReadOnlySpan entry = body[pos..]; + int entryLength = BinaryPrimitives.ReadUInt16LittleEndian(entry.Slice(EntryLengthOffset, sizeof(ushort))); + int nameIndex = BinaryPrimitives.ReadUInt16LittleEndian(entry.Slice(EntryNameIndexOffset, sizeof(ushort))); + int valueLength = BinaryPrimitives.ReadUInt16LittleEndian(entry.Slice(EntryValueLengthOffset, sizeof(ushort))); + if (entryLength < EntryHeaderSize || entryLength > body.Length - pos || valueLength != entryLength - EntryHeaderSize) throw new InvalidDataException( $"Property entry at {pos} has inconsistent entry/value lengths {entryLength}/{valueLength}."); if (nameIndex >= names.Count) throw new InvalidDataException( $"Property entry at {pos} names pool index {nameIndex}, but the pool has {names.Count} entries."); - var dataType = (JetDataType)body[pos + 3]; - byte[] raw = body.Slice(pos + 8, valueLength).ToArray(); + var dataType = (JetDataType)entry[EntryTypeOffset]; + byte[] raw = entry.Slice(EntryHeaderSize, valueLength).ToArray(); properties.Add(new Property(owner, names[nameIndex], Format(Decode(dataType, raw)), dataType, raw) { - Flags = body[pos + 2], + Flags = entry[EntryFlagsOffset], Block = block, }); pos += entryLength; @@ -506,9 +546,9 @@ private static void ValidateNames(List names) int length = Encoding.Unicode.GetByteCount(name); if (length > ushort.MaxValue) throw new ArgumentException($"Property name '{name[..Math.Min(name.Length, 32)]}' is too long for its 16-bit byte length."); - bodyLength += 2L + length; + bodyLength += (long)NameLengthSize + length; } - if (bodyLength > int.MaxValue - 6) + if (bodyLength > int.MaxValue - BlockHeaderSize) throw new ArgumentException("Property name-pool block exceeds its 32-bit block length."); } @@ -516,21 +556,21 @@ private static void ValidateOwnerProperties( string owner, IEnumerable properties, Dictionary nameIndex) { int ownerLength = Encoding.Unicode.GetByteCount(owner); - if (ownerLength > ushort.MaxValue - 6) + if (ownerLength > ushort.MaxValue - OwnerHeaderSize) throw new ArgumentException("Property owner name is too long for its 16-bit owner-record length.", nameof(owner)); - long bodyLength = 6L + ownerLength; + long bodyLength = (long)OwnerHeaderSize + ownerLength; foreach (Property property in properties) { if (!nameIndex.TryGetValue(property.Name, out int index) || index > ushort.MaxValue) throw new ArgumentException($"Property '{property.Name}' has no encodable 16-bit name-pool index."); int valueLength = PropertyValue(property).Length; - if (valueLength > ushort.MaxValue - 8) + if (valueLength > ushort.MaxValue - EntryHeaderSize) throw new ArgumentException( $"Property '{property.Name}' value is too long for its 16-bit entry length."); - bodyLength += 8L + valueLength; + bodyLength += (long)EntryHeaderSize + valueLength; } - if (bodyLength > int.MaxValue - 6) + if (bodyLength > int.MaxValue - BlockHeaderSize) throw new ArgumentException($"Property block for owner '{owner}' exceeds its 32-bit block length."); } @@ -561,14 +601,15 @@ private static void ValidateOwnerProperties( } break; case JetDataType.Currency when raw.Length == 8: - return BinaryPrimitives.ReadInt64LittleEndian(raw) / 10000m; + return JetTypeCodec.CurrencyFromScaled(BinaryPrimitives.ReadInt64LittleEndian(raw)); case JetDataType.Single when raw.Length == 4: return BinaryPrimitives.ReadSingleLittleEndian(raw); case JetDataType.Double when raw.Length == 8: return BinaryPrimitives.ReadDoubleLittleEndian(raw); case JetDataType.DateTime when raw.Length == 8: - double serial = BinaryPrimitives.ReadDoubleLittleEndian(raw); - return serial is >= -657435.0 and < 2958466.0 ? DateTime.FromOADate(serial) : raw.ToArray(); + return JetTypeCodec.TryFromOaDate(BinaryPrimitives.ReadDoubleLittleEndian(raw), out DateTime date) + ? date + : raw.ToArray(); case JetDataType.Guid when raw.Length == 16: return new Guid(raw); } @@ -631,9 +672,7 @@ private static byte[] Encode(JetDataType type, object value) case JetDataType.Int64: b = new byte[8]; BinaryPrimitives.WriteInt64LittleEndian(b, Convert.ToInt64(value, invariant)); return b; case JetDataType.Currency: - b = new byte[8]; - BinaryPrimitives.WriteInt64LittleEndian(b, decimal.ToInt64(decimal.Round(Convert.ToDecimal(value, invariant) * 10000m))); - return b; + b = new byte[8]; BinaryPrimitives.WriteInt64LittleEndian(b, JetTypeCodec.CurrencyToScaled(value, invariant)); return b; case JetDataType.Single: b = new byte[4]; BinaryPrimitives.WriteSingleLittleEndian(b, Convert.ToSingle(value, invariant)); return b; case JetDataType.Double: @@ -667,7 +706,7 @@ private static byte[] Encode(JetDataType type, object value) private static void AppendBlock(List blob, ushort type, List body) { - AppendInt32(blob, checked(body.Count + 6)); + AppendInt32(blob, checked(body.Count + BlockHeaderSize)); // [length][type], in field order AppendUInt16(blob, type); blob.AddRange(body); } diff --git a/src/LibRed/LibRed.Core/Catalog/RecordLayout.cs b/src/LibRed/LibRed.Core/Catalog/RecordLayout.cs deleted file mode 100644 index 263b4824..00000000 --- a/src/LibRed/LibRed.Core/Catalog/RecordLayout.cs +++ /dev/null @@ -1,92 +0,0 @@ -using LibRed.Formats; - -namespace LibRed.Catalog; - -/// -/// Validates a column declaration against the two width limits ACE enforces when it opens a file. LibRed -/// writes the format directly, bypassing ACE's DDL parser, so without these it emits tables ACE refuses — -/// both measured against ACE (OLE DB) 2026-09-06, and both in the same way as the name rules in -/// : the damage is to the whole database, not the one table. -/// -/// -/// -/// Per-field width. A non-long-value column holds at most bytes — 255 -/// characters of Text, or 510 bytes of Binary — fixed or variable alike. ACE refuses the declaration -/// outright through its own DDL (TEXT(256) and BINARY(511) both give "Size of field is too -/// long", on CREATE TABLE, ALTER COLUMN and ADD COLUMN), and a file LibRed writes with -/// a wider column opens but cannot be queried: SELECT fails with "The size of a field is too long". -/// -/// -/// Declared record size. The widest record the declaration permits must fit -/// . ACE budgets the fixed region, the null bitmap and the -/// variable section's own overhead — but not the declared widths of the variable columns, which is -/// why an all-Text table is unconstrained while a wide fixed region is not. Measured at two very different -/// column counts, 31 bytes apart exactly as the differing bitmap widths predict: -/// -/// -/// 8 fixed columns: 4053 fixed bytes opens, 4054 gives "Unrecognized database format". -/// 252 fixed columns: 4022 opens, 4023 does not. -/// 4018 fixed bytes with 0, 1 and 2 variable columns opens; a third takes it past and fails. -/// -/// -/// The 4-byte allowance for a table with no variable columns at all is ACE's, not a row LibRed writes — -/// omits the variable section entirely in that case, so LibRed's own rows -/// come in 4 bytes under what ACE reserves for them. See docs/format/page-02b-columns.md. -/// -/// -public static class RecordLayout -{ - /// The widest a single non-Memo/OLE column may be declared: 255 Text characters, or 510 bytes - /// of Binary. - public const int MaxFieldBytes = 510; - - /// The widest a BigBinary column may be declared: BIGBINARY(4001) gives ACE's "Size of - /// field is too long". - public const int MaxBigBinaryBytes = 4000; - - /// Throws if a column is declared wider than ACE stores. Memo and OLE are exempt — their data - /// lives on long-value pages and the in-row descriptor is a fixed size. - public static void ValidateFieldWidth(string columnName, JetDataType type, int lengthBytes) - { - if (type is JetDataType.Memo or JetDataType.Ole) return; - if (type == JetDataType.BigBinary) - { - if (lengthBytes > MaxBigBinaryBytes) - throw new NotSupportedException( - $"Column '{columnName}' is declared {lengthBytes} bytes wide; a BigBinary column holds at most " - + $"{MaxBigBinaryBytes}."); - return; - } - if (lengthBytes > MaxFieldBytes) - throw new NotSupportedException( - $"Column '{columnName}' is declared {lengthBytes} bytes wide; a column holds at most {MaxFieldBytes} " - + $"bytes ({MaxFieldBytes / 2} Text characters). Use Memo or OLE for anything longer."); - } - - /// The largest record the declaration can produce: the row header, the fixed region, the - /// variable section's overhead and the null bitmap. Mirrors 's layout - /// except that a table with no variable columns still gets ACE's 4-byte allowance for the section. - /// Sum of the fixed columns' widths, excluding Boolean (which has no data). - /// Number of variable-length columns, Memo and OLE included. - /// The column-id high-water plus one — what sizes the null bitmap. - public static int WidestRecord(int fixedBytes, int variableColumns, int columnCount) => - 2 // leading column count - + fixedBytes - + (variableColumns == 0 ? 4 : (variableColumns + 1) * 2 + 2) // offset table + the count - + (columnCount + 7) / 8; // null bitmap - - /// Throws if the declaration's widest possible record is one ACE would refuse the file for. - /// may be null where the caller does not know it (the table is still being - /// built), in which case the message just says "the table". - public static void ValidateRecordFits( - string? tableName, int fixedBytes, int variableColumns, int columnCount, JetFormatBase format) - { - int widest = WidestRecord(fixedBytes, variableColumns, columnCount); - if (widest > format.MaxRecordSize) - throw new NotSupportedException( - $"{(tableName is null ? "The table" : $"Table '{tableName}'")} declares {fixedBytes} bytes of " - + $"fixed-length columns over {columnCount} column ids, so its widest record would be {widest} " - + $"bytes; a record holds at most {format.MaxRecordSize}. Make the wide columns variable-length, " - + "or move them to Memo/OLE, which live on their own pages."); - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/RelationshipSpec.cs b/src/LibRed/LibRed.Core/Catalog/RelationshipSpec.cs index da6767e8..2c39ca28 100644 --- a/src/LibRed/LibRed.Core/Catalog/RelationshipSpec.cs +++ b/src/LibRed/LibRed.Core/Catalog/RelationshipSpec.cs @@ -24,12 +24,12 @@ public sealed record RelationshipSpec( bool DeleteSetNull = false, // ON UPDATE SET NULL: the docs list it, but the ACE OLE DB provider rejects it via SQL DDL ("Invalid // argument"), so its on-disk storage (grbit flag + info-block +0x15 byte) couldn't be probed. The - // pathway is threaded through; TableCreator throws NotImplemented rather than guess the bytes. See the + // pathway is threaded through; SchemaEditor throws NotImplemented rather than guess the bytes. See the // libred-foreign-key-status memory / spec §11. bool UpdateSetNull = false, bool ReferencesPrimaryKey = false, // How many of the table's columns were declared before this constraint, which is what decides the order - // of the usage-map rows CREATE TABLE lays out (see TableCreator). Only a statement knows it; 0 — the + // of the usage-map rows CREATE TABLE lays out (see SchemaEditor). Only a statement knows it; 0 — the // default a direct caller gets — puts the constraint's map row ahead of every long-value column's, which // is what ACE writes for a constraint declared on the first column. int DeclaredAfterColumns = 0) diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Catalog/SchemaEditor.cs similarity index 63% rename from src/LibRed/LibRed.Core/Storage/TableCreator.cs rename to src/LibRed/LibRed.Core/Catalog/SchemaEditor.cs index 4d0e0fd2..85039f48 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs +++ b/src/LibRed/LibRed.Core/Catalog/SchemaEditor.cs @@ -1,13 +1,13 @@ using EntityFrameworkCore.Jet.Data; -using LibRed.Catalog; using LibRed.Formats; using LibRed.IO; using LibRed.Pages; +using LibRed.Storage; using System.Buffers.Binary; using System.Globalization; using MapRetirement = ((int Row, int Page) Map, System.Collections.Generic.IReadOnlyList<(int Row, int Page)> Clear, System.Collections.Generic.IReadOnlyList Pages); -namespace LibRed.Storage; +namespace LibRed.Catalog; /// /// Creates and alters tables in an existing database. allocates and writes the TDEF @@ -22,48 +22,27 @@ namespace LibRed.Storage; /// database, whose keys the rest of the file does not sort by. /// The database file. /// The catalog to read and keep current. -public sealed class TableCreator(PageChannel channel, JetCatalog catalog, Collation collation) +internal sealed class SchemaEditor(PageChannel channel, JetCatalog catalog, Collation collation) { private readonly PageChannel _channel = channel; private readonly JetCatalog _catalog = catalog; - private readonly PageAllocator _allocator = new(channel); + private readonly PageAllocator _allocator = channel.Allocator; private readonly Collation _collation = collation; - /// - /// Jet/ACE caps a table at 32 indexes and the cap applies to BOTH TDEF counts — the index-data blocks at - /// 0x33 and the logical index-info blocks at 0x2F. Microsoft states it against the logical - /// one: "Number of indexes in a table: 32, including indexes created internally to maintain table - /// relationships, single-field and composite indexes." - /// - /// - /// - /// checks both when a table is created with all its indexes at once, but EF does - /// not work that way: it creates the table, then adds indexes and relationships one statement at a time, - /// and each of those comes through a surgical insert here instead. Nothing checked those paths, so the - /// counts simply walked past 32 — the fields are Int32, so nothing overflowed. - /// - /// - /// The logical count is the one that binds, because a data block must be named by a logical block, so - /// 0x33 ≤ 0x2F always holds. A table many others reference gains a logical block per incoming - /// relationship and no data block, so it overruns on 0x2F while 0x33 still looks healthy — - /// which is exactly what the read-side check inspected. Building EF Core's - /// ComplexNavigationsSharedType model, Level1 reached 46 logical against 31 data, and the - /// resulting file was unreadable by Access ("Unrecognized database format", the table missing entirely) - /// while LibRed read it back without complaint. Measured in IndexCountLimitAccessTests; see - /// docs/format/page-02d-constraints.md. - /// - /// - private const int MaxIndexesPerTable = 32; - - /// Rejects an incremental index or relationship that would take either TDEF count past the - /// Jet/ACE limit, naming both counts so the failure says which one bound. - private static void EnsureIndexCapacity(string tableName, string what, int dataCount, int logicalCount) + /// Rejects an incremental index or relationship that would take either TDEF count past + /// , naming both counts so the failure says which one bound. + /// checks both when a table is created with all its indexes at once, but EF + /// does not work that way: it creates the table, then adds indexes and relationships one statement at a time, + /// and each of those comes through a surgical insert here instead. Nothing checked those paths, so the counts + /// simply walked past the limit — the fields are Int32, so nothing overflowed. + private void EnsureIndexCapacity(string tableName, string what, int dataCount, int logicalCount) { - if (dataCount > MaxIndexesPerTable || logicalCount > MaxIndexesPerTable) + int max = _channel.Format.MaxIndexesPerTable; + if (dataCount > max || logicalCount > max) { throw new NotSupportedException( $"Cannot add {what} to '{tableName}': it would leave the table with {dataCount} index-data blocks and " + - $"{logicalCount} logical index blocks. A table can have at most {MaxIndexesPerTable} of each, counting " + + $"{logicalCount} logical index blocks. A table can have at most {max} of each, counting " + "those backing primary keys, unique constraints and relationships - and a table referenced by many " + "others accumulates a logical block per incoming relationship without gaining a data block."); } @@ -79,7 +58,8 @@ public void Create( IReadOnlyList<(string Column, string DefaultSql)>? columnDefaults = null, IReadOnlyList<(string Name, string Expression)>? checkConstraints = null, string? primaryKeyName = null, - int primaryKeyDeclaredAfterColumns = 0) + int primaryKeyDeclaredAfterColumns = 0, + TableType tableType = TableType.User) { relationships ??= []; uniqueConstraints ??= []; @@ -109,11 +89,10 @@ public void Create( if (ObjectNameExists(name, exceptObjectId: 0)) 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. - if (columns.Count > MaxColumnsPerTable) + // Fail early with a clear message rather than write a table Access would refuse to open. + if (columns.Count > _channel.Format.MaxColumnsPerTable) throw new InvalidOperationException( - $"Table '{name}' has {columns.Count} columns; a table can have at most {MaxColumnsPerTable}."); + $"Table '{name}' has {columns.Count} columns; a table can have at most {_channel.Format.MaxColumnsPerTable}."); // Before the foreign keys' type match, as ACE checks it: an OLE column referencing a LONG key gets this. RejectOleIndexColumns( @@ -176,11 +155,10 @@ public void Create( // many *whole* columns as fit (a page holds ~57 inline records). Once the primary page is full, each // remaining long-value column gets its OWN usage-map page (owned = row 0, free = row 1). All maps // start empty. This keeps a wide table's per-column maps from overflowing a single page. - // How many 69-byte inline map records (plus their 2-byte directory slot) fit on one page. - int mapsPerPage = (format.PageSize - format.DataRowDirectoryOffset) / (UsageMapRecordLength + 2); + int mapsPerPage = UsageMap.RecordsFitting(format, format.PageSize - format.DataRowDirectoryOffset); int primaryRecords = 2 + indexPlans.Count; // data owned/free + one per index int colsOnPrimary = Math.Clamp((mapsPerPage - primaryRecords) / 2, 0, longValueCols.Count); - WriteUsageMaps(format, usageMapPage, mapCount: primaryRecords + colsOnPrimary * 2); + _channel.WritePage(usageMapPage, UsageMap.NewMapPage(format, primaryRecords + colsOnPrimary * 2)); // Which row each of them gets. Rows 0 and 1 are the table's own; the rest go in the order the CREATE // TABLE statement DECLARES them (verified vs ACE): a long-value column takes two where its column is @@ -221,7 +199,7 @@ public void Create( else { int columnMapPage = _allocator.Allocate(); - WriteUsageMaps(format, columnMapPage, mapCount: 2); // owned = row 0, free = row 1 + _channel.WritePage(columnMapPage, UsageMap.NewMapPage(format, 2)); // owned = row 0, free = row 1 longValueSpecs.Add(new LongValueColumnSpec(colId, UsedRow: 0, FreeRow: 1, MapPage: columnMapPage)); } } @@ -233,7 +211,12 @@ public void Create( for (int i = 0; i < indexPlans.Count; i++) { var plan = indexPlans[i]; - int rootPage = plan.Fk is null ? AllocateIndexRoot(format, tdefPage, indexRows[i], usageMapPage) : 0; + int rootPage = 0; + if (plan.Fk is null) + { + rootPage = AllocateIndexRoot(format, tdefPage); + new UsageMap(_channel).SetBit(indexRows[i], usageMapPage, rootPage, set: true); + } indexes.Add(new IndexSpec(plan.Name, plan.Columns, plan.IsPk, plan.IsUnique, rootPage, UsageMapRow: indexRows[i], UsageMapPage: usageMapPage)); } @@ -242,7 +225,7 @@ public void Create( // a foreign key's data block instead carries the *outgoing* relationship block (§3.6), linked // to an *incoming* block. The two ends cross-reference by index_num. For a cross-table FK the // incoming block is added to the parent's TDEF; for a self-reference it lives in this same TDEF. - var childLogical = new List(indexPlans.Count); + var childLogical = new List(indexPlans.Count); var incoming = new List(); var parentAdds = new Dictionary(); // Incoming blocks that this table hosts for its own self-references are numbered after the @@ -253,19 +236,14 @@ public void Create( var plan = indexPlans[i]; if (plan.Fk is null) { - childLogical.Add(new TdefBuilder.LogicalIndexSpec( - Number: i, DataOrdinal: i, FkType: 0, FkNumber: 0xFFFFFFFF, FkTablePage: 0, - UpdateAction: IndexBlockFormat.PlainAction, DeleteAction: IndexBlockFormat.PlainAction, - Type: plan.IsPk ? IndexBlockFormat.TypePrimary : IndexBlockFormat.TypeSecondary, Name: plan.Name)); + childLogical.Add(TableDefinition.LogicalIndexSpec.Plain(i, i, plan.IsPk, plan.Name)); continue; } RelationshipSpec fk = plan.Fk; if (fk.UpdateSetNull) throw UpdateSetNullNotImplemented(); - byte upd = fk.CascadeUpdate ? IndexBlockFormat.CascadeAction : IndexBlockFormat.NoCascadeAction; - byte del = fk.CascadeDelete ? IndexBlockFormat.CascadeAction - : fk.DeleteSetNull ? IndexBlockFormat.SetNullAction : IndexBlockFormat.NoCascadeAction; - byte outgoingType = fk.NoIndex ? FkTypeOutgoingNoIndex : FkTypeOutgoing; + (RelationshipAction upd, RelationshipAction del) = Actions(fk); + ForeignKeyType outgoingType = fk.NoIndex ? ForeignKeyType.OutgoingNoIndex : ForeignKeyType.Outgoing; // A self-referencing FK: the table is not in the catalog yet (we are creating it), so resolve // the referenced index within the plans we are building and host both ends here. @@ -274,15 +252,13 @@ public void Create( int refOrdinal = SelfReferencedOrdinal(indexPlans, fk); int inNum = selfIncomingNum++; // Outgoing block (this table's child side) — NO INDEX flags it 0x03 instead of 0x02. - childLogical.Add(new TdefBuilder.LogicalIndexSpec( + childLogical.Add(new TableDefinition.LogicalIndexSpec( Number: i, DataOrdinal: i, FkType: outgoingType, FkNumber: (uint)inNum, FkTablePage: tdefPage, UpdateAction: upd, DeleteAction: del, - Type: IndexBlockFormat.TypeForeign, Name: fk.Name)); + Type: IndexInfoType.Foreign, Name: fk.Name)); // Incoming block (this table's parent side), hidden ".r" name unique within the table. - childLogical.Add(new TdefBuilder.LogicalIndexSpec( - Number: inNum, DataOrdinal: refOrdinal, FkType: FkTypeIncoming, FkNumber: (uint)i, - FkTablePage: tdefPage, UpdateAction: upd, DeleteAction: del, - Type: IndexBlockFormat.TypeForeign, Name: HiddenRelationshipName(inNum))); + childLogical.Add(new IncomingRelationship(tdefPage, inNum, refOrdinal, (uint)i, tdefPage, upd, del) + .Spec(HiddenRelationshipName(inNum))); continue; } @@ -294,10 +270,10 @@ public void Create( : parentNextNum; parentAdds[parentPage] = parentNum; - childLogical.Add(new TdefBuilder.LogicalIndexSpec( + childLogical.Add(new TableDefinition.LogicalIndexSpec( Number: i, DataOrdinal: i, FkType: outgoingType, FkNumber: (uint)parentNum, FkTablePage: parentPage, UpdateAction: upd, DeleteAction: del, - Type: IndexBlockFormat.TypeForeign, Name: fk.Name)); + Type: IndexInfoType.Foreign, Name: fk.Name)); incoming.Add(new IncomingRelationship(parentPage, parentNum, refOrd, ChildBlockNumber: (uint)i, ChildPage: tdefPage, upd, del)); } @@ -305,16 +281,12 @@ public void Create( // Access stores logical blocks sorted by name, ignoring case (with their names in the same order). childLogical.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); - // Build the definition and point it at the usage maps: owned-pages = row 0, free-pages = - // row 1, both on the usage-map page. - byte[] tdef = TdefBuilder.Build(format, TableType.User, columns, _collation, indexes, longValueSpecs, childLogical).Page; - tdef[format.TdefOwnedPagesOffset] = 0; // owned map record row - WriteInt24(tdef, format.TdefOwnedPagesOffset + 1, usageMapPage); - tdef[format.TdefFreePagesOffset] = 1; // free map record row - WriteInt24(tdef, format.TdefFreePagesOffset + 1, usageMapPage); + // Build the definition, pointed at the usage maps on the usage-map page. + byte[] tdef = TableDefinition.Build(format, tableType, columns, _collation, indexes, longValueSpecs, childLogical, + usageMapPage).Page; // A wide table's definition can exceed one page; write it split across continuation pages if needed. - int defEnd = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(format.TdefLengthOffset, 4)); - WriteDefinition(tdefPage, tdef[..defEnd], [], rewrite: false); + int defEnd = TableDefinition.ReadLength(tdef, format); + TableDefinition.WriteChain(_channel, _allocator, tdefPage, tdef[..defEnd], [], rewrite: false); // Per-column extended properties, in column order with DefaultValue before Required (matching ACE): // a DEFAULT is a memo property; a NOT NULL column carries a boolean Required property, and a nullable @@ -333,7 +305,8 @@ public void Create( AddCatalogRow(name, tdefPage, columnProps, checkConstraints, ownsComplexColumns: columns.Any(c => c.Type == JetDataType.Complex)); - AddPermissionRows(tdefPage); + // A new table's permission rows: what the Tables container grants what it creates (system-catalog §11). + _catalog.AddPermissionRows(tdefPage, JetCatalog.ObjectContainerParentId); // Each foreign key's index is built after its relationship's rows, as ACE builds it (verified: a database's // first relationship takes MSysRelationships' first data page, and the key's index root the page after). @@ -341,23 +314,24 @@ public void Create( { if (indexPlans[i].Fk is not { } fk) continue; AddRelationshipRows(name, fk); - int rootPage = AllocateIndexRoot(format, tdefPage, indexRows[i], usageMapPage); + int rootPage = AllocateIndexRoot(format, tdefPage); + new UsageMap(_channel).SetBit(indexRows[i], usageMapPage, rootPage, set: true); _catalog.Invalidate(); - TableDef created = _catalog.RequireTable(name); - new IndexWriter(_channel, created).UpdateIndexRoot(created.Indexes.First(ix => ix.RealIndexOrdinal == i), rootPage); + TableDefinition created = _catalog.RequireTable(name); + new IndexTree(_channel, created).UpdateIndexRoot(created.Indexes.First(ix => ix.RealIndexOrdinal == i), rootPage); } foreach (IncomingRelationship inc in incoming) AddIncomingRelationshipBlock(inc); } - /// Allocates an index's root — an empty leaf — and records it in the index's own pages usage map, as - /// Access does at CREATE, before any row exists (verified: a freshly created empty index has exactly its root - /// bit set). As the tree grows, IndexWriter adds each page it allocates, so the map covers the whole B-tree. - private int AllocateIndexRoot(JetFormatBase format, int tdefPage, int mapRow, int mapPage) + /// Allocates an index's root — an empty leaf owned by the table at . Every + /// caller then records it in the index's own pages usage map, as Access does at CREATE, before any row exists + /// (verified: a freshly created empty index has exactly its root bit set); as the tree grows, IndexTree adds + /// each page it allocates, so the map covers the whole B-tree. + private int AllocateIndexRoot(JetFormatBase format, int tdefPage) { int rootPage = _allocator.Allocate(); - WriteEmptyLeafIndexPage(format, rootPage, owner: tdefPage); - new UsageMapWriter(_channel).SetBit(mapRow, mapPage, rootPage, set: true); + _channel.WritePage(rootPage, IndexTree.EmptyLeaf(format, owner: tdefPage)); return rootPage; } @@ -397,14 +371,24 @@ private int AllocateIndexRoot(JetFormatBase format, int tdefPage, int mapRow, in /// guess the bytes, fail loudly until a UI/DAO-created sample can be probed. private static NotImplementedException UpdateSetNullNotImplemented() => new( "ON UPDATE SET NULL is not supported. Only ON UPDATE NO ACTION and ON UPDATE CASCADE are supported."); - private const byte FkTypeIncoming = 0x01; // this table is the parent/referenced end - private const byte FkTypeOutgoing = 0x02; // this table is the child/referencing end (indexed) - private const byte FkTypeOutgoingNoIndex = 0x03; // child/referencing end declared FOREIGN KEY NO INDEX + + /// The update and delete actions a relationship's info blocks carry, on both its ends. + private static (RelationshipAction Update, RelationshipAction Delete) Actions(RelationshipSpec fk) => + (fk.CascadeUpdate ? RelationshipAction.Cascade : RelationshipAction.NoCascade, + fk.CascadeDelete ? RelationshipAction.Cascade + : fk.DeleteSetNull ? RelationshipAction.SetNull : RelationshipAction.NoCascade); /// An incoming-relationship logical block to add to a parent table's TDEF. private readonly record struct IncomingRelationship( int ParentPage, int Number, int ReferencedOrdinal, uint ChildBlockNumber, int ChildPage, - byte UpdateAction, byte DeleteAction); + RelationshipAction UpdateAction, RelationshipAction DeleteAction) + { + /// The block: index_num2 names the parent's referenced-key data block, and the foreign + /// key fields cross-link to the child's outgoing block. + public TableDefinition.LogicalIndexSpec Spec(string name) => new( + Number, ReferencedOrdinal, ForeignKeyType.Incoming, ChildBlockNumber, ChildPage, + UpdateAction, DeleteAction, IndexInfoType.Foreign, name); + } /// /// ACE's "same data types" rule for a relationship, measured over every pairing of the column types: each child @@ -429,7 +413,7 @@ private static void EnsureSameDataTypes(RelationshipSpec fk, private static Func ColumnOf(IReadOnlyList columns) => name => columns.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase))?.Type; - private static Func ColumnOf(TableDef? table) => name => table?.FindColumn(name)?.Type; + private static Func ColumnOf(TableDefinition? table) => name => table?.FindColumn(name)?.Type; /// The data-block ordinal of the index over a self-reference's referenced columns, found /// among the indexes being created for this table (the table is not in the catalog yet). @@ -461,20 +445,15 @@ private static int SelfReferencedOrdinal( /// number past it — a self-reference's incoming block, numbered after its outgoing one. private int NextLogicalIndexNumber(int tdefPage, int above = -1) { - (LibRed.IO.PageBuffer buf, _) = ReadDefinition(tdefPage); - TdefRegions regions = TdefRegions.Of(buf.Span, _channel.Format); - - var used = new HashSet(); - for (int i = 0; i < regions.LogicalCount; i++) - used.Add(buf.ReadInt32( - regions.InfoBlocks + i * IndexBlockFormat.InfoBlockSize + IndexBlockFormat.InfoNumberOffset)); + var used = TableDefinition.ReadParts(_channel, tdefPage).Logical + .Select(b => TableDefinition.ReadInfoBlock(b.Info, _channel.Format, NameOf(b.Name)).Number).ToHashSet(); int next = above + 1; while (used.Contains(next)) next++; return next; } /// A relationship's parent table, or ACE's error when it does not exist. - private TableDef ReferencedTableOf(RelationshipSpec fk) => + private TableDefinition ReferencedTableOf(RelationshipSpec fk) => _catalog.FindTable(fk.ReferencedTable) ?? throw new InvalidOperationException( $"Cannot find table or constraint: the referenced table '{fk.ReferencedTable}' does not exist."); @@ -508,11 +487,11 @@ private RelationshipSpec ResolvePrimaryKeyReference(RelationshipSpec fk, string? private (int Page, int ReferencedOrdinal, int NextIndexNumber) ResolveParent(RelationshipSpec fk, int childPage) { - TableDef parent = ReferencedTableOf(fk); + TableDefinition parent = ReferencedTableOf(fk); if (parent.DefinitionPage == childPage) throw new InvalidOperationException($"Self-referencing foreign key '{fk.Name}' should have been handled inline."); - var ptdef = new Pages.TableDefinitionPage(); + var ptdef = new TableDefinition(); ptdef.Read(_channel, parent.DefinitionPage); var refColumns = fk.Columns.Select(c => c.ReferencedColumn).ToList(); IndexDef refIndex = FindParentKeyIndex(ptdef.Indexes, refColumns, fk.ReferencedTable); @@ -527,7 +506,7 @@ private void EnsureRelationshipNameFree(string name) throw RelationshipNameTaken(name); } - private static SchemaObjectExistsException RelationshipNameTaken(string name) => + internal static SchemaObjectExistsException RelationshipNameTaken(string name) => new($"There is already a relationship named '{name}' in the current database.", name); /// @@ -538,10 +517,10 @@ private static SchemaObjectExistsException RelationshipNameTaken(string name) => /// private void AddRelationshipRows(string childTable, RelationshipSpec fk) { - TableDef msys = _catalog.RequireTable("MSysRelationships"); - new ViewCreator(_channel, _catalog).CreateRelationshipObject(fk.Name); + TableDefinition msys = _catalog.RequireTable("MSysRelationships"); + _catalog.CreateRelationshipObject(fk.Name); - int grbit = 0; + var grbit = RelationshipFlags.None; if (!fk.IsEnforced) grbit |= RelationshipFlags.DontEnforce; if (fk.CascadeUpdate) grbit |= RelationshipFlags.UpdateCascade; if (fk.CascadeDelete) grbit |= RelationshipFlags.DeleteCascade; @@ -551,14 +530,14 @@ private void AddRelationshipRows(string childTable, RelationshipSpec fk) { var (column, referencedColumn) = fk.Columns[i]; var values = new object?[msys.Columns.Count]; - CatalogWriter.Set(msys, values, "szRelationship", fk.Name); - CatalogWriter.Set(msys, values, "szObject", childTable); - CatalogWriter.Set(msys, values, "szColumn", column); - CatalogWriter.Set(msys, values, "szReferencedObject", fk.ReferencedTable); - CatalogWriter.Set(msys, values, "szReferencedColumn", referencedColumn); - CatalogWriter.Set(msys, values, "ccolumn", fk.Columns.Count); - CatalogWriter.Set(msys, values, "icolumn", i); - CatalogWriter.Set(msys, values, "grbit", grbit); + JetCatalog.Set(msys, values, "szRelationship", fk.Name); + JetCatalog.Set(msys, values, "szObject", childTable); + JetCatalog.Set(msys, values, "szColumn", column); + JetCatalog.Set(msys, values, "szReferencedObject", fk.ReferencedTable); + JetCatalog.Set(msys, values, "szReferencedColumn", referencedColumn); + JetCatalog.Set(msys, values, "ccolumn", fk.Columns.Count); + JetCatalog.Set(msys, values, "icolumn", i); + JetCatalog.Set(msys, values, "grbit", (int)grbit); new RowInserter(_channel, msys).Insert(values, updateIndexes: true); } } @@ -573,9 +552,16 @@ private void AddRelationshipRows(string childTable, RelationshipSpec fk) public void AddIndex(string tableName, string indexName, IReadOnlyList<(string Column, bool Descending)> columns, bool isUnique, bool isPrimary, bool disallowNull, bool ignoreNulls) { - JetName.Validate(indexName, "index name"); - TableDef table = _catalog.FindTable(tableName) + TableDefinition table = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' was not found."); + AddIndex(table, indexName, columns, isUnique, isPrimary, disallowNull, ignoreNulls); + } + + /// Adds an index using an already resolved definition, including during catalog creation. + internal void AddIndex(TableDefinition table, string indexName, IReadOnlyList<(string Column, bool Descending)> columns, + bool isUnique, bool isPrimary, bool disallowNull, bool ignoreNulls) + { + JetName.Validate(indexName, "index name"); RejectCalculatedIndexColumns(indexName, columns.Select(c => c.Column), n => table.Columns.FirstOrDefault( c => c.IsCalculated && string.Equals(c.Name, n, StringComparison.OrdinalIgnoreCase))?.Name); @@ -586,7 +572,7 @@ public void AddIndex(string tableName, string indexName, IReadOnlyList<(string C throw new InvalidOperationException($"Primary key already exists on table '{table.Name}'."); InsertIndex(table, indexName, slots, unique: isUnique || isPrimary, required: isPrimary || disallowNull, ignoreNulls, - (num, ord) => BuildPlainInfoBlock(num, ord, isPrimary)); + (num, ord) => TableDefinition.LogicalIndexSpec.Plain(num, ord, isPrimary, indexName)); } /// Refuses an index over a calculated column, on whichever route asked for it. @@ -621,7 +607,7 @@ private static void RejectOleIndexColumns(IEnumerable columnNames, Func< /// Resolves index column names to (columnId, ascending) slots against a table. private static List<(int Id, bool Ascending)> ResolveSlots( - TableDef table, IEnumerable<(string Column, bool Ascending)> columns) + TableDefinition table, IEnumerable<(string Column, bool Ascending)> columns) { var byName = table.Columns.ToDictionary(c => c.Name, c => c.ColumnId, StringComparer.OrdinalIgnoreCase); return columns.Select(c => byName.TryGetValue(c.Column, out int id) ? (Id: id, c.Ascending) @@ -629,10 +615,10 @@ private static void RejectOleIndexColumns(IEnumerable columnNames, Func< } /// Surgically inserts one data index and its logical info block into an existing table's TDEF, - /// name-sorted. gets (block number, data-block ordinal) and - /// returns the 28-byte info block — a plain index or an outgoing-FK block. Returns the new block number. - private int InsertIndex(TableDef table, string indexName, List<(int Id, bool Ascending)> slots, - bool unique, bool required, bool ignoreNulls, Func buildInfo) + /// name-sorted. gets (block number, data-block ordinal) and returns the info + /// block's fields — a plain index or an outgoing-FK block. Returns the new block number. + private int InsertIndex(TableDefinition table, string indexName, List<(int Id, bool Ascending)> slots, + bool unique, bool required, bool ignoreNulls, Func buildInfo) { JetFormatBase format = _channel.Format; @@ -644,20 +630,19 @@ private int InsertIndex(TableDef table, string indexName, List<(int Id, bool Asc throw new InvalidOperationException( $"Table '{table.Name}' already has an index named '{indexName}'."); - // The index-data block holds exactly IndexBlockFormat.MaxColumns slots, with no count and no - // continuation, so a wider index cannot be represented. TdefBuilder rejects this when a table is - // created with its indexes; this is the incremental path, where BuildIndexDataBlock would otherwise - // write the first ten and mark the rest unused — silently storing a different index from the one - // asked for, which ACE reads without complaint. ACE refuses instead: "Cannot have more than 10 - // fields in an index." - if (slots.Count > IndexBlockFormat.MaxColumns) + // The index-data block holds exactly IndexDataMaxColumns slots, with no count and no continuation, so + // a wider index cannot be represented. TableDefinition rejects this when a table is created with its + // indexes; this is the incremental path, where BuildDataBlock would otherwise write the first ten and + // mark the rest unused — silently storing a different index from the one asked for, which ACE reads + // without complaint. ACE refuses instead: "Cannot have more than 10 fields in an index." + if (slots.Count > format.IndexDataMaxColumns) throw new NotSupportedException( $"Cannot create index '{indexName}' on '{table.Name}' over {slots.Count} columns: " - + $"an index holds at most {IndexBlockFormat.MaxColumns} fields."); + + $"an index holds at most {format.IndexDataMaxColumns} fields."); - TdefParts parts = ParseTdef(table.DefinitionPage); // stitches continuation pages for a multi-page TDEF - var header = new LibRed.IO.PageBuffer(parts.Header, table.DefinitionPage); - int existingRowCount = header.ReadInt32(format.TdefRowCountOffset); + TableDefinition.Parts parts = TableDefinition.ReadParts(_channel, table.DefinitionPage); // stitches continuation pages for a multi-page TDEF + var header = new PageBuffer(parts.Header, table.DefinitionPage); + int existingRowCount = TableDefinition.ReadRowCount(parts.Header, format); // A unique index over rows that already exist has to be rejected if those rows aren't unique, and a // required one if any row leaves a key column NULL — ACE refuses the DDL for both. Done *here*, before a @@ -677,9 +662,8 @@ private int InsertIndex(TableDef table, string indexName, List<(int Id, bool Asc int newNum = NextLogicalIndexNumber(table.DefinitionPage); // Allocate the new index's root (empty leaf) and its usage-map row (appended after existing rows). - int rootPage = _allocator.Allocate(); - WriteEmptyLeafIndexPage(format, rootPage, owner: table.DefinitionPage); - int usageMapPage = ReadInt24(header, format.TdefOwnedPagesOffset + 1); + int rootPage = AllocateIndexRoot(format, table.DefinitionPage); + int usageMapPage = header.ReadRecordPointer(format.TdefOwnedPagesOffset).Page; // Where the new index's usage map goes: appended after the last row, as ACE does. A dropped index // leaves its row behind and ACE never hands it out again, so the row cannot be derived from the // table's shape — after a DROP INDEX that names a row already taken, on a table without long-value @@ -688,46 +672,42 @@ private int InsertIndex(TableDef table, string indexName, List<(int Id, bool Asc // When the primary page is full ACE does not squeeze the row in but spills, exactly as it does for // long-value columns: measured on a 40-memo table, CREATE INDEX leaves the full 57-row primary page // alone and puts the new index's map at row 0 of a page of its own (WideMemoUsageMapAccessTests). - var primaryMap = new DataPage(); - primaryMap.Read(_channel.ReadPage(usageMapPage), format); - int primaryFree = BinaryPrimitives.ReadUInt16LittleEndian( - _channel.ReadPage(usageMapPage).Span.Slice(format.DataFreeSpaceOffset, 2)); - - int newIndexUsageRow = primaryMap.Rows.Count; - bool ownPage = primaryFree < UsageMapRecordLength + 2; - if (ownPage) + int newIndexUsageRow; + if (UsageMap.RecordsFitting(format, DataPage.ReadFreeSpace(_channel.ReadPage(usageMapPage).Span, format)) < 1) { + // A page of its own, its one row written empty. usageMapPage = _allocator.Allocate(); - WriteUsageMaps(format, usageMapPage, mapCount: 1); + _channel.WritePage(usageMapPage, UsageMap.NewMapPage(format, 1)); newIndexUsageRow = 0; } - // Append the new index's (empty) usage-map row, preserving every existing record. The data maps are - // empty on an empty table but the *existing indexes'* maps already carry their root bits (set at - // creation), so we must not rewrite the page from scratch even when the table has no rows. A page of - // its own already has the row, written empty above. - if (!ownPage) AppendEmptyUsageMapRow(format, usageMapPage, newIndexUsageRow); + else + { + // Appended, preserving every existing record. The data maps are empty on an empty table but the + // *existing indexes'* maps already carry their root bits (set at creation), so the page must not be + // rewritten from scratch even when the table has no rows. + newIndexUsageRow = AppendEmptyUsageMapRow(format, usageMapPage); + } // Record this index's own root, as Access does at CREATE INDEX (the empty root is the index's sole - // page until it splits, after which IndexWriter adds each page it allocates). - new UsageMapWriter(_channel).SetBit(newIndexUsageRow, usageMapPage, rootPage, set: true); + // page until it splits, after which IndexTree adds each page it allocates). + new UsageMap(_channel).SetBit(newIndexUsageRow, usageMapPage, rootPage, set: true); // The new index adds a (zero) stats block and its data block after the existing ones, and a logical // block in name order. ACE's order ignores case (verified: a3 goes before IX2, and an FK named fk before // IX2); how it orders punctuation and accented letters is not measured. parts.Stats.Add(new byte[format.RealIndexEntrySize]); - // An index over a complex column carries the complex-column flag — every one Access writes does. bool complexColumn = slots.Any(s => table.Columns.Any(c => c.ColumnId == s.Id && c.Type == JetDataType.Complex)); - parts.DataBlocks.Add(BuildIndexDataBlock( - slots, rootPage, newIndexUsageRow, usageMapPage, unique, required, ignoreNulls, complexColumn)); + parts.DataBlocks.Add(TableDefinition.BuildDataBlock(format, slots, rootPage, newIndexUsageRow, usageMapPage, + unique, ignoreNulls, required, complexColumn)); int k = parts.Logical.Count(b => string.Compare(NameOf(b.Name), indexName, StringComparison.OrdinalIgnoreCase) < 0); - parts.Logical.Insert(k, (buildInfo(newNum, dataCount), EncodeName(indexName))); + parts.Logical.Insert(k, (TableDefinition.BuildInfoBlock(format, buildInfo(newNum, dataCount)), TableDefinition.NameEntry(indexName, format))); WriteTdef(table.DefinitionPage, parts); _catalog.Invalidate(); // Back-fill the new (empty) index B-tree with an entry per existing row, so the index is complete. if (existingRowCount != 0) - BackfillIndex(table.Name, indexName, ignoreNulls, validateUnique: false); + BackfillIndex(table, indexName, ignoreNulls, validateUnique: false); return newNum; } @@ -745,17 +725,17 @@ private int InsertIndex(TableDef table, string indexName, List<(int Id, bool Asc /// Comparison is on the encoded key, not the raw values, which is deliberate: the encoding is what the /// B-tree stores and it is collation-lossy, so "ABC" and "abc" share a key. That is precisely Access's /// uniqueness domain, and it keeps this agreeing with the insert- and update-time checks, which compare - /// the same way via . + /// the same way via . /// private void EnsureExistingRowsFitIndex( - TableDef table, string indexName, IReadOnlyList<(int Id, bool Ascending)> slots, bool unique, bool required) + TableDefinition table, string indexName, IReadOnlyList<(int Id, bool Ascending)> slots, bool unique, bool required) { var keyColumns = slots .Select(s => (Column: table.Columns.First(c => c.ColumnId == s.Id), s.Ascending)) .ToArray(); var seen = new HashSet(); - var rows = new Table(_channel, table); + var rows = new Table(_channel, table, _catalog); foreach (object?[] values in rows.Rows(rows.DecodeOnly(keyColumns.Select(k => k.Column.Index)))) { if (keyColumns.Any(k => values[k.Column.Index] is null)) @@ -765,100 +745,81 @@ private void EnsureExistingRowsFitIndex( $"Index or primary key cannot contain a Null value: a row of '{table.Name}' has no value for index '{indexName}'."); continue; } - if (unique && !seen.Add(Convert.ToHexString(IndexKeyEncoder.Encode(keyColumns, values)))) + if (unique && !seen.Add(Convert.ToHexString(IndexKeyCodec.Encode(keyColumns, values)))) throw new InvalidOperationException( $"Cannot create unique index '{indexName}' on '{table.Name}': duplicate key values exist."); } } /// Populates a freshly added index over a table's existing rows: encodes every live row's key, - /// then hands the lot to , which sorts them and writes each B-tree page + /// then hands the lot to , which sorts them and writes each B-tree page /// once. Rows with a null in an IGNORE NULL index's key are skipped, matching the per-insert path. /// /// This used to insert the rows one at a time, which re-read, re-parsed and rebuilt a whole leaf page per /// row: an index over 10,000 rows cost ~440 ms and allocated over a gigabyte, nearly all of it leaves /// replaced by the next entry. Sorting first lets each leaf be filled and written once. /// Uniqueness moves with it: on sorted keys a duplicate is an adjacent pair, so the per-row - /// descent is gone. The comparison is still on the encoded key, which + /// descent is gone. The comparison is still on the encoded key, which /// is Access's uniqueness domain (see ). /// A built index's statistics are set as ACE sets them (verified, for CREATE INDEX, a foreign key's /// backing index and an index an ALTER COLUMN rebuilds): the total entry count to the entries it now holds and the /// unique entry count to its distinct keys — both from the rows present, not from any earlier history. /// - private void BackfillIndex(string tableName, string indexName, bool ignoreNulls, bool validateUnique) + private void BackfillIndex(TableDefinition definition, string indexName, bool ignoreNulls, bool validateUnique) { - TableDef table = _catalog.FindTable(tableName) - ?? throw new InvalidOperationException($"Table '{tableName}' was not found after adding the index."); + // The catalog's first index cannot resolve its own table until it has been back-filled. + TableDefinition table = _catalog.ReadTableDefinition(definition.DefinitionPage, definition.Name, definition.IsSystem); IndexDef index = table.Indexes.First(ix => string.Equals(ix.Name, indexName, StringComparison.OrdinalIgnoreCase)); List<(byte[] Key, int Pointer, bool NullKey)> entries = IndexEntries(table, index, ignoreNulls); - new IndexWriter(_channel, table).BulkBuild(index, entries, validateUnique && index.IsUnique); + new IndexTree(_channel, table).BulkBuild(index, entries, validateUnique && index.IsUnique); SetBuiltStatistics(table, index, entries); } /// The entries an index over the table's current rows holds: every live row's encoded key and row /// pointer, less the rows an IGNORE NULL index leaves out. - private List<(byte[] Key, int Pointer, bool NullKey)> IndexEntries(TableDef table, IndexDef index, bool ignoreNulls) + private List<(byte[] Key, int Pointer, bool NullKey)> IndexEntries(TableDefinition table, IndexDef index, bool ignoreNulls) { var keyColumnIds = index.Columns.Select(c => c.Column.Index).ToArray(); var entries = new List<(byte[] Key, int Pointer, bool NullKey)>(); // The key is all an entry is made from, so it is all that is decoded: a wide row, or one whose memo the // index cannot even hold, would otherwise be read whole for every entry. - var rows = new Table(_channel, table); + var rows = new Table(_channel, table, _catalog); foreach ((RowId id, object?[] values) in rows.Rows(rows.DecodeOnly(keyColumnIds)).WithIds()) { bool hasNullKey = keyColumnIds.Any(i => values[i] is null); if (ignoreNulls && hasNullKey) continue; - entries.Add((IndexKeyEncoder.Encode(index.Columns, values), (id.Page << 8) | id.Row, hasNullKey)); + entries.Add((IndexKeyCodec.Encode(index.Columns, values), id.Packed, hasNullKey)); } return entries; } /// Sets a just-built index's statistics block as ACE sets it: total = the entries it holds, unique = /// its distinct keys among them. - private void SetBuiltStatistics(TableDef table, IndexDef index, List<(byte[] Key, int Pointer, bool NullKey)> entries) => - WriteIndexStatistics(table, index, entries.Count, entries.Select(e => Convert.ToHexString(e.Key)).Distinct().Count()); - - private void WriteIndexStatistics(TableDef table, IndexDef index, int total, int unique) + private void SetBuiltStatistics(TableDefinition table, IndexDef index, List<(byte[] Key, int Pointer, bool NullKey)> entries) { byte[] tdef = _channel.ReadPage(table.DefinitionPage).Span.ToArray(); - int at = _channel.Format.TdefRealIndexBlockOffset + index.RealIndexOrdinal * _channel.Format.RealIndexEntrySize; - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(at, 4), total); - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(at + 4, 4), unique); + TableDefinition.WriteIndexCounts(tdef, _channel.Format, index.RealIndexOrdinal, + entries.Count, entries.Select(e => Convert.ToHexString(e.Key)).Distinct().Count()); _channel.WritePage(table.DefinitionPage, tdef); } - /// Appends one empty inline usage-map record (row ) to an existing - /// usage-map page, preserving every existing record. The new index tracks no pages here (IndexWriter - /// navigates the B-tree structurally), so an empty bitmap is correct. - private void AppendEmptyUsageMapRow(JetFormatBase format, int pageNumber, int newRow) + /// Appends one empty inline usage-map record to an existing usage-map page, preserving every existing + /// record, and returns its row. The new map tracks no pages yet, so an empty bitmap is correct. + private int AppendEmptyUsageMapRow(JetFormatBase format, int pageNumber) { - const int MapLength = 1 + 4 + 64; // inline type + start page + 64-byte bitmap (matches WriteUsageMaps) byte[] page = _channel.ReadPage(pageNumber).Span.ToArray(); - int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2)); - - if (rowCount != newRow) - throw new InvalidOperationException( - $"Usage-map page has {rowCount} rows; expected {newRow} before appending the new index's map."); - - int minOffset = format.PageSize; - for (int r = 0; r < rowCount; r++) - minOffset = Math.Min(minOffset, BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + r * 2, 2))); - int newOffset = minOffset - MapLength; // A record written below the directory would overwrite the slots themselves — which reads back later // as a slot with offset 0 "outside the row heap", a long way from the cause. The caller is expected // to spill onto a fresh page rather than get here; this is the backstop that keeps a mistake loud. - if (newOffset < format.DataRowDirectoryOffset + (rowCount + 1) * 2) + // An all-zero record is an empty inline map: inline type, start page 0, zero bitmap. + if (!DataPage.TryAppendRow(page, format, new byte[format.UsageMapInlineRecordSize], RowSlotFlags.None, out int row)) throw new InvalidOperationException( - $"Usage-map page {pageNumber} has no room for another record: {rowCount} rows already reach " - + $"offset {minOffset}. The new map belongs on a page of its own."); - Array.Clear(page, newOffset, MapLength); // inline type 0x00, start page 0, zero bitmap - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + newRow * 2, 2), (ushort)newOffset); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2), (ushort)(rowCount + 1)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), - (ushort)(newOffset - format.DataRowDirectoryOffset - (rowCount + 1) * 2)); + $"Usage-map page {pageNumber} has no room for another record: {DataPage.ReadRowCount(page, format)} rows " + + $"already reach offset {DataPage.LowestRowOffset(page, format)}. The new map belongs on a page of its own."); _channel.WritePage(pageNumber, page); + return row; } /// @@ -872,7 +833,7 @@ public void AddForeignKey(string childTable, RelationshipSpec fk) { JetName.Validate(fk.Name, "foreign key name"); EnsureRelationshipNameFree(fk.Name); - TableDef child = _catalog.FindTable(childTable) + TableDefinition child = _catalog.FindTable(childTable) ?? throw new InvalidOperationException($"Table '{childTable}' was not found."); if (fk.NoIndex) throw new NotSupportedException("ALTER TABLE ADD FOREIGN KEY … NO INDEX is not supported yet."); @@ -895,9 +856,7 @@ public void AddForeignKey(string childTable, RelationshipSpec fk) return; } - byte upd = fk.CascadeUpdate ? IndexBlockFormat.CascadeAction : IndexBlockFormat.NoCascadeAction; - byte del = fk.CascadeDelete ? IndexBlockFormat.CascadeAction - : fk.DeleteSetNull ? IndexBlockFormat.SetNullAction : IndexBlockFormat.NoCascadeAction; + (RelationshipAction upd, RelationshipAction del) = Actions(fk); var slots = ResolveSlots(child, fk.Columns.Select(c => (c.Column, Ascending: true))); // A self-reference (child == parent) hosts both ends in the same TDEF: the outgoing block takes the @@ -910,7 +869,8 @@ public void AddForeignKey(string childTable, RelationshipSpec fk) : ResolveParent(fk, child.DefinitionPage); int childBlockNum = InsertIndex(child, fk.Name, slots, unique: false, required: false, ignoreNulls: false, - (num, ord) => BuildOutgoingInfoBlock(num, ord, FkTypeOutgoing, parentNum, parentPage, upd, del)); + (num, ord) => new TableDefinition.LogicalIndexSpec(num, ord, ForeignKeyType.Outgoing, (uint)parentNum, + parentPage, upd, del, IndexInfoType.Foreign, fk.Name)); AddIncomingRelationshipBlock(new IncomingRelationship( parentPage, parentNum, refOrdinal, (uint)childBlockNum, child.DefinitionPage, upd, del)); @@ -933,51 +893,44 @@ public bool DropConstraint(string childTable, string name) string.Equals(r.Table, childTable, StringComparison.OrdinalIgnoreCase)); if (rel is null) return false; - TableDef child = _catalog.FindTable(childTable) + TableDefinition child = _catalog.FindTable(childTable) ?? throw new InvalidOperationException($"Table '{childTable}' was not found."); IndexDef? fkIndex = child.Indexes.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); bool selfRef = string.Equals(rel.ReferencedTable, childTable, StringComparison.OrdinalIgnoreCase); if (fkIndex is not null) { - TdefParts childParts = ParseTdef(child.DefinitionPage); + TableDefinition.Parts childParts = TableDefinition.ReadParts(_channel, child.DefinitionPage); // The FK's backing index is an index like any other: its B-tree is released whole and its usage-map - // record retired, read here while its data block is still in place (see DropIndex). - var owned = new HashSet { fkIndex.RootPage }; + // record retired. + var owned = new HashSet(); var retire = new List(); - int at = IndexBlockFormat.UsageMapRowOffset; - byte[] dataBlock = childParts.DataBlocks[fkIndex.RealIndexOrdinal]; - (int Row, int Page) map = (dataBlock[at], dataBlock[at + 1] | dataBlock[at + 2] << 8 | dataBlock[at + 3] << 16); var maps = new UsageMap(_channel, child); - if (map.Page != 0) - { - List pages = maps.PagesInMap(map.Row, map.Page).ToList(); - owned.UnionWith(pages); - retire.Add((map, [map], pages)); - } + QueueIndexMap(fkIndex, maps, owned, retire); - int outgoing = childParts.Logical.FindIndex(b => NameOf(b.Name).Equals(name, StringComparison.OrdinalIgnoreCase)); - int childBlockNum = BinaryPrimitives.ReadInt32LittleEndian(childParts.Logical[outgoing].Info.AsSpan(0x04, 4)); + (byte[] Info, byte[] Name) outgoing = + childParts.Logical.First(b => NameOf(b.Name).Equals(name, StringComparison.OrdinalIgnoreCase)); + int childBlockNum = TableDefinition.ReadInfoBlock(outgoing.Info, _channel.Format, name).Number; // Remove the FK index (data ordinal) + its outgoing info block from the child, plus — for a // self-reference — the incoming block, which also lives here. - RemoveTdefBlocks(childParts, removeDataOrdinal: fkIndex.RealIndexOrdinal, removeLogical: b => + TableDefinition.RemoveIndexBlocks(_channel.Format, childParts, removeDataOrdinal: fkIndex.RealIndexOrdinal, removeLogical: b => NameOf(b.Name).Equals(name, StringComparison.OrdinalIgnoreCase) || - (selfRef && IsIncomingBlockFor(b.Info, childBlockNum, child.DefinitionPage))); + (selfRef && IsIncomingBlockFor(b, childBlockNum, child.DefinitionPage))); WriteTdef(child.DefinitionPage, childParts); if (!selfRef) { - TableDef parent = _catalog.FindTable(rel.ReferencedTable) + TableDefinition parent = _catalog.FindTable(rel.ReferencedTable) ?? throw new InvalidOperationException($"Table '{rel.ReferencedTable}' was not found."); - TdefParts parentParts = ParseTdef(parent.DefinitionPage); - RemoveTdefBlocks(parentParts, removeDataOrdinal: null, removeLogical: b => - IsIncomingBlockFor(b.Info, childBlockNum, child.DefinitionPage)); + TableDefinition.Parts parentParts = TableDefinition.ReadParts(_channel, parent.DefinitionPage); + TableDefinition.RemoveIndexBlocks(_channel.Format, parentParts, removeDataOrdinal: null, removeLogical: b => + IsIncomingBlockFor(b, childBlockNum, child.DefinitionPage)); WriteTdef(parent.DefinitionPage, parentParts); } RetireMapRecords(retire, maps, owned); - var allocator = new PageAllocator(_channel); + var allocator = _channel.Allocator; foreach (int page in owned) allocator.Release(page); } @@ -985,7 +938,7 @@ public bool DropConstraint(string childTable, string name) DeleteRelationshipRows(name); // Its MSysObjects object and permission rows go with it, as ACE removes them (verified). A relationship // written before LibRed recorded the object has none. - if (FindObjectId(name, CatalogFormat.ObjectTypeRelationship) is { } objectId) + if (_catalog.FindObjectId(name, ObjectType.Relationship) is { } objectId) { DeleteCatalogRows("MSysObjects", "Id", objectId); DeleteCatalogRows("MSysACEs", "ObjectId", objectId); @@ -1016,7 +969,7 @@ public bool DropConstraint(string childTable, string name) /// . private bool DropTable(string tableName, bool keepPermissions) { - TableDef? table = _catalog.FindTable(tableName); + TableDefinition? table = _catalog.FindTable(tableName); if (table is null) return false; // Remove the relationships this table owns as the child (referencing) side. Materialize first — @@ -1052,7 +1005,7 @@ private bool DropTable(string tableName, bool keepPermissions) ?? throw new InvalidOperationException($"Table '{tableName}' was not found."); int tdefPage = table.DefinitionPage; - var allocator = new PageAllocator(_channel); + var allocator = _channel.Allocator; var maps = new UsageMap(_channel, table); // Collect before freeing anything: the pointers are read out of the TDEF, which this method frees. @@ -1060,7 +1013,7 @@ private bool DropTable(string tableName, bool keepPermissions) // Through the chain reader: a wide table's definition spans continuation pages, and parsing only the // first one throws on the declared length. (The map POINTERS below are at fixed offsets inside the // first page, so those are read from it directly, as UsageMap does.) - var definition = new TableDefinitionPage(); + var definition = new TableDefinition(); definition.Read(_channel, tdefPage); // The map RECORDS live as rows on owner-zero data pages, and each is retired in turn — its bits cleared @@ -1075,33 +1028,20 @@ private bool DropTable(string tableName, bool keepPermissions) foreach (ColumnDef column in table.Columns) QueueLongValueMaps(definition, column, maps, owned, retire); - // Each real index keeps its B-tree pages in its own owned map, whose (row, page) pointer sits in its data - // block. Freeing only the root strands every other page of a multi-level index, and leaving the map's - // record live keeps its holder from ever being freed. - foreach (byte[] block in ParseTdef(tdefPage).DataBlocks) - { - int at = IndexBlockFormat.UsageMapRowOffset; - (int Row, int Page) map = (block[at], block[at + 1] | block[at + 2] << 8 | block[at + 3] << 16); - if (map.Page == 0) continue; - List pages = maps.PagesInMap(map.Row, map.Page).ToList(); - owned.UnionWith(pages); - retire.Add((map, [map], pages)); - } - foreach (IndexDef index in table.RealIndexes) - owned.Add(index.RootPage); + // Then each real index's whole B-tree and its own map, in index order. + foreach (IndexDef index in table.Indexes) + QueueIndexMap(index, maps, owned, retire); List dataPages = maps.DataPages().ToList(); owned.UnionWith(dataPages); owned.Add(tdefPage); // A wide table's definition continues on further pages. ACE frees them too, and leaves every byte of // them alone — only the first page is marked released. - owned.UnionWith(TdefChainReader.Read(_channel, tdefPage).ContinuationPages); + owned.UnionWith(TableDefinition.ReadChain(_channel, tdefPage).ContinuationPages); PageBuffer tdef = _channel.ReadPage(tdefPage); - (int Row, int Page) dataOwned = (tdef.ReadByte(_channel.Format.TdefOwnedPagesOffset), - tdef.ReadInt24(_channel.Format.TdefOwnedPagesOffset + 1)); + (int Row, int Page) dataOwned = tdef.ReadRecordPointer(_channel.Format.TdefOwnedPagesOffset); retire.Add((dataOwned, [dataOwned], dataPages)); - retire.Add(((tdef.ReadByte(_channel.Format.TdefFreePagesOffset), - tdef.ReadInt24(_channel.Format.TdefFreePagesOffset + 1)), [], [])); + retire.Add((tdef.ReadRecordPointer(_channel.Format.TdefFreePagesOffset), [], [])); RetireMapRecords(retire, maps, owned); @@ -1110,7 +1050,7 @@ private bool DropTable(string tableName, bool keepPermissions) // Measured across an ACE DROP TABLE: exactly one byte of the 4,096 differs, the type's low byte. Only // the TDEF is marked — the data, long-value and map-holder pages ACE frees keep their 0x0101. byte[] released = _channel.ReadPage(tdefPage).Span.ToArray(); - PageHeader.WriteType(released, PageType.ReleasedTableDefinition); + TableDefinition.MarkReleased(released); _channel.WritePage(tdefPage, released); foreach (int page in owned) @@ -1132,7 +1072,7 @@ private bool DropTable(string tableName, bool keepPermissions) /// a column with no long-value maps. /// private static void QueueLongValueMaps( - TableDefinitionPage definition, ColumnDef column, UsageMap maps, HashSet owned, List retire) + TableDefinition definition, ColumnDef column, UsageMap maps, HashSet owned, List retire) { definition.LongValueOwnedMaps.TryGetValue(column.ColumnId, out (int Row, int Page) map); definition.LongValueFreeMaps.TryGetValue(column.ColumnId, out (int Row, int Page) columnFree); @@ -1150,6 +1090,24 @@ private static void QueueLongValueMaps( if (columnFree.Page != 0) retire.Add((columnFree, [], [])); } + /// + /// Queues what dropping releases — the whole B-tree, not just its root — and its own + /// usage map's record for . Every page of a split index but the root is recorded + /// only in that map, so releasing the root alone strands them, owned by nothing and never handed out again; and + /// a map record left live keeps its holder from ever being freed. ACE frees the lot and takes the record off its + /// holder as well (measured by whole-file diff: the bits cleared, the row tombstoned, the holder's free space + /// back). Read from the index as the catalog has it, so before its data block goes. A zero root is a foreign + /// key's index whose root is not allocated yet (see ), and has none to free. + /// + private static void QueueIndexMap(IndexDef index, UsageMap maps, HashSet owned, List retire) + { + if (index.RootPage > 0) owned.Add(index.RootPage); + if (index.UsageMap.Page == 0) return; + List pages = maps.PagesInMap(index.UsageMap.Row, index.UsageMap.Page).ToList(); + owned.UnionWith(pages); + retire.Add((index.UsageMap, [index.UsageMap], pages)); + } + /// /// Takes usage-map records off their pages the way ACE does, in the order given — tombstone the slot, slide the /// rows below it up, return the bytes to the page's free count — rather than leaving dead maps behind. On a @@ -1159,7 +1117,7 @@ private static void QueueLongValueMaps( /// private void RetireMapRecords(IEnumerable retire, UsageMap maps, HashSet owned) { - var usageMaps = new UsageMapWriter(_channel); + var usageMaps = new UsageMap(_channel); var holders = new List(); var retired = new HashSet<(int Row, int Page)>(); foreach (((int Row, int Page) map, IReadOnlyList<(int Row, int Page)> clear, IReadOnlyList pages) in retire) @@ -1169,18 +1127,11 @@ private void RetireMapRecords(IEnumerable retire, UsageMap maps, foreach (int page in pages) usageMaps.SetBit(cleared.Row, cleared.Page, page, set: false); - // A reference-form record keeps its bitmap on dedicated pages. ACE zeroes each one's bitmap — even - // for the table's own owned map, whose bits an inline record keeps — leaves its header, and frees it. - foreach (int bitmapPage in maps.BitmapPagesOf(map.Row, map.Page)) - { - byte[] bitmap = _channel.ReadPage(bitmapPage).Span.ToArray(); - bitmap.AsSpan(4).Clear(); - _channel.WritePage(bitmapPage, bitmap); - owned.Add(bitmapPage); - } + // A reference-form record keeps its bitmap on dedicated pages, which go with it, zeroed. + owned.UnionWith(usageMaps.ClearBitmapPages(maps.ReadRecordAt(map.Row, map.Page))); byte[] holderBytes = _channel.ReadPage(map.Page).Span.ToArray(); - RowInserter.ReclaimRow(_channel.Format, holderBytes, map.Row); + DataPage.ReclaimRow(holderBytes, _channel.Format, map.Row); _channel.WritePage(map.Page, holderBytes); if (!holders.Contains(map.Page)) holders.Add(map.Page); } @@ -1203,14 +1154,14 @@ private void RetireMapRecords(IEnumerable retire, UsageMap maps, /// /// Drops a view or stored procedure — DROP VIEW name / DROP PROCEDURE name. Both are a /// type-5 MSysObjects object; ACE's two statements are interchangeable (verified: DROP VIEW works - /// on a procedure and vice versa), so this handles either. The inverse of ViewCreator: deletes the + /// on a procedure and vice versa), so this handles either. The inverse of JetCatalog: deletes the /// object's MSysObjects row, its MSysQueries rows, and its two MSysACEs permission rows (index entries /// removed, not just soft-deleted). No pages to free (a query owns none — its MSysQueries rows live on the /// shared MSysQueries pages). Returns false if no such query object exists. /// public bool DropQueryObject(string name) { - if (FindObjectId(name, StoredQueryFormat.ObjectTypeQuery) is not { } objId) return false; + if (_catalog.FindObjectId(name, ObjectType.Query) is not { } objId) return false; DeleteCatalogRows("MSysObjects", "Id", objId); DeleteCatalogRows("MSysQueries", "ObjectId", objId); @@ -1219,24 +1170,6 @@ public bool DropQueryObject(string name) return true; } - /// The MSysObjects id of the object of named , - /// or null when there is none. - private int? FindObjectId(string name, short type) - { - TableDef mo = _catalog.RequireTable("MSysObjects"); - int idIdx = mo.RequireColumn("Id").Index; - int nameIdx = mo.RequireColumn("Name").Index; - int typeIdx = mo.RequireColumn("Type").Index; - - var objects = new Table(_channel, mo); - foreach (object?[] values in objects.Rows(objects.DecodeOnly([idIdx, nameIdx, typeIdx]))) - if (string.Equals(values[nameIdx] as string, name, StringComparison.OrdinalIgnoreCase) - && Convert.ToInt16(values[typeIdx] ?? (short)0, CultureInfo.InvariantCulture) == type) - return Convert.ToInt32(values[idIdx], CultureInfo.InvariantCulture); - return null; - } - - /// /// Renames a table — ALTER TABLE … RENAME TO. Measured against ACE (see RenameFanOutProbeTest): /// only two things move — the object's MSysObjects.Name, and the by-name table references in @@ -1251,7 +1184,7 @@ public bool DropQueryObject(string name) /// public bool RenameTable(string oldName, string newName) { - TableDef? table = _catalog.FindTable(oldName); + TableDefinition? table = _catalog.FindTable(oldName); if (table is null) return false; // The same names Create refuses. A rename reaches the identical bytes by a different route, so // validating only on the way in left it open: renaming a COLUMN to over 64 characters makes the @@ -1266,36 +1199,36 @@ public bool RenameTable(string oldName, string newName) $"ALTER TABLE '{oldName}' RENAME TO '{newName}': a table or query named '{newName}' already exists.", newName); - RenameCatalogObject(table.DefinitionPage, newName); - RepointRelationshipTables(oldName, newName); + // The object's MSysObjects row — its Id is the table's TDEF page. + UpdateCatalogRows("MSysObjects", "Id", table.DefinitionPage, required: true, ("Name", newName)); + RepointRelationships(columnOf: null, oldName, newName); return true; } - /// Sets the Name of the MSysObjects row whose Id is this table's TDEF page. - private void RenameCatalogObject(int tdefPage, string newName) => - UpdateCatalogRows("MSysObjects", "Id", tdefPage, required: true, ("Name", newName)); - - /// Repoints every relationship that names on either side. Both the - /// child (szObject) and parent (szReferencedObject) are stored by name, and a self-reference - /// names the table twice — hence updating both columns in one pass over each row. - private void RepointRelationshipTables(string oldName, string newName) + /// Repoints every relationship that names a renamed table — or, with , a + /// renamed column of that table — on whichever side names it. Both sides are stored by name: the child + /// (szObject, szColumn) and the parent (szReferencedObject, szReferencedColumn). + /// A self-reference names the table twice, hence both sides in one pass over each row; and a column name is + /// only unique within its table, so a side is matched on its table as well. + private void RepointRelationships(string? columnOf, string oldName, string newName) { - TableDef? def = _catalog.FindTable("MSysRelationships"); + TableDefinition? def = _catalog.FindTable("MSysRelationships"); if (def is null) return; // a database with no relationships has no catalog table to fix up - int childIndex = def.RequireColumn("szObject").Index; - int parentIndex = def.RequireColumn("szReferencedObject").Index; - var table = new Table(_channel, def); + (int Table, int Column)[] sides = + [ + (def.RequireColumn("szObject").Index, def.RequireColumn("szColumn").Index), + (def.RequireColumn("szReferencedObject").Index, def.RequireColumn("szReferencedColumn").Index), + ]; + bool Names(object?[] v, (int Table, int Column) side) => columnOf is null + ? NameMatches(v[side.Table], oldName) + : NameMatches(v[side.Table], columnOf) && NameMatches(v[side.Column], oldName); + var table = new Table(_channel, def, _catalog); - foreach ((RowId id, object?[] values) in table.RowsWhere([childIndex, parentIndex], - v => NameMatches(v[childIndex], oldName) || NameMatches(v[parentIndex], oldName)).ToList()) - { - var updates = new List<(int Column, object? Value)>(2); - if (NameMatches(values[childIndex], oldName)) updates.Add((childIndex, newName)); - if (NameMatches(values[parentIndex], oldName)) updates.Add((parentIndex, newName)); - if (updates.Count > 0) - SetCatalogValues(table, def, id, values, updates.ToArray()); - } + foreach ((RowId id, object?[] values) in table.RowsWhere([.. sides.SelectMany(s => new[] { s.Table, s.Column })], + v => sides.Any(s => Names(v, s))).ToList()) + SetCatalogValues(table, def, id, values, [.. sides.Where(s => Names(values, s)) + .Select(s => (columnOf is null ? s.Table : s.Column, (object?)newName))]); } /// @@ -1308,7 +1241,7 @@ private void RepointRelationshipTables(string oldName, string newName) /// public bool RenameColumn(string tableName, string oldName, string newName) { - TableDef table = _catalog.FindTable(tableName) + TableDefinition table = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' was not found."); ColumnDef? col = table.Columns.FirstOrDefault(c => string.Equals(c.Name, oldName, StringComparison.OrdinalIgnoreCase)); if (col is null) return false; @@ -1323,50 +1256,17 @@ public bool RenameColumn(string tableName, string oldName, string newName) // the catalog still resolves the old name. int? complexId = _catalog.FindComplexColumn(tableName, oldName)?.ComplexId; - TdefParts parts = ParseTdef(table.DefinitionPage); // stitches continuation pages for a multi-page TDEF - RenameColumnInParts(parts, table.Columns.Count, col.Index, newName, _channel.Format); + TableDefinition.Parts parts = TableDefinition.ReadParts(_channel, table.DefinitionPage); // stitches continuation pages for a multi-page TDEF + // Only the name entry changes; the descriptor stays as it is, and WriteTdef re-lays the names after it. + parts.Columns[col.Index] = (parts.Columns[col.Index].Descriptor, TableDefinition.NameEntry(newName, _channel.Format)); WriteTdef(table.DefinitionPage, parts); RenameColumnProperties(table.DefinitionPage, oldName, newName); - RepointRelationshipColumns(tableName, oldName, newName); + RepointRelationships(columnOf: tableName, oldName, newName); if (complexId is int id) SetComplexColumnName(id, newName); _catalog.Invalidate(); return true; } - /// Replaces the name entry of the column at in the column region. - /// The descriptors are fixed-size and untouched; only the variable-length name pool is rebuilt (a - /// different-length name shifts every following entry), and the column count is unchanged. - private static void RenameColumnInParts( - TdefParts parts, int colCount, int renameIndex, string newName, JetFormatBase format) - { - int descSize = format.ColumnDescriptorSize; - ReadOnlySpan cols = parts.Columns; - - var descriptors = new List(colCount); - for (int i = 0; i < colCount; i++) - descriptors.Add(cols.Slice(i * descSize, descSize).ToArray()); - - int np = colCount * descSize; - var names = new List(colCount); - for (int i = 0; i < colCount; i++) - { - int len = BinaryPrimitives.ReadUInt16LittleEndian(cols.Slice(np, 2)); - names.Add(cols.Slice(np, 2 + len).ToArray()); - np += 2 + len; - } - - byte[] nameBytes = System.Text.Encoding.Unicode.GetBytes(newName); - byte[] entry = new byte[2 + nameBytes.Length]; - BinaryPrimitives.WriteUInt16LittleEndian(entry.AsSpan(0, 2), (ushort)nameBytes.Length); - nameBytes.CopyTo(entry, 2); - names[renameIndex] = entry; - - var blob = new List(parts.Columns.Length); - foreach (byte[] d in descriptors) blob.AddRange(d); - foreach (byte[] n in names) blob.AddRange(n); - parts.Columns = [.. blob]; - } - /// Re-owns the renamed column's extended-property block in its table's MSysObjects.LvProp /// blob, so its DefaultValue/Required/validation survive the rename (ACE does this — verified). No-op when /// the column had no properties. @@ -1391,40 +1291,32 @@ private void RenameColumnProperties(int tdefPage, string oldName, string newName .Select(r => r.Values[lvProp.Index] as byte[]).FirstOrDefault(); } - /// The object's MSysObjects.Flags, or null when it has no row. - private int? ReadObjectFlags(int objectId) - { - (TableDef msys, Table table, int idIdx, _) = ObjectProperties(); - int flags = msys.FindColumn("Flags")!.Index; - return RowsKeyed(table, idIdx, objectId).Select(r => r.Values[flags] as int?).FirstOrDefault(); - } - /// Replaces an object's extended-property blob with . Not an /// call, because LvProp is a long value: the blob has to be stored first /// — inline or on a page — and the row given the descriptor for it. private void WriteObjectProperties(int objectId, byte[] properties) { - (TableDef msys, Table table, int idIdx, ColumnDef lvProp) = ObjectProperties(); + (TableDefinition msys, Table table, int idIdx, ColumnDef lvProp) = ObjectProperties(); foreach ((RowId id, object?[] values) in RowsKeyed(table, idIdx, objectId)) { - byte[] descriptor = new RowInserter(_channel, msys).StorePackedLongValue(lvProp.ColumnId, properties); - values[lvProp.Index] = new LongValueDescriptor(descriptor); + values[lvProp.Index] = new RowInserter(_channel, msys).StorePackedLongValue(lvProp.ColumnId, properties); table.Update(id, values, new HashSet { lvProp.Index }); return; } + throw new InvalidOperationException($"MSysObjects row for object {objectId} was not found."); } /// The LvProp property holding an object's copy of its Name AutoCorrect map. private const string NameMapProperty = "NameMap"; /// Every MSysNameMap row, its map decoded; empty when the file has no such table. - public IReadOnlyList ReadNameMapRows() + public IReadOnlyList ReadNameMapRows() { if (_catalog.FindTable("MSysNameMap") is not { } def) return []; int guid = def.RequireColumn("GUID").Index, id = def.RequireColumn("Id").Index, name = def.RequireColumn("Name").Index, type = def.RequireColumn("Type").Index, map = def.RequireColumn("NameMap").Index; - return [.. new Table(_channel, def).Rows().Select(r => new NameMapRow( + return [.. new Table(_channel, def, _catalog).Rows().Select(r => new NameMap.CatalogRow( (Guid)r[guid]!, (int)r[id]!, ((string)r[name]!).TrimEnd('\0'), (int)r[type]!, r[map] is byte[] blob ? NameMap.ReadRow(blob) : null))]; } @@ -1437,13 +1329,12 @@ public bool WriteNameMapRow(Guid objectGuid, NameMap map, string? name) if (_catalog.FindTable("MSysNameMap") is not { } def) return false; int guidIndex = def.RequireColumn("GUID").Index, nameIndex = def.RequireColumn("Name").Index; ColumnDef mapColumn = def.RequireColumn("NameMap"); - var table = new Table(_channel, def); + var table = new Table(_channel, def, _catalog); foreach ((RowId id, object?[] values) in table.RowsWhere([guidIndex], v => v[guidIndex] is Guid g && g == objectGuid).ToList()) { // A long value, like LvProp: stored first, the row given the descriptor (see WriteObjectProperties). - byte[] descriptor = new RowInserter(_channel, def).StorePackedLongValue(mapColumn.ColumnId, map.WriteRow()); - values[mapColumn.Index] = new LongValueDescriptor(descriptor); + values[mapColumn.Index] = new RowInserter(_channel, def).StorePackedLongValue(mapColumn.ColumnId, map.WriteRow()); var changed = new HashSet { mapColumn.Index }; if (name is not null) { @@ -1459,7 +1350,7 @@ public bool WriteNameMapRow(Guid objectGuid, NameMap map, string? name) /// The NameMap property of the object named of /// MSysObjects.Type ; null when the object has none. /// There is no such object. - public NameMap? ReadNameMapProperty(string objectName, short objectType) + public NameMap? ReadNameMapProperty(string objectName, ObjectType objectType) { byte[]? blob = ReadObjectProperties(RequireObjectId(objectName, objectType)); return blob is { Length: > 0 } @@ -1472,7 +1363,7 @@ public bool WriteNameMapRow(Guid objectGuid, NameMap map, string? name) /// existing entry keeps its place and flag byte; a new one takes the flag 0x00 Access gives it. Every /// other property is left as it was. /// There is no such object. - public void WriteNameMapProperty(string objectName, short objectType, NameMap? map) + public void WriteNameMapProperty(string objectName, ObjectType objectType, NameMap? map) { int objectId = RequireObjectId(objectName, objectType); byte[] blob = ReadObjectProperties(objectId) ?? []; @@ -1493,31 +1384,23 @@ public void WriteNameMapProperty(string objectName, short objectType, NameMap? m } /// The MSysObjects.Id of the object with this name and type. - private int RequireObjectId(string name, short type) - { - TableDef msys = _catalog.RequireTable("MSysObjects"); - int idIndex = msys.RequireColumn("Id").Index, nameIndex = msys.RequireColumn("Name").Index, - typeIndex = msys.RequireColumn("Type").Index; - var objects = new Table(_channel, msys); - foreach (object?[] row in objects.Rows(objects.DecodeOnly([idIndex, nameIndex, typeIndex]))) - if (row[typeIndex] is short t && t == type && NameMatches(row[nameIndex], name)) - return (int)row[idIndex]!; - throw new InvalidOperationException($"There is no object '{name}' of type {type}."); - } + private int RequireObjectId(string name, ObjectType type) => + _catalog.FindObjectId(name, type) + ?? throw new InvalidOperationException($"There is no object '{name}' of type {type}."); /// MSysObjects and the two columns every extended-property path works through: the /// Id it matches an object by, and the LvProp holding the blob. - private (TableDef Definition, Table Table, int IdIndex, ColumnDef LvProp) ObjectProperties() + private (TableDefinition Definition, Table Table, int IdIndex, ColumnDef LvProp) ObjectProperties() { - TableDef msys = _catalog.RequireTable("MSysObjects"); - return (msys, new Table(_channel, msys), msys.RequireColumn("Id").Index, msys.RequireColumn("LvProp")); + TableDefinition msys = _catalog.RequireTable("MSysObjects"); + return (msys, new Table(_channel, msys, _catalog), msys.RequireColumn("Id").Index, msys.RequireColumn("LvProp")); } /// The calculated columns of whose expression reads /// . A malformed expression counts as reading nothing rather than throwing: /// refusing to drop is a safeguard, and it must not turn into a refusal to drop anything at all because /// some other column's expression cannot be parsed. - private static List CalculatedColumnsReading(TableDef table, ColumnDef column) + private static List CalculatedColumnsReading(TableDefinition table, ColumnDef column) { var dependents = new List(); foreach (ColumnDef candidate in table.Columns) @@ -1528,7 +1411,7 @@ private static List CalculatedColumnsReading(TableDef table, ColumnDef c if (CalculatedValue.ReferencedIndexes(candidate, table.Columns).Contains(column.Index)) dependents.Add(candidate.Name); } - catch (Calculated.CalculatedExpressionException) { /* unparseable: reads nothing we can prove */ } + catch (Storage.Calculated.CalculatedExpressionException) { /* unparseable: reads nothing we can prove */ } } return dependents; } @@ -1545,7 +1428,7 @@ private static byte[] RewriteCalculatedReferences(byte[] blob, string oldName, s for (int i = 0; i < properties.Count; i++) { if (properties[i].Name != PropertyBlob.ExpressionProperty) continue; - string rewritten = Calculated.CalculatedExpression.RenameColumnReference( + string rewritten = Storage.Calculated.CalculatedExpression.RenameColumnReference( properties[i].Value, oldName, newName); if (rewritten == properties[i].Value) continue; // Drop RawValue so the new text is encoded rather than the original bytes replayed. @@ -1557,59 +1440,20 @@ private static byte[] RewriteCalculatedReferences(byte[] blob, string oldName, s : blob; } - /// Repoints every relationship that names this column, on whichever side owns it. Unlike a table - /// name, a column name is only unique within its table, so each side is matched on its table name too. - private void RepointRelationshipColumns(string tableName, string oldName, string newName) - { - TableDef? def = _catalog.FindTable("MSysRelationships"); - if (def is null) return; - - int childTable = def.RequireColumn("szObject").Index; - int childColumn = def.RequireColumn("szColumn").Index; - int parentTable = def.RequireColumn("szReferencedObject").Index; - int parentColumn = def.RequireColumn("szReferencedColumn").Index; - var table = new Table(_channel, def); - - foreach ((RowId id, object?[] values) in table.RowsWhere([childTable, childColumn, parentTable, parentColumn], - v => (NameMatches(v[childTable], tableName) && NameMatches(v[childColumn], oldName)) - || (NameMatches(v[parentTable], tableName) && NameMatches(v[parentColumn], oldName))).ToList()) - { - var updates = new List<(int Column, object? Value)>(2); - if (NameMatches(values[childTable], tableName) && NameMatches(values[childColumn], oldName)) - updates.Add((childColumn, newName)); - if (NameMatches(values[parentTable], tableName) && NameMatches(values[parentColumn], oldName)) - updates.Add((parentColumn, newName)); - if (updates.Count > 0) - SetCatalogValues(table, def, id, values, updates.ToArray()); - } - } - /// /// Whether a table, saved query or linked table already uses this name — the objects of the Tables /// container, whose names MSysObjects' unique (ParentId, Name) index keeps distinct. That is ACE's rule /// for a new or renamed table (measured, and RenameFanOutProbeTest for renames): a form, report, macro, - /// module, relationship or database document of the same name does not collide. Scanned straight from - /// MSysObjects rather than the catalog's reconstructed tables and Views, which omit linked tables and - /// queries LibRed can't rebuild. + /// module, relationship or database document of the same name does not collide. Found by the catalog's + /// (ParentId, Name) lookup on the MSysObjects row itself, so a linked table or a query LibRed can't + /// rebuild still counts. /// private bool ObjectNameExists(string name, int exceptObjectId) { - TableDef mo = _catalog.RequireTable("MSysObjects"); - int idIndex = mo.RequireColumn("Id").Index; - int nameIndex = mo.RequireColumn("Name").Index; - int parentIndex = mo.RequireColumn("ParentId").Index; - - var objects = new Table(_channel, mo); - foreach (object?[] values in objects.Rows(objects.DecodeOnly([idIndex, nameIndex, parentIndex]))) - { - if (!NameMatches(values[nameIndex], name)) continue; - // Skip the object being renamed — it can't collide with itself (same-name and case-only renames). - if (values[idIndex] is not null - && Convert.ToInt32(values[idIndex], CultureInfo.InvariantCulture) == exceptObjectId) continue; - if (values[parentIndex] is int parent && parent == CatalogFormat.ObjectContainerParentId) return true; - } - - return false; + int idIndex = _catalog.RequireTable("MSysObjects").RequireColumn("Id").Index; + // Skip the object being renamed — it can't collide with itself (same-name and case-only renames). + return _catalog.FindObjectRow(JetCatalog.ObjectContainerParentId, name) is { } row + && (row[idIndex] is null || Convert.ToInt32(row[idIndex], CultureInfo.InvariantCulture) != exceptObjectId); } private static bool NameMatches(object? value, string name) => @@ -1619,7 +1463,7 @@ private static bool NameMatches(object? value, string name) => /// step — MSysObjects is uniquely indexed on (ParentId, Name), so a rename has to move that entry rather /// than just overwrite the value. private static void SetCatalogValues( - Table table, TableDef def, RowId id, object?[] values, params (int Column, object? Value)[] updates) + Table table, TableDefinition def, RowId id, object?[] values, params (int Column, object? Value)[] updates) { var newValues = (object?[])values.Clone(); var changed = new HashSet(); @@ -1633,14 +1477,9 @@ private static void SetCatalogValues( if (index.Columns.Any(c => changed.Contains(c.Column.Index))) table.MoveIndexEntry(index, values, newValues, id); - table.Update(id, newValues, changed); + new RowInserter(table.Channel, def).Update(id, newValues, changed); } - /// Deletes every row of whose equals - /// (the object id) — used to remove a dropped table's MSysObjects and MSysACEs - /// rows. A full delete: its index entries are removed (not just the slot soft-deleted) so, e.g., the - /// MSysObjects ParentIdName unique index doesn't retain a stale entry that would then reject - /// re-creating a same-named table. /// Sets on every row of whose /// equals — the update counterpart of /// , and the one way this class changes a catalog row: through @@ -1651,10 +1490,10 @@ private static void SetCatalogValues( /// Whether a missing catalog table is an error. False for the ones a database need /// not have at all (a Jet 4 file has no MSysComplexColumns). /// The column/value pairs to set on each matching row. - private void UpdateCatalogRows(string catalogTable, string keyColumn, int keyValue, + internal void UpdateCatalogRows(string catalogTable, string keyColumn, int keyValue, bool required, params (string Column, object? Value)[] updates) { - TableDef? t = _catalog.FindTable(catalogTable); + TableDefinition? t = _catalog.FindTable(catalogTable); if (t is null) { if (!required) return; @@ -1663,7 +1502,7 @@ private void UpdateCatalogRows(string catalogTable, string keyColumn, int keyVal int key = t.RequireColumn(keyColumn).Index; var columns = updates.Select(u => (Column: t.RequireColumn(u.Column).Index, u.Value)).ToArray(); - var table = new Table(_channel, t); + var table = new Table(_channel, t, _catalog); foreach ((RowId id, object?[] values) in RowsKeyed(table, key, keyValue)) SetCatalogValues(table, t, id, values, [.. columns]); @@ -1675,17 +1514,22 @@ private void UpdateCatalogRows(string catalogTable, string keyColumn, int keyVal [.. table.RowsWhere([keyColumn], values => values[keyColumn] is not null && Convert.ToInt32(values[keyColumn], CultureInfo.InvariantCulture) == keyValue)]; + /// Deletes every row of whose equals + /// (the object id) — used to remove a dropped table's MSysObjects and MSysACEs + /// rows. A full delete: its index entries are removed (not just the slot soft-deleted) so, e.g., the + /// MSysObjects ParentIdName unique index doesn't retain a stale entry that would then reject + /// re-creating a same-named table. private void DeleteCatalogRows(string catalogTable, string keyColumn, int keyValue) { - TableDef t = _catalog.RequireTable(catalogTable); + TableDefinition t = _catalog.RequireTable(catalogTable); int idx = t.RequireColumn(keyColumn).Index; - var table = new Table(_channel, t); + var table = new Table(_channel, t, _catalog); foreach ((RowId id, object?[] values) in RowsKeyed(table, idx, keyValue)) { foreach (IndexDef index in t.RealIndexes) table.RemoveIndexEntry(index, values, id); - table.Delete(id); + new RowInserter(table.Channel, t).Delete(id); } } @@ -1697,8 +1541,8 @@ private void SetComplexColumnName(int complexId, string columnName) => /// /// Drops a secondary/unique/primary index — DROP INDEX index ON table. Byte-faithful with ACE - /// (probed): remove the index's 12-byte stats block, 52-byte index-data block, and its 28-byte logical - /// info block + name from the TDEF (decrementing counts and the data-ordinal ref of any block past it), + /// (probed): remove the index's statistics entry, index-data block, and its logical info block + name from + /// the TDEF (decrementing counts and the data-ordinal ref of any block past it), /// and free its B-tree root page back to the global free map — the same index-removal path as DROP /// CONSTRAINT, minus the relationship linkage. A secondary index lives only in the TDEF (no MSys row). /// Returns false if no such index exists. Throws if the index backs a relationship (ACE rejects that — @@ -1706,7 +1550,7 @@ private void SetComplexColumnName(int complexId, string columnName) => /// public bool DropIndex(string tableName, string indexName) { - TableDef table = _catalog.FindTable(tableName) + TableDefinition table = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' was not found."); IndexDef? index = table.Indexes.FirstOrDefault(i => string.Equals(i.Name, indexName, StringComparison.OrdinalIgnoreCase)); if (index is null) return false; @@ -1715,32 +1559,19 @@ public bool DropIndex(string tableName, string indexName) throw new InvalidOperationException( $"Cannot drop index '{indexName}': it is used in a relationship — drop the relationship first."); - TdefParts parts = ParseTdef(table.DefinitionPage); // stitches continuation pages for a multi-page TDEF + TableDefinition.Parts parts = TableDefinition.ReadParts(_channel, table.DefinitionPage); // stitches continuation pages for a multi-page TDEF - // The whole B-tree, not just its root. Every other page of a split index is recorded only in the index's - // own usage map, so releasing the root alone strands them — owned by nothing and never handed out again. - // Read before the blocks go: the map pointer lives in the data block this is about to remove. ACE frees - // the lot and takes the map's record off its holder page as well (measured by whole-file diff: the bits - // cleared, the row tombstoned, the holder's free space back), which is what RetireMapRecords does. - var owned = new HashSet { index.RootPage }; + var owned = new HashSet(); var retire = new List(); - int at = IndexBlockFormat.UsageMapRowOffset; - byte[] dataBlock = parts.DataBlocks[index.RealIndexOrdinal]; - (int Row, int Page) map = (dataBlock[at], dataBlock[at + 1] | dataBlock[at + 2] << 8 | dataBlock[at + 3] << 16); var maps = new UsageMap(_channel, table); - if (map.Page != 0) - { - List pages = maps.PagesInMap(map.Row, map.Page).ToList(); - owned.UnionWith(pages); - retire.Add((map, [map], pages)); - } + QueueIndexMap(index, maps, owned, retire); - RemoveTdefBlocks(parts, removeDataOrdinal: index.RealIndexOrdinal, + TableDefinition.RemoveIndexBlocks(_channel.Format, parts, removeDataOrdinal: index.RealIndexOrdinal, removeLogical: b => NameOf(b.Name).Equals(indexName, StringComparison.OrdinalIgnoreCase)); WriteTdef(table.DefinitionPage, parts); RetireMapRecords(retire, maps, owned); - var allocator = new PageAllocator(_channel); + var allocator = _channel.Allocator; foreach (int page in owned) allocator.Release(page); _catalog.Invalidate(); @@ -1757,7 +1588,7 @@ public bool DropIndex(string tableName, string indexName) /// Two indexes are protected: on the child, the FK's backing index — named after the relationship, /// as both ACE and create it; on the parent, the referenced key — the /// unique/primary index over the referenced columns. - private bool IndexParticipatesInRelationship(TableDef table, IndexDef index) + private bool IndexParticipatesInRelationship(TableDefinition table, IndexDef index) { var cols = index.Columns.Select(c => c.Column.Name).ToList(); bool SameCols(IEnumerable other) => @@ -1777,7 +1608,7 @@ bool SameCols(IEnumerable other) => /// /// Adds a column — ALTER TABLE t ADD COLUMN c type. A metadata TDEF edit (probed vs ACE, the - /// inverse of DROP COLUMN): appends the column's 25-byte descriptor + name, gives it the next column id + /// inverse of DROP COLUMN): appends the column's descriptor + name, gives it the next column id /// from the 0x29 max-columns high-water (which keeps counting even past dropped ids), appends its /// fixed offset (end of the fixed region) or variable index (current variable count), and bumps /// ColumnCount (0x2D), the 0x29 high-water, and — for a variable column — VariableColumnCount (0x2B). @@ -1790,41 +1621,35 @@ bool SameCols(IEnumerable other) => public bool AddColumn(string tableName, ColumnSpec spec, string? defaultValue = null) { JetName.Validate(spec.Name, "column name"); - TableDef table = _catalog.FindTable(tableName) + TableDefinition table = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' was not found."); if (table.Columns.Any(c => string.Equals(c.Name, spec.Name, StringComparison.OrdinalIgnoreCase))) return false; - if (table.Columns.Count >= MaxColumnsPerTable) - throw new NotSupportedException($"Table '{tableName}' already has {MaxColumnsPerTable} columns, the most a table can have."); JetFormatBase format = _channel.Format; + if (table.Columns.Count >= format.MaxColumnsPerTable) + throw new NotSupportedException($"Table '{tableName}' already has {format.MaxColumnsPerTable} columns, the most a table can have."); // As on the create path, a calculated column with a Memo RESULT needs the long-value maps even though // its declared type is Text — the value reaches a page through a descriptor either way (§3.4a). bool isLongValue = spec.Type is JetDataType.Memo or JetDataType.Ole || spec.CalculatedResultType is JetDataType.Memo; - TdefParts parts = ParseTdef(table.DefinitionPage); // stitches continuation pages for a multi-page TDEF - - int maxCols = BinaryPrimitives.ReadUInt16LittleEndian(parts.Header.AsSpan(format.TdefMaxColumnsOffset, 2)); - int varCount = BinaryPrimitives.ReadUInt16LittleEndian(parts.Header.AsSpan(format.TdefVariableColumnsOffset, 2)); - int colCount = BinaryPrimitives.ReadUInt16LittleEndian(parts.Header.AsSpan(format.TdefColumnCountOffset, 2)); - - // The 0x29 column-id high-water never decrements on DROP COLUMN, so once 255 ids have been handed out - // no further column can be added — even if the *live* count is lower (the guard above) — until the - // database is compacted (which renumbers and reclaims dropped ids). ACE enforces exactly this: create - // 255 columns, drop some, ADD COLUMN → "Too many fields defined." Mirror it rather than write a 256th - // id ACE can't represent. Verified vs ACE. - if (maxCols >= MaxColumnsPerTable) - throw new NotSupportedException( - $"Cannot add column '{spec.Name}' to '{tableName}': too many fields defined — {MaxColumnsPerTable} column ids " + - "have been used over this table's lifetime, and dropped ids are only reclaimed by compacting the database."); + TableDefinition.Parts parts = TableDefinition.ReadParts(_channel, table.DefinitionPage); // stitches continuation pages for a multi-page TDEF + int colCount = parts.Columns.Count; - // The width limits Create enforces through TdefBuilder apply just as much to a column added later: + // Even when the *live* count is under the cap (the guard above), the id space may not be. This only bumps + // the high-waters in `parts`, so a refusal below still writes nothing. + (int columnId, int varCount) = TableDefinition.TakeColumnId(parts, spec.IsFixedLength, + $"Cannot add column '{spec.Name}' to '{tableName}'", format); + + // The width limits Create enforces through TableDefinition apply just as much to a column added later: // the new column's own width, and what it does to the widest record the table can now hold. - RecordLayout.ValidateFieldWidth(spec.Name, spec.Type, spec.Length); + int fixedEnd = FixedBytes(table); + RowCodec.ValidateFieldWidth(spec.Name, spec.Type, spec.Length); JetDataTypeVersions.EnsureStorable(spec.Type, _channel.Format.Version, spec.Name); - RecordLayout.ValidateRecordFits(tableName, - FixedBytes(table) + (spec.IsFixedLength && spec.Type != JetDataType.Boolean ? spec.Length : 0), + TableDefinition.ValidateNumericPrecision(spec); + RowCodec.ValidateRecordFits(tableName, + fixedEnd + (spec.IsFixedLength && spec.Type != JetDataType.Boolean ? spec.Length : 0), varCount + (spec.IsFixedLength ? 0 : 1), - maxCols + 1, + columnId + 1, format); // Same single-counter rule the CREATE path and the promote path enforce; ADD COLUMN had neither. The @@ -1841,28 +1666,25 @@ public bool AddColumn(string tableName, ColumnSpec spec, string? defaultValue = Name = spec.Name, Type = spec.Type, Index = colCount, - ColumnId = maxCols, // next id from the high-water (dropped ids are never reused) + ColumnId = columnId, // next id from the high-water (dropped ids are never reused) Length = spec.Length, - // Boolean is fixed but occupies no data — the bit IS the value — so it must not advance the fixed - // offset. Every other computation of this quantity excludes it; this one did not, putting an added - // column one byte past where ACE puts it on a table whose only fixed columns are Booleans. - FixedOffset = spec.IsFixedLength - ? table.Columns.Where(c => c.IsFixedLength && c.Type != JetDataType.Boolean) - .Select(c => c.FixedOffset + c.Length).DefaultIfEmpty(0).Max() - : 0, + FixedOffset = spec.IsFixedLength ? fixedEnd : 0, VariableIndex = spec.IsFixedLength ? -1 : varCount, // Descriptor 0x07. A VARIABLE column carries its own variable index, which is the 0x2B high-water // (measured vs ACE in VariableColumnHighWaterAccessTests — NOT the count of live variable columns, - // which is lower once one has been dropped), so leave it unset and let TdefBuilder use VariableIndex. + // which is lower once one has been dropped), so leave it unset and let TableDefinition use VariableIndex. // A FIXED column carries the same high-water: every variable column ever given a slot has a smaller // id than the one being added, the dropped ones included (verified vs ACE: a LONG added after a // TEXT was dropped takes 2 where two TEXT columns had been, one of them gone). VariableTableIndex = spec.IsFixedLength ? varCount : -1, IsFixedLength = spec.IsFixedLength, IsAutoNumber = spec.IsAutoNumber, - Precision = spec.Precision, + // As CREATE TABLE stamps it: a declared 0 is ACE's 18, because a 0 on disk leaves a column ACE cannot read. + Precision = TableDefinition.EffectivePrecision(spec), Scale = spec.Scale, IsNullable = spec.IsNullable, + // WITH COMPRESSION: the capable flag ACE sets for a column declared so, on the SQL route as on CREATE TABLE. + SupportsCompressedUnicode = spec.SupportsCompressedUnicode, Collation = spec.Type == JetDataType.FixedPoint ? Collation.GeneralLegacy : _collation, // Without this the descriptor gets no 0xC0 and the column reads back as an ordinary one: its // Expression and ResultType properties are written, nothing looks at them, and every row stores @@ -1882,22 +1704,17 @@ public bool AddColumn(string tableName, ColumnSpec spec, string? defaultValue = int rank = -1, previous = -1; for (int i = 0; i < colCount; i++) { - Span ordinal = parts.Columns.AsSpan(i * format.ColumnDescriptorSize + format.ColumnSecondaryNumberOffset, 2); + Span ordinal = parts.Columns[i].Descriptor.AsSpan(format.ColumnSecondaryNumberOffset, 2); int value = BinaryPrimitives.ReadUInt16LittleEndian(ordinal); if (i == 0 || value != previous) rank++; previous = value; BinaryPrimitives.WriteUInt16LittleEndian(ordinal, (ushort)rank); } - byte[] descriptor = TdefBuilder.BuildColumnDescriptor(newColumn, format); + byte[] descriptor = TableDefinition.BuildColumnDescriptor(newColumn, format); BinaryPrimitives.WriteUInt16LittleEndian(descriptor.AsSpan(format.ColumnSecondaryNumberOffset, 2), (ushort)(rank + 1)); - AppendColumnToParts(parts, colCount, descriptor, spec.Name, format); - - BinaryPrimitives.WriteUInt16LittleEndian(parts.Header.AsSpan(format.TdefColumnCountOffset, 2), (ushort)(colCount + 1)); - BinaryPrimitives.WriteUInt16LittleEndian(parts.Header.AsSpan(format.TdefMaxColumnsOffset, 2), (ushort)(maxCols + 1)); - if (!spec.IsFixedLength) - BinaryPrimitives.WriteUInt16LittleEndian(parts.Header.AsSpan(format.TdefVariableColumnsOffset, 2), (ushort)(varCount + 1)); + parts.Columns.Add((descriptor, TableDefinition.NameEntry(spec.Name, format))); - LongValueMapPlacement? lvMaps = isLongValue ? PlaceLongValueMaps(parts, maxCols) : null; + LongValueMapPlacement? lvMaps = isLongValue ? PlaceLongValueMaps(parts, columnId) : null; WriteTdef(table.DefinitionPage, parts); @@ -1924,22 +1741,21 @@ public bool AddColumn(string tableName, ColumnSpec spec, string? defaultValue = /// Gives a column that has become Memo/OLE its §3.3.2 usage-map entry in and says where /// its owned and free map records go — for to write once the TDEF is. ACE /// appends the two maps to the table's existing usage-map page right after the maps already there (verified, - /// for ADD COLUMN and for an ALTER COLUMN to Memo alike), and adds the 10-byte entry before the list's 0xFFFF - /// terminator. + /// for ADD COLUMN and for an ALTER COLUMN to Memo alike), and adds the column's long-value map entry before the + /// list's terminator. /// - private LongValueMapPlacement PlaceLongValueMaps(TdefParts parts, int columnId) + private LongValueMapPlacement PlaceLongValueMaps(TableDefinition.Parts parts, int columnId) { JetFormatBase format = _channel.Format; - int o = format.TdefOwnedPagesOffset + 1; - int primaryPage = parts.Header[o] | (parts.Header[o + 1] << 8) | (parts.Header[o + 2] << 16); + int primaryPage = PageBuffer.ReadRecordPointer(parts.Header, format.TdefOwnedPagesOffset).Page; byte[] primaryBytes = _channel.ReadPage(primaryPage).Span.ToArray(); - int primaryFree = BinaryPrimitives.ReadUInt16LittleEndian(primaryBytes.AsSpan(format.DataFreeSpaceOffset, 2)); + int primaryFree = DataPage.ReadFreeSpace(primaryBytes, format); LongValueMapPlacement placement; - if (primaryFree >= 2 * (UsageMapRecordLength + 2)) + if (UsageMap.RecordsFitting(format, primaryFree) >= 2) { // Room on the table's usage-map page — append the two maps there (as ACE does). - int used = BinaryPrimitives.ReadUInt16LittleEndian(primaryBytes.AsSpan(format.DataRowCountOffset, 2)); + int used = DataPage.ReadRowCount(primaryBytes, format); placement = new LongValueMapPlacement(primaryPage, used, used + 1, Dedicated: false); } else @@ -1948,20 +1764,26 @@ private LongValueMapPlacement PlaceLongValueMaps(TdefParts parts, int columnId) // fallback CREATE TABLE uses once its primary map page fills on a wide table. placement = new LongValueMapPlacement(_allocator.Allocate(), 0, 1, Dedicated: true); } - AddLongValueMapEntry(parts, columnId, placement.UsedRow, placement.FreeRow, placement.Page); + TableDefinition.AddLongValueMapEntry(_channel.Format, parts, columnId, placement.UsedRow, placement.FreeRow, placement.Page); return placement; } - /// Writes the two empty map records placed. + /// Writes the two empty map records placed. The TDEF already names + /// their rows, so appending them anywhere else is refused rather than left pointing at another map. private void WriteLongValueMaps(LongValueMapPlacement placement) { JetFormatBase format = _channel.Format; if (placement.Dedicated) - WriteUsageMaps(format, placement.Page, mapCount: 2); // owned = row 0, free = row 1, both empty - else { - AppendEmptyUsageMapRow(format, placement.Page, placement.UsedRow); - AppendEmptyUsageMapRow(format, placement.Page, placement.FreeRow); + _channel.WritePage(placement.Page, UsageMap.NewMapPage(format, 2)); // owned = row 0, free = row 1, both empty + return; + } + foreach (int planned in new[] { placement.UsedRow, placement.FreeRow }) + { + int row = AppendEmptyUsageMapRow(format, placement.Page); + if (row != planned) + throw new InvalidOperationException( + $"Usage-map page {placement.Page} took the long-value map at row {row}; the definition names row {planned}."); } } @@ -1978,13 +1800,13 @@ private void WriteLongValueMaps(LongValueMapPlacement placement) /// private void NumberExistingRows(string tableName, ColumnSpec spec) { - TableDef table = _catalog.FindTable(tableName) + TableDefinition table = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' was not found after adding column '{spec.Name}'."); ColumnDef column = table.FindColumn(spec.Name)!; - if (spec.Increment == 0) throw TdefBuilder.ZeroIncrement(spec.Name); + if (spec.Increment == 0) throw TableDefinition.ZeroIncrement(spec.Name); int increment = spec.Increment; - var rows = new Table(_channel, table).Rows().WithIds().ToList(); + var rows = new Table(_channel, table, _catalog).Rows().WithIds().ToList(); if (rows.Count > 0) { var writer = new RowInserter(_channel, table); @@ -2000,41 +1822,7 @@ private void NumberExistingRows(string tableName, ColumnSpec spec) ReseedCounter(table, column, spec.Seed == 1 && increment == 1 ? rows.Count + 1 : spec.Seed, increment); } - /// Inserts a long-value (memo/OLE) column's 10-byte §3.3.2 usage-map entry - /// ({col_num:2}{used row+page:4}{free row+page:4}) just before the list's 0xFFFF terminator. - /// The new column has the highest id, so appending keeps the list in ascending column order. - private static void AddLongValueMapEntry(TdefParts parts, int columnId, int usedRow, int freeRow, int mapPage) - { - byte[] lval = parts.Lval; - int at = lval.Length - 2; // before the terminator - - var entry = new byte[10]; - BinaryPrimitives.WriteUInt16LittleEndian(entry, (ushort)columnId); - entry[2] = (byte)usedRow; WriteInt24(entry, 3, mapPage); - entry[6] = (byte)freeRow; WriteInt24(entry, 7, mapPage); - - var result = new byte[lval.Length + 10]; - Array.Copy(lval, 0, result, 0, at); - entry.CopyTo(result, at); - Array.Copy(lval, at, result, at + 10, 2); // the 0xFFFF terminator - parts.Lval = result; - } - /// Removes a long-value column's 10-byte §3.3.2 usage-map entry from the list, keeping the other - /// entries and the 0xFFFF terminator. A no-op for a column without one. - private static void RemoveLongValueMapEntry(TdefParts parts, int columnId) - { - byte[] lval = parts.Lval; - for (int at = 0; at + 2 < lval.Length; at += 10) - { - if (BinaryPrimitives.ReadUInt16LittleEndian(lval.AsSpan(at, 2)) != columnId) continue; - var result = new byte[lval.Length - 10]; - Array.Copy(lval, 0, result, 0, at); - Array.Copy(lval, at + 10, result, at, lval.Length - at - 10); - parts.Lval = result; - return; - } - } /// Sets (replaces) a column's DefaultValue in the table's MSysObjects.LvProp blob — /// ALTER TABLE … ALTER COLUMN … DEFAULT. Reads all properties, drops any existing DefaultValue for the @@ -2070,24 +1858,14 @@ public void SetColumnRequired(string tableName, string columnName, bool required /// and rewrites it — the shared read-modify-write behind ALTER COLUMN … SET/DROP DEFAULT. private void MutateLvPropForColumn(string tableName, string columnName, Action> mutate) { - TableDef target = _catalog.FindTable(tableName) + TableDefinition target = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' does not exist."); int tdefPage = target.DefinitionPage; - (TableDef msys, Table table, int idIdx, ColumnDef lvProp) = ObjectProperties(); - - foreach ((RowId id, object?[] values) in RowsKeyed(table, idIdx, tdefPage)) - { - byte[] blob = values[lvProp.Index] as byte[] ?? []; - var props = PropertyBlob.Read(blob).ToList(); - mutate(props); - byte[] updated = PropertyBlob.Write(props, blob); - byte[] descriptor = new RowInserter(_channel, msys).StorePackedLongValue(lvProp.ColumnId, updated); - values[lvProp.Index] = new LongValueDescriptor(descriptor); - table.Update(id, values, new HashSet { lvProp.Index }); - return; - } - throw new InvalidOperationException($"MSysObjects row for table '{tableName}' (page {tdefPage}) was not found."); + byte[] blob = ReadObjectProperties(tdefPage) ?? []; + var props = PropertyBlob.Read(blob).ToList(); + mutate(props); + WriteObjectProperties(tdefPage, PropertyBlob.Write(props, blob)); } /// Appends a column's extended properties (DefaultValue/Required) to its table's @@ -2095,53 +1873,32 @@ private void MutateLvPropForColumn(string tableName, string columnName, Action. private void SetColumnProperties(int tdefPage, string columnName, IReadOnlyList props) { - (TableDef msys, Table table, int idIdx, ColumnDef lvProp) = ObjectProperties(); - - foreach ((RowId id, object?[] values) in RowsKeyed(table, idIdx, tdefPage)) - { - byte[] blob = values[lvProp.Index] as byte[] ?? []; - byte[] updated = PropertyBlob.AddColumnProperties(blob, columnName, props); - byte[] descriptor = new RowInserter(_channel, msys).StorePackedLongValue(lvProp.ColumnId, updated); - values[lvProp.Index] = new LongValueDescriptor(descriptor); - table.Update(id, values, new HashSet { lvProp.Index }); - return; - } + byte[] blob = ReadObjectProperties(tdefPage) ?? []; + WriteObjectProperties(tdefPage, PropertyBlob.AddColumnProperties(blob, columnName, props)); } /// Adds a table-level CHECK to the table's MSysObjects.LvProp blob — ALTER TABLE ADD /// CONSTRAINT … CHECK. Merges with any existing checks: reads the current CheckConstraints property, /// appends the new (name, expression), and rewrites the single empty-owner table block (RemoveOwner + re-add), /// keeping the name pool and every column block intact. The check is enforced by the engine from the - /// re-loaded TableDef.CheckConstraints. + /// re-loaded TableDefinition.CheckConstraints. public void AddCheckConstraint(string tableName, string checkName, string expression) { JetName.Validate(checkName, "check constraint name"); - TableDef target = _catalog.FindTable(tableName) + TableDefinition target = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' does not exist."); int tdefPage = target.DefinitionPage; - (TableDef msys, Table table, int idIdx, ColumnDef lvProp) = ObjectProperties(); + byte[] blob = ReadObjectProperties(tdefPage) ?? []; + var checks = PropertyBlob.ReadCheckConstraints(PropertyBlob.Read(blob)).ToList(); + checks.Add((checkName, expression)); - foreach ((RowId id, object?[] values) in RowsKeyed(table, idIdx, tdefPage)) - { - byte[] blob = values[lvProp.Index] as byte[] ?? []; - - var checks = PropertyBlob.ReadCheckConstraints(PropertyBlob.Read(blob)).ToList(); - checks.Add((checkName, expression)); - - // Replace only the CheckConstraints entry. Dropping the whole table-owned block and re-adding one - // property takes every OTHER table-level property with it — ValidationRule / ValidationText above - // all, which LibRed reads and reports but does not re-emit, so an Access-authored table validation - // rule vanished on the first CHECK anyone added. The column-property paths already do it this way. - byte[] updated = ReplaceTableProperty(blob, PropertyBlob.CheckConstraintsProperty, - checks.Count > 0 ? PropertyBlob.WriteCheckList(checks) : null); - - byte[] descriptor = new RowInserter(_channel, msys).StorePackedLongValue(lvProp.ColumnId, updated); - values[lvProp.Index] = new LongValueDescriptor(descriptor); - table.Update(id, values, new HashSet { lvProp.Index }); - return; - } - throw new InvalidOperationException($"MSysObjects row for table '{tableName}' (page {tdefPage}) was not found."); + // Replace only the CheckConstraints entry. Dropping the whole table-owned block and re-adding one + // property takes every OTHER table-level property with it — ValidationRule / ValidationText above + // all, which LibRed reads and reports but does not re-emit, so an Access-authored table validation + // rule vanished on the first CHECK anyone added. The column-property paths already do it this way. + WriteObjectProperties(tdefPage, ReplaceTableProperty(blob, PropertyBlob.CheckConstraintsProperty, + checks.Count > 0 ? PropertyBlob.WriteCheckList(checks) : null)); } /// Drops a named table-level CHECK — ALTER TABLE … DROP CONSTRAINT. Removes the matching entry from @@ -2151,31 +1908,20 @@ public void AddCheckConstraint(string tableName, string checkName, string expres /// false if no CHECK of that name exists (so the caller can try other constraint kinds). public bool DropCheckConstraint(string tableName, string checkName) { - TableDef target = _catalog.FindTable(tableName) + TableDefinition target = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' does not exist."); int tdefPage = target.DefinitionPage; - (TableDef msys, Table table, int idIdx, ColumnDef lvProp) = ObjectProperties(); - - foreach ((RowId id, object?[] values) in RowsKeyed(table, idIdx, tdefPage)) - { - byte[] blob = values[lvProp.Index] as byte[] ?? []; + byte[] blob = ReadObjectProperties(tdefPage) ?? []; + var checks = PropertyBlob.ReadCheckConstraints(PropertyBlob.Read(blob)).ToList(); + if (checks.RemoveAll(c => string.Equals(c.Name, checkName, StringComparison.OrdinalIgnoreCase)) == 0) + return false; // no CHECK of that name — let the caller try FK/PK/unique - var checks = PropertyBlob.ReadCheckConstraints(PropertyBlob.Read(blob)).ToList(); - if (checks.RemoveAll(c => string.Equals(c.Name, checkName, StringComparison.OrdinalIgnoreCase)) == 0) - return false; // no CHECK of that name — let the caller try FK/PK/unique - - // Rewrite the list, or remove the entry when that was the last check — leaving every other - // table-level property (ValidationRule, ValidationText, …) untouched. See AddCheckConstraint. - byte[] updated = ReplaceTableProperty(blob, PropertyBlob.CheckConstraintsProperty, - checks.Count > 0 ? PropertyBlob.WriteCheckList(checks) : null); - - byte[] descriptor = new RowInserter(_channel, msys).StorePackedLongValue(lvProp.ColumnId, updated); - values[lvProp.Index] = new LongValueDescriptor(descriptor); - table.Update(id, values, new HashSet { lvProp.Index }); - return true; - } - throw new InvalidOperationException($"MSysObjects row for table '{tableName}' (page {tdefPage}) was not found."); + // Rewrite the list, or remove the entry when that was the last check — leaving every other + // table-level property (ValidationRule, ValidationText, …) untouched. See AddCheckConstraint. + WriteObjectProperties(tdefPage, ReplaceTableProperty(blob, PropertyBlob.CheckConstraintsProperty, + checks.Count > 0 ? PropertyBlob.WriteCheckList(checks) : null)); + return true; } /// Changes a column's declared type — ALTER TABLE … ALTER COLUMN. Apart from the counter edits and @@ -2185,7 +1931,7 @@ public bool DropCheckConstraint(string tableName, string checkName) /// change. public void AlterColumn(string tableName, string columnName, ColumnSpec newSpec) { - TableDef table = _catalog.FindTable(tableName) + TableDefinition table = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' does not exist."); ColumnDef col = table.FindColumn(columnName) ?? throw new InvalidOperationException($"Column '{columnName}' does not exist in '{tableName}'."); @@ -2194,19 +1940,20 @@ public void AlterColumn(string tableName, string columnName, ColumnSpec newSpec) // Widening a column reaches the same bytes as declaring it wide in the first place, so the limits // Create enforces apply here too. This sits ahead of the identity and counter short-circuits below // deliberately: re-declaring an already-oversized column should report the problem, not wave it on. - RecordLayout.ValidateFieldWidth(newSpec.Name, newSpec.Type, newSpec.Length); + RowCodec.ValidateFieldWidth(newSpec.Name, newSpec.Type, newSpec.Length); JetDataTypeVersions.EnsureStorable(newSpec.Type, _channel.Format.Version, newSpec.Name); + TableDefinition.ValidateNumericPrecision(newSpec); // A pre-check, so an oversized re-declaration reports rather than being waved on by the short-circuits // below; AlterColumnTypeInPlace re-runs it against the measured fixed region, which is authoritative. // The variable-slot count comes from the stored 0x2B high-water — it never decrements, so deriving it // from the live columns under-counts on a table that has dropped or retyped one. - RecordLayout.ValidateRecordFits(tableName, + // A type or length change burns a fresh column id, so the bitmap can widen by one. + RowCodec.ValidateRecordFits(tableName, FixedBytes(table) - (col.IsFixedLength && col.Type != JetDataType.Boolean ? col.Length : 0) + (newSpec.IsFixedLength && newSpec.Type != JetDataType.Boolean ? newSpec.Length : 0), table.VariableColumnCount + (col.IsFixedLength && !newSpec.IsFixedLength ? 1 : 0), - // A type or length change burns a fresh column id, so the bitmap can widen by one. - HighWater(table) + (col.Type == newSpec.Type && col.Length == newSpec.Length ? 0 : 1), + table.ColumnIdHighWater + (col.Type == newSpec.Type && col.Length == newSpec.Length ? 0 : 1), _channel.Format); // A pure reseed of an existing counter — ALTER COLUMN c COUNTER(seed, increment) where c is already an @@ -2219,21 +1966,16 @@ public void AlterColumn(string tableName, string columnName, ColumnSpec newSpec) return; } - // Promote a plain Int32 column to an AutoNumber — a counter is stored identically (both a 4-byte Int32); - // the only differences are the column's 0x04 flag and the header's seed/increment. So it's a metadata - // edit, not a rebuild. (ACE/SQL Server reject this; PostgreSQL/MySQL and LibRed allow it — see spec.) - if (!col.IsAutoNumber && newSpec.IsAutoNumber && col.Type == newSpec.Type) + // Promote a plain Int32 column to an AutoNumber, or demote a counter back to a plain Int32 — a counter is + // stored identically (both a 4-byte Int32); the only differences are the column's 0x04 flag and the + // header's seed/increment. So either way it's a metadata edit, not a rebuild. ACE/SQL Server reject + // promotion (PostgreSQL/MySQL and LibRed allow it — see spec); ACE *allows* demotion, which resets the + // header to a non-AutoNumber table's state (0x14 = 0, 0x18 = 1), keeps existing values, and stops the + // column auto-assigning. + if (col.IsAutoNumber != newSpec.IsAutoNumber && col.Type == newSpec.Type) { - PromoteColumnToCounter(table, col, newSpec.Seed, newSpec.Increment); - return; - } - - // Demote a counter back to a plain Int32 — the reverse, and likewise a metadata edit: clear the 0x04 - // flag and reset the header to a non-AutoNumber table's state (0x14 = 0, 0x18 = 1). ACE *allows* this - // (unlike promotion), so LibRed matches; existing values are kept and the column stops auto-assigning. - if (col.IsAutoNumber && !newSpec.IsAutoNumber && col.Type == newSpec.Type) - { - DemoteCounterToInt(table, col); + SetAutoNumber(table, col, newSpec.IsAutoNumber, + newSpec.IsAutoNumber ? newSpec.Seed : 1, newSpec.IsAutoNumber ? newSpec.Increment : 1); return; } @@ -2280,20 +2022,15 @@ private static byte[] ReplaceTableProperty(byte[] blob, string name, string? val /// precisely that hole, and would pass a declaration whose rows then overrun 4060, which is a table Access /// refuses to open the database for. On a table that has dropped nothing the two agree, because the /// offsets pack from zero. Boolean is fixed but occupies no data, so it contributes nothing, exactly as - /// counts it on create. - private static int FixedBytes(TableDef table) => + /// counts it on create. + private static int FixedBytes(TableDefinition table) => table.Columns.Where(c => c.IsFixedLength && c.Type != JetDataType.Boolean) .Select(c => c.FixedOffset + c.Length).DefaultIfEmpty(0).Max(); - /// The TDEF's `0x29` column-id high-water — the number of ids handed out over the table's - /// lifetime, which is what sizes a record's null bitmap (dropped ids keep their bit). - private int HighWater(TableDef table) => - ReadDefinition(table.DefinitionPage).Buffer.ReadUInt16(_channel.Format.TdefMaxColumnsOffset); - /// Whether the column is either end of a relationship — the child's FK column or the parent's /// referenced key. ACE refuses to alter or drop such a column; the two callers differ only in the message /// they raise, so the rule itself lives here. - private bool ColumnIsInRelationship(TableDef table, ColumnDef column) + private bool ColumnIsInRelationship(TableDefinition table, ColumnDef column) { const StringComparison oic = StringComparison.OrdinalIgnoreCase; return _catalog.Relationships.Any(r => @@ -2306,7 +2043,7 @@ private bool ColumnIsInRelationship(TableDef table, ColumnDef column) /// ACE rejects every type/length alteration of a relationship column, on either the /// referencing or referenced side. Keep this check ahead of all specialized ALTER paths so none of /// them can bypass it. - private void EnsureColumnIsNotInRelationship(TableDef table, ColumnDef column) + private void EnsureColumnIsNotInRelationship(TableDefinition table, ColumnDef column) { if (ColumnIsInRelationship(table, column)) throw new InvalidOperationException( @@ -2318,46 +2055,46 @@ private void EnsureColumnIsNotInRelationship(TableDef table, ColumnDef column) /// increment (0x18); no data or descriptor changes. ACE rejects reseeding a counter that participates /// in a relationship ("Cannot change field 'X'. It is part of one or more relationships." — verified); match /// that. - private void ReseedCounter(TableDef table, ColumnDef col, int seed, int increment) + private void ReseedCounter(TableDefinition table, ColumnDef col, int seed, int increment) { EnsureColumnIsNotInRelationship(table, col); - if (increment == 0) throw TdefBuilder.ZeroIncrement(col.Name); - JetFormatBase format = _channel.Format; + if (increment == 0) throw TableDefinition.ZeroIncrement(col.Name); byte[] tdef = _channel.ReadPage(table.DefinitionPage).Span.ToArray(); - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(format.TdefLastAutoNumberOffset, 4), seed - increment); - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(format.TdefAutoNumberIncrementOffset, 4), increment); + TableDefinition.WriteCounter(tdef, _channel.Format, seed, increment); _channel.WritePage(table.DefinitionPage, tdef); _catalog.Invalidate(); } - /// Promotes a plain Int32 column to an AutoNumber in place — ALTER COLUMN c COUNTER(seed, increment) - /// where c is a plain integer. A counter is stored identically to a Long Integer, so this only sets the - /// column descriptor's 0x04 AutoNumber flag and the header's seed/increment (0x14/0x18); - /// existing values are untouched. Only one column may draw on that pair, so a second is rejected — complex - /// columns are flagged 0x04 too but allocate from 0x1C, so they do not count as the existing - /// one; and (like the reseed path) a column in a relationship is rejected, matching ACE. - private void PromoteColumnToCounter(TableDef table, ColumnDef col, int seed, int increment) + /// Turns a plain Int32 column into an AutoNumber in place, or an AutoNumber back into a plain Int32 — + /// ALTER COLUMN c COUNTER(seed, increment) where c is a plain integer, or ALTER COLUMN c LONG where c is a + /// counter. A counter is stored identically to a Long Integer, so this only sets or clears the column + /// descriptor's 0x04 AutoNumber flag and writes the header's seed/increment (0x14/0x18) — + /// for a demotion, seed 1 and increment 1, a non-AutoNumber table's state. Existing values are untouched; a + /// demoted column just stops auto-assigning. + /// Promotion carries ACE's refusals: only one column may draw on that pair, so a second is rejected — + /// complex columns are flagged 0x04 too but allocate from 0x1C, so they do not count as the + /// existing one — and (like the reseed path) a column in a relationship is rejected. Demotion has neither: + /// ACE permits it, unlike int→counter promotion. + private void SetAutoNumber(TableDefinition table, ColumnDef col, bool autoNumber, int seed, int increment) { - if (table.Columns.Any(c => c.IsAutoNumber && c.Type != JetDataType.Complex && c.ColumnId != col.ColumnId)) - throw new InvalidOperationException( - $"Cannot make '{col.Name}' an AutoNumber: table '{table.Name}' already has one, and a table " - + "can have only one."); - EnsureColumnIsNotInRelationship(table, col); - - if (increment == 0) throw TdefBuilder.ZeroIncrement(col.Name); - JetFormatBase format = _channel.Format; - TdefParts parts = ParseTdef(table.DefinitionPage); - int descSize = format.ColumnDescriptorSize; - for (int i = 0; i < table.Columns.Count; i++) + if (autoNumber) { - int entry = i * descSize; - if (BinaryPrimitives.ReadUInt16LittleEndian(parts.Columns.AsSpan(entry + format.ColumnNumberOffset, 2)) != col.ColumnId) continue; - parts.Columns[entry + format.ColumnFlagsOffset] |= JetFormatBase.ColumnFlagAutoNumber; - break; + if (table.Columns.Any(c => c.IsAutoNumber && c.Type != JetDataType.Complex && c.ColumnId != col.ColumnId)) + throw new InvalidOperationException( + $"Cannot make '{col.Name}' an AutoNumber: table '{table.Name}' already has one, and a table " + + "can have only one."); + EnsureColumnIsNotInRelationship(table, col); + if (increment == 0) throw TableDefinition.ZeroIncrement(col.Name); } - BinaryPrimitives.WriteInt32LittleEndian(parts.Header.AsSpan(format.TdefLastAutoNumberOffset, 4), seed - increment); - BinaryPrimitives.WriteInt32LittleEndian(parts.Header.AsSpan(format.TdefAutoNumberIncrementOffset, 4), increment); + + JetFormatBase format = _channel.Format; + TableDefinition.Parts parts = TableDefinition.ReadParts(_channel, table.DefinitionPage); + byte[] descriptor = parts.Columns[col.Index].Descriptor; + var flags = (ColumnFlags)descriptor[format.ColumnFlagsOffset]; + descriptor[format.ColumnFlagsOffset] = + (byte)(autoNumber ? flags | ColumnFlags.AutoNumber : flags & ~ColumnFlags.AutoNumber); + TableDefinition.WriteCounter(parts.Header, format, seed, increment); WriteTdef(table.DefinitionPage, parts); _catalog.Invalidate(); @@ -2365,83 +2102,20 @@ private void PromoteColumnToCounter(TableDef table, ColumnDef col, int seed, int // make it a "Random" AutoNumber (IsRandomAutoNumber) — assigning random ids and ignoring the seed — so // clear it to honour the requested sequence. Other (literal) defaults are inert on a counter (the insert // path skips defaults for AutoNumber columns) and are left as-is. - if (col.DefaultValue?.Trim().Equals("GenUniqueID()", StringComparison.OrdinalIgnoreCase) == true) + if (autoNumber && col.DefaultValue?.Trim().Equals("GenUniqueID()", StringComparison.OrdinalIgnoreCase) == true) DropColumnDefault(table.Name, col.Name); } - /// Demotes an AutoNumber column back to a plain Int32 in place — ALTER COLUMN c LONG where c is a - /// counter. Clears the descriptor's 0x04 flag and resets the header to a non-AutoNumber table's state - /// (0x14 = 0, 0x18 = 1); existing values are kept, the column just stops auto-assigning. ACE - /// permits this (unlike int→counter promotion), so no divergence. - private void DemoteCounterToInt(TableDef table, ColumnDef col) - { - JetFormatBase format = _channel.Format; - TdefParts parts = ParseTdef(table.DefinitionPage); - int descSize = format.ColumnDescriptorSize; - for (int i = 0; i < table.Columns.Count; i++) - { - int entry = i * descSize; - if (BinaryPrimitives.ReadUInt16LittleEndian(parts.Columns.AsSpan(entry + format.ColumnNumberOffset, 2)) != col.ColumnId) continue; - parts.Columns[entry + format.ColumnFlagsOffset] &= unchecked((byte)~JetFormatBase.ColumnFlagAutoNumber); - break; - } - BinaryPrimitives.WriteInt32LittleEndian(parts.Header.AsSpan(format.TdefLastAutoNumberOffset, 4), 0); - BinaryPrimitives.WriteInt32LittleEndian(parts.Header.AsSpan(format.TdefAutoNumberIncrementOffset, 4), 1); - WriteTdef(table.DefinitionPage, parts); - _catalog.Invalidate(); - } - /// Applies ACE's in-place column retype to the target descriptor within - /// (no page write — the caller writes the TDEF once): the target becomes a NEW column with a fresh id from the - /// 0x29 high-water and its fixed data appended to the END of the current fixed region (its old slot left - /// as dead space — ACE does not compact); 0x29 bumps, and 0x2B too for a variable retype. Only - /// the target descriptor changes; every other descriptor stays byte-identical. Returns the burned new id. - private static int EditTargetDescriptor(TdefParts parts, ColumnDef target, ColumnSpec newSpec, int fixedEnd, - Collation collation, JetFormatBase format) - { - int maxCols = BinaryPrimitives.ReadUInt16LittleEndian(parts.Header.AsSpan(format.TdefMaxColumnsOffset, 2)); - // ACE-only probe: with 254 columns one retype succeeds, the next fails; with 255 - // columns the first retype fails. A same-type ALTER does not reach this id-burning path. - if (maxCols >= MaxColumnsPerTable) - throw new NotSupportedException( - $"Cannot change the type of '{target.Name}': too many fields defined — {MaxColumnsPerTable} column ids have been used."); - int varCount = BinaryPrimitives.ReadUInt16LittleEndian(parts.Header.AsSpan(format.TdefVariableColumnsOffset, 2)); - - Span d = parts.Columns.AsSpan(target.Index * format.ColumnDescriptorSize, format.ColumnDescriptorSize); - d[format.ColumnTypeOffset] = (byte)newSpec.Type; - BinaryPrimitives.WriteUInt16LittleEndian(d[format.ColumnNumberOffset..], (ushort)maxCols); // +0x05 id burned - // The target's var-index (+0x07) becomes the old variable-column count — the next var slot — for BOTH a - // fixed and a variable retype (verified vs ACE); a variable retype also bumps the 0x2B var-column count. - BinaryPrimitives.WriteUInt16LittleEndian(d[format.ColumnVariableIndexOffset..], (ushort)varCount); - // +0x09 is deliberately left unchanged: it is the column's ordinal position, which a retype does not - // move (verified: ACE does not update it). - byte flags = d[format.ColumnFlagsOffset]; - flags = newSpec.IsFixedLength ? (byte)(flags | JetFormatBase.ColumnFlagFixedLength) - : (byte)(flags & ~JetFormatBase.ColumnFlagFixedLength); - flags = newSpec.IsAutoNumber ? (byte)(flags | JetFormatBase.ColumnFlagAutoNumber) - : (byte)(flags & ~JetFormatBase.ColumnFlagAutoNumber); - d[format.ColumnFlagsOffset] = flags; - BinaryPrimitives.WriteUInt16LittleEndian(d[format.ColumnFixedOffsetOffset..], (ushort)(newSpec.IsFixedLength ? fixedEnd : 0)); // +0x15 - BinaryPrimitives.WriteUInt16LittleEndian(d[format.ColumnLengthOffset..], (ushort)newSpec.Length); // +0x17 - // 0x0B–0x0E is a union keyed by type, so the WHOLE union is rewritten, not just the decimal arm. - // Writing precision/scale on the way in but nothing on the way out left a former DECIMAL(12,3) with - // 0x0C 0x03 in its LANGID bytes, which reads back as collating order 0x030C on a text column. - TdefBuilder.WriteLocaleUnion(d, newSpec.Type, newSpec.Precision, newSpec.Scale, collation, format); - - BinaryPrimitives.WriteUInt16LittleEndian(parts.Header.AsSpan(format.TdefMaxColumnsOffset, 2), (ushort)(maxCols + 1)); // 0x29++ - if (!newSpec.IsFixedLength) - BinaryPrimitives.WriteUInt16LittleEndian(parts.Header.AsSpan(format.TdefVariableColumnsOffset, 2), (ushort)(varCount + 1)); // 0x2B++ - return maxCols; - } /// Full in-place column type change, byte-for-byte like ACE for fixed and variable columns and /// targets, fixed↔variable, indexed targets, and a Memo/OLE source or target (whose long-value maps are placed - /// and retired as ADD and DROP COLUMN do). Edits the TDEF in place () and re-lays every row — the target's + /// and retired as ADD and DROP COLUMN do). Edits the TDEF in place () and re-lays every row — the target's /// OLD fixed slot is kept as dead space, its converted value appended at the new offset, count + null bitmap /// updated. Converts values in memory first (throws on bad data before any write); runs in a transaction. public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSpec newSpec) { - TableDef oldDef = _catalog.FindTable(tableName) + TableDefinition oldDef = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' does not exist."); ColumnDef oldTarget = oldDef.FindColumn(columnName) ?? throw new InvalidOperationException($"Column '{columnName}' does not exist in '{tableName}'."); @@ -2452,8 +2126,9 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp // Also reached directly, not only through AlterColumn, so it carries the width limits itself. // The record-fits check needs the true fixed-region end, so it runs once that is measured, below. - RecordLayout.ValidateFieldWidth(newSpec.Name, newSpec.Type, newSpec.Length); + RowCodec.ValidateFieldWidth(newSpec.Name, newSpec.Type, newSpec.Length); JetDataTypeVersions.EnsureStorable(newSpec.Type, _channel.Format.Version, newSpec.Name); + TableDefinition.ValidateNumericPrecision(newSpec); // A column becoming Memo/OLE, or ceasing to be one, takes the same in-place edit as any other retype plus the // long-value side of ADD and DROP COLUMN (verified vs ACE, both directions): the new column gets its §3.3.2 @@ -2471,7 +2146,7 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp // 1. Materialize (id + raw bytes + values) before touching disk; conversion throws here on bad data. var reader = new RowInserter(_channel, oldDef); - var rows = new Table(_channel, oldDef).Rows().WithIds() + var rows = new Table(_channel, oldDef, _catalog).Rows().WithIds() .Select(r => (r.Id, Raw: reader.ReadRow(r.Id), Values: (object?[])r.Values.Clone())) .ToList(); foreach (var r in rows) @@ -2484,20 +2159,22 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp // Sizing the whole re-lay from whichever row happened to be first either truncates the long rows' fixed // tails or drags the short rows' variable data up into their fixed region. The schema floor covers an // empty table, and rows shorter than the result are zero-filled by BuildRelaidRecord. + // Each row's own fixed region is read from the row itself (RowCodec.Layout.FixedRegionLength), which is + // authoritative over the live column descriptors: those omit dead fixed slots left by prior retypes. int oldFixedLen = FixedBytes(oldDef); foreach (var r in rows) - oldFixedLen = Math.Max(oldFixedLen, FixedRegionLength(r.Raw, RowLayout.HasVariableSection(r.Raw, oldDef.Columns))); + oldFixedLen = Math.Max(oldFixedLen, RowCodec.ParseLayout(oldDef.Columns, _channel.Format, r.Raw).FixedRegionLength); // Now the widest-record check, against what this path actually produces. Both counts come from stored // state, not the live column list: the re-lay KEEPS the old target's fixed slot as dead space rather // than reclaiming it (so nothing is subtracted), and 0x2B is a high-water that never decrements (so a // variable→fixed retype leaves it where it is). Deriving either from the live columns under-counts on // any table that has dropped or retyped a column, passing a declaration that then overflows 4060 — - // and per RecordLayout's own remarks, Access cannot open a database containing such a table at all. - RecordLayout.ValidateRecordFits(tableName, + // and per RowCodec's own remarks, Access cannot open a database containing such a table at all. + RowCodec.ValidateRecordFits(tableName, oldFixedLen + (newSpec.IsFixedLength && newSpec.Type != JetDataType.Boolean ? newSpec.Length : 0), oldDef.VariableColumnCount + (oldTarget.IsFixedLength && !newSpec.IsFixedLength ? 1 : 0), - HighWater(oldDef) + 1, // the type change burns a fresh id + oldDef.ColumnIdHighWater + 1, // the type change burns a fresh id _channel.Format); bool ownTx = !_channel.InTransaction; @@ -2510,8 +2187,8 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp // appended fixed offset is the row's true fixed-region end incl. dead slots) AND re-point every // index over the target, all into the SAME parts, then write the TDEF a single time. Each index // re-point needs the fresh root allocated + owned-map recycled first (page work off the TDEF). - TdefParts parts = ParseTdef(oldDef.DefinitionPage); - int newTargetId = EditTargetDescriptor(parts, oldTarget, newSpec, oldFixedLen, _collation, format); + TableDefinition.Parts parts = TableDefinition.ReadParts(_channel, oldDef.DefinitionPage); + int newTargetId = TableDefinition.EditTargetDescriptor(parts, oldTarget, newSpec, oldFixedLen, _collation, format); // A column becoming a long value gets its entry, placed as ADD COLUMN places it, and its two map records // ahead of the index rebuilds' records on the usage-map page (verified vs ACE). @@ -2520,7 +2197,7 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp // ACE rebuilds the indexes over the target in logical-block (name) order, allocating their roots in that // order, and hands them back the real-index slots they held in that order too. List rebuildOrder = parts.Logical - .Select(b => BinaryPrimitives.ReadInt32LittleEndian(b.Info.AsSpan(IndexBlockFormat.InfoDataNumberOffset, 4))) + .Select(b => TableDefinition.ReadInfoBlock(b.Info, format, NameOf(b.Name)).DataOrdinal) .Distinct().Where(affectedIndexes.ContainsKey).ToList(); var pending = new List<(string Name, int OldRoot, int NewRoot, bool IgnoreNulls)>(); foreach (int ordinal in rebuildOrder) @@ -2537,11 +2214,11 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp var retire = new List(); if (fromLongValue) { - var oldDefinition = new TableDefinitionPage(); + var oldDefinition = new TableDefinition(); oldDefinition.Read(_channel, oldDef.DefinitionPage); oldMaps = new UsageMap(_channel, oldDef); QueueLongValueMaps(oldDefinition, oldTarget, oldMaps, released, retire); - RemoveLongValueMapEntry(parts, oldTargetId); + TableDefinition.RemoveLongValueMapEntry(_channel.Format, parts, oldTargetId); } WriteTdef(oldDef.DefinitionPage, parts); @@ -2553,12 +2230,12 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp foreach (int page in released) _allocator.Release(page); // reusable only after this handle closes } - TableDef newDef = _catalog.FindTable(tableName)!; + TableDefinition newDef = _catalog.FindTable(tableName)!; ColumnDef newTarget = newDef.FindColumn(columnName)!; int newMaxId = newDef.Columns.Max(c => c.ColumnId); int newFixedLen = newTarget.IsFixedLength ? oldFixedLen + newTarget.Length : oldFixedLen; - // Slots per row comes from the TDEF high-water, exactly as RowEncoder derives it — never from a + // Slots per row comes from the TDEF high-water, exactly as RowCodec derives it — never from a // row's own stored numVar. A row written before a variable ADD COLUMN carries fewer slots than the // table has, and appending the retyped column onto such a row lands it at the wrong index while its // descriptor names the high-water one, which is the "A column Id is incorrect" file ACE rejects. @@ -2582,7 +2259,7 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp // (last, so the new root got the appended page rather than reusing this one) — as ACE does. foreach (var p in pending) { - BackfillIndex(tableName, p.Name, p.IgnoreNulls, validateUnique: true); + BackfillIndex(newDef, p.Name, p.IgnoreNulls, validateUnique: true); _allocator.Release(p.OldRoot); } @@ -2597,65 +2274,48 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp /// the converted target is appended (a new fixed slot if it is fixed, else a new variable chunk), and the /// leading count (= max id + 1), variable-offset table + numVar (omitted if none), and null bitmap /// (each dead id's bit carried over from the old row) are rebuilt. - private static byte[] BuildRelaidRecord(byte[] oldRow, int oldFixedLen, IReadOnlyList oldCols, + private byte[] BuildRelaidRecord(byte[] oldRow, int oldFixedLen, IReadOnlyList oldCols, ColumnDef newTarget, object?[] values, IReadOnlyList newCols, int newMaxId, int newFixedLen, int newVarCount) { object? tv = values[newTarget.Index]; byte[] targetBytes = tv is null ? (newTarget.IsFixedLength ? new byte[newTarget.Length] : []) - : Types.JetTypeCodec.Encode(newTarget, tv); + : Storage.Types.JetTypeCodec.Encode(newTarget, tv, _channel.Format); - // Whether THIS row has a variable trailer, not whether the schema does — see RowLayout.HasVariableSection. - bool hasVar = RowLayout.HasVariableSection(oldRow, oldCols); + // Whether THIS row has a variable trailer, not whether the schema does — see RowCodec.Layout.HasVariableSection. + JetFormatBase format = _channel.Format; + int countSize = format.RowColumnCountSize; + RowCodec.Layout oldLayout = RowCodec.ParseLayout(oldCols, format, oldRow); // Fixed region: old fixed bytes verbatim (incl. a dead fixed slot); append the target if it is fixed. // A row predating a fixed ADD COLUMN is shorter than the region; copy what it has and leave the rest // zeroed, which is what its null bitmap already says those columns are. var newFixed = new byte[newFixedLen]; - int rowFixedLen = Math.Min(oldFixedLen, RowLayout.Parse(oldRow, 2, hasVar).FixedRegionLength); - Array.Copy(oldRow, 2, newFixed, 0, rowFixedLen); + int rowFixedLen = Math.Min(oldFixedLen, oldLayout.FixedRegionLength); + Array.Copy(oldRow, countSize, newFixed, 0, rowFixedLen); if (newTarget.IsFixedLength && tv is not null) Array.Copy(targetBytes, 0, newFixed, newTarget.FixedOffset, newTarget.Length); else if (newTarget.IsFixedLength && rowFixedLen == oldFixedLen) { // ACE writes nothing into the new slot of a NULL: it holds whatever the old record had at those // offsets — the start of its variable data, say — rather than zeros (verified on full-width rows). - int from = 2 + newTarget.FixedOffset; + int from = countSize + newTarget.FixedOffset; int length = Math.Min(newTarget.Length, oldRow.Length - from); if (length > 0) Array.Copy(oldRow, from, newFixed, newTarget.FixedOffset, length); } // Variable chunks: old chunks verbatim (incl. a dead variable chunk), padded out to the table's slot // count so the target lands on the index its descriptor names, then the target placed at that index. - List chunks = ExtractVarChunks(oldRow, hasVar); + var chunks = new List(Math.Max(oldLayout.NumVar, newVarCount)); + for (int j = 0; j < oldLayout.NumVar; j++) chunks.Add(oldLayout.VarChunk(j).ToArray()); while (chunks.Count < newVarCount) chunks.Add([]); if (!newTarget.IsFixedLength) chunks[newTarget.VariableIndex] = targetBytes; // Assemble via the shared row layout (count + var table + null bitmap identical to a fresh encode), each // dead id's bit carried over from the old row's bitmap. - int oldCount = BinaryPrimitives.ReadUInt16LittleEndian(oldRow); - return RowEncoder.AssembleRow(newMaxId, newFixed, chunks, newCols, values, - priorBitmap: oldRow.AsSpan(oldRow.Length - (oldCount + 7) / 8)); - } - - /// The length of a row's fixed-data region (bytes between the leading count and the variable data), - /// read from the row itself — its variable-offset table's last entry is the variable-data start (= 2 + fixed - /// length), or for an all-fixed row it's the whole row minus the count field and null bitmap. This is - /// authoritative over the live column descriptors, which omit dead fixed slots left by prior retypes. - private static int FixedRegionLength(byte[] row, bool hasVar) => - RowLayout.Parse(row, 2, hasVar).FixedRegionLength; - - /// Extracts a row's variable-column chunks (in variable-index order) verbatim, using the row's own - /// stored numVar. (from the schema) says whether a variable section exists at all — - /// an all-fixed table omits it entirely, so its "numVar" bytes would otherwise be misread from fixed data. - private static List ExtractVarChunks(byte[] row, bool hasVar) - { - RowLayout layout = RowLayout.Parse(row, 2, hasVar); - var chunks = new List(layout.NumVar); - for (int j = 0; j < layout.NumVar; j++) - chunks.Add(layout.VarChunk(j).ToArray()); - return chunks; + return RowCodec.AssembleRow(format, newMaxId, newFixed, chunks, newCols, values, + priorBitmap: oldLayout.NullBitmap); } /// Hands the rebuilt indexes back the real-index slots they held, in the order they were rebuilt: the @@ -2667,8 +2327,9 @@ private static List ExtractVarChunks(byte[] row, bool hasVar) /// IX_E, IX_Z (all verified vs ACE, including logical indexes sharing one real index — the primary /// key moves like any other index). Only plain indexes and the primary key get here: a relationship's column /// cannot be retyped. - private static void ReassignRebuiltIndexSlots(TdefParts parts, List rebuildOrder) + private void ReassignRebuiltIndexSlots(TableDefinition.Parts parts, List rebuildOrder) { + JetFormatBase format = _channel.Format; List slots = [.. rebuildOrder.Order()]; byte[][] stats = [.. rebuildOrder.Select(o => parts.Stats[o])]; byte[][] blocks = [.. rebuildOrder.Select(o => parts.DataBlocks[o])]; @@ -2677,15 +2338,16 @@ private static void ReassignRebuiltIndexSlots(TdefParts parts, List rebuild parts.Stats[slots[k]] = stats[k]; parts.DataBlocks[slots[k]] = blocks[k]; } - List moved = [.. parts.Logical.Select(b => b.Info).Where(info => - rebuildOrder.Contains(BinaryPrimitives.ReadInt32LittleEndian(info.AsSpan(IndexBlockFormat.InfoDataNumberOffset, 4))))]; - List numbers = [.. moved.Select(info => BinaryPrimitives.ReadInt32LittleEndian(info.AsSpan(IndexBlockFormat.InfoNumberOffset, 4))).Order()]; + List<(byte[] Block, TableDefinition.LogicalIndexSpec Info)> moved = [.. parts.Logical + .Select(b => (b.Info, Info: TableDefinition.ReadInfoBlock(b.Info, format, NameOf(b.Name)))) + .Where(b => rebuildOrder.Contains(b.Info.DataOrdinal))]; + List numbers = [.. moved.Select(b => b.Info.Number).Order()]; for (int i = 0; i < moved.Count; i++) - { - Span ordinal = moved[i].AsSpan(IndexBlockFormat.InfoDataNumberOffset, 4); - BinaryPrimitives.WriteInt32LittleEndian(ordinal, slots[rebuildOrder.IndexOf(BinaryPrimitives.ReadInt32LittleEndian(ordinal))]); - BinaryPrimitives.WriteInt32LittleEndian(moved[i].AsSpan(IndexBlockFormat.InfoNumberOffset, 4), numbers[i]); - } + TableDefinition.WriteInfoBlock(moved[i].Block, format, moved[i].Info with + { + DataOrdinal = slots[rebuildOrder.IndexOf(moved[i].Info.DataOrdinal)], + Number = numbers[i], + }); } /// Prepares one index rebuild over a just-modified column, matching ACE's reconstruction: allocate a @@ -2693,35 +2355,30 @@ private static void ReassignRebuiltIndexSlots(TdefParts parts, List rebuild /// track it, then re-point the index-data block within to the new root with the /// target's burned column id and the new usage-map row (bumping the stats block). The caller writes the TDEF /// once, then backfills the fresh B-tree and frees the old root. Returns the new root page. - private int PrepareIndexRebuild(TdefParts parts, TableDef table, IndexDef index, int oldTargetId, int newTargetId) + private int PrepareIndexRebuild(TableDefinition.Parts parts, TableDefinition table, IndexDef index, int oldTargetId, int newTargetId) { JetFormatBase format = _channel.Format; // A fresh empty root leaf, appended; the old root is freed by the caller afterwards (ACE reuses it on the // next alloc). This and the owned-map recycle touch pages OFF the TDEF, so they happen before the single // TDEF write; only the index-data block + stats mutations below go into the shared parts. - int newRoot = _allocator.Allocate(); - WriteEmptyLeafIndexPage(format, newRoot, owner: table.DefinitionPage); - - int usageMapPage = parts.Header[format.TdefOwnedPagesOffset + 1] - | (parts.Header[format.TdefOwnedPagesOffset + 2] << 8) | (parts.Header[format.TdefOwnedPagesOffset + 3] << 16); + int newRoot = AllocateIndexRoot(format, table.DefinitionPage); // Recycle the index's owned-map row (ACE soft-deletes the old row and reuses its space for a new row - // tracking the new root), reading the current row number from the (as-yet-unwritten) data block. + // tracking the new root), reading the current pointer from the (as-yet-unwritten) data block. The map's + // holder is the page that pointer names, not the table's own owned map's. Span block = parts.DataBlocks[index.RealIndexOrdinal]; - int oldUsageRow = block[IndexBlockFormat.UsageMapRowOffset]; - int newRow = RecycleOwnedMapRow(format, usageMapPage, oldUsageRow, newRoot); + (int Row, int Page) map = index.UsageMap; + int newRow = RecycleOwnedMapRow(format, map.Page, map.Row, newRoot); // Re-point the index-data block: the target's burned id in its column slot, the new root, the new // usage-map row. Its statistics are set by the backfill that follows, from the rows it then holds. - for (int slot = 0; slot < IndexBlockFormat.MaxColumns; slot++) + for (int slot = 0; slot < format.IndexDataMaxColumns; slot++) { - int at = IndexBlockFormat.ColumnsOffset + slot * IndexBlockFormat.ColumnSlotSize; - if (BinaryPrimitives.ReadInt16LittleEndian(block.Slice(at, 2)) == oldTargetId) - BinaryPrimitives.WriteInt16LittleEndian(block.Slice(at, 2), (short)newTargetId); + (short columnId, IndexColumnOrder order) = TableDefinition.ReadIndexSlot(block, format, slot); + if (columnId == oldTargetId) TableDefinition.WriteIndexSlot(block, format, slot, newTargetId, order); } - block[IndexBlockFormat.UsageMapRowOffset] = (byte)newRow; - BinaryPrimitives.WriteInt32LittleEndian(block.Slice(IndexBlockFormat.RootPageOffset, 4), newRoot); + TableDefinition.WriteIndexTree(block, format, newRoot, newRow, map.Page); return newRoot; } @@ -2757,18 +2414,15 @@ private int PrepareIndexRebuild(TdefParts parts, TableDef table, IndexDef index, /// private int RecycleOwnedMapRow(JetFormatBase format, int usageMapPage, int oldRow, int newRoot) { - int dir = format.DataRowDirectoryOffset; - int rowCount = BinaryPrimitives.ReadUInt16LittleEndian( - _channel.ReadPage(usageMapPage).Span.Slice(format.DataRowCountOffset, 2)); - int newRow = rowCount; + int rowCount = DataPage.ReadRowCount(_channel.ReadPage(usageMapPage).Span, format); if (oldRow < 0 || oldRow >= rowCount) throw new InvalidDataException( $"Usage-map row {usageMapPage}:{oldRow} does not exist; the page has {rowCount} rows."); // (1) ACE's first write, kept verbatim: the appended row is where the new root's bit is set, and the // bytes it leaves behind are part of the file ACE produces. - AppendEmptyUsageMapRow(format, usageMapPage, newRow); - new UsageMapWriter(_channel).SetBit(newRow, usageMapPage, newRoot, set: true); + int newRow = AppendEmptyUsageMapRow(format, usageMapPage); + new UsageMap(_channel).SetBit(newRow, usageMapPage, newRoot, set: true); // (2) Re-lay the live records. Starting from the page as it stands keeps everything this does not // write — the abandoned append included — exactly where ACE leaves it. @@ -2776,30 +2430,19 @@ private int RecycleOwnedMapRow(JetFormatBase format, int usageMapPage, int oldRo var holder = new DataPage(); holder.Read(_channel.ReadPage(usageMapPage), format); + // The recycled row becomes a zero-length deleted + overflow tombstone; every other slot keeps its flags. var records = new byte[rowCount + 1][]; - var flags = new ushort[rowCount + 1]; + var flags = new RowSlotFlags[rowCount + 1]; for (int i = 0; i <= rowCount; i++) { records[i] = i == oldRow ? [] : page.AsSpan(holder.Rows[i].Offset, holder.Rows[i].Length).ToArray(); - flags[i] = (ushort)((i == oldRow || holder.Rows[i].IsDeleted ? RowPointer.DeletedFlag : 0) - | (i == oldRow || holder.Rows[i].HasOverflow ? RowPointer.OverflowFlag : 0)); + flags[i] = i == oldRow ? RowSlotFlags.Deleted | RowSlotFlags.Overflow : DataPage.Flags(holder.Rows[i]); } - int directoryEnd = dir + (rowCount + 1) * 2; - int offset = format.PageSize; - for (int i = 0; i <= rowCount; i++) - { - offset -= records[i].Length; // a 0-length tombstone lands on the previous start - if (offset < directoryEnd) - throw new InvalidOperationException( - $"Usage-map page {usageMapPage} has no room to recycle row {oldRow}: {rowCount} rows already. " - + "The new map belongs on a page of its own."); - records[i].CopyTo(page.AsSpan(offset)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(dir + i * 2, 2), - (ushort)(flags[i] | (offset & RowPointer.OffsetMask))); - } - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), - (ushort)(offset - directoryEnd)); + if (!DataPage.LayRows(page, format, records, flags)) + throw new InvalidOperationException( + $"Usage-map page {usageMapPage} has no room to recycle row {oldRow}: {rowCount} rows already. " + + "The new map belongs on a page of its own."); _channel.WritePage(usageMapPage, page); return newRow; } @@ -2856,26 +2499,9 @@ private int RecycleOwnedMapRow(JetFormatBase format, int usageMapPage, int oldRo } - /// Appends the new column's descriptor (after the existing descriptors) and its name (after the - /// existing names) to the column region. - private static void AppendColumnToParts(TdefParts parts, int colCount, byte[] descriptor, string name, JetFormatBase format) - { - int namesStart = colCount * format.ColumnDescriptorSize; - ReadOnlySpan cols = parts.Columns; - - byte[] nameBytes = System.Text.Encoding.Unicode.GetBytes(name); - var blob = new List(parts.Columns.Length + descriptor.Length + 2 + nameBytes.Length); - blob.AddRange(cols[..namesStart].ToArray()); // existing descriptors - blob.AddRange(descriptor); // new descriptor - blob.AddRange(cols[namesStart..].ToArray()); // existing names - blob.Add((byte)nameBytes.Length); blob.Add((byte)(nameBytes.Length >> 8)); - blob.AddRange(nameBytes); // new name - parts.Columns = [.. blob]; - } - /// /// Drops a column byte-faithfully with ACE (probed): a **metadata-only TDEF edit** — removes the - /// column's 25-byte descriptor and its name, and decrements the live ColumnCount (0x2D). It does + /// column's descriptor and its name, and decrements the live ColumnCount (0x2D). It does /// **not** renumber the surviving columns, recompute their fixed offsets/variable indexes, decrement the /// VariableColumnCount (0x2B stays a high-water mark), or rewrite existing rows — survivors keep /// their stored variable index (§3.4) so old rows still decode (the dropped column's data becomes dead @@ -2888,7 +2514,7 @@ private static void AppendColumnToParts(TdefParts parts, int colCount, byte[] de /// public bool DropColumn(string tableName, string columnName) { - TableDef table = _catalog.FindTable(tableName) + TableDefinition table = _catalog.FindTable(tableName) ?? throw new InvalidOperationException($"Table '{tableName}' was not found."); ColumnDef? col = table.Columns.FirstOrDefault(c => string.Equals(c.Name, columnName, StringComparison.OrdinalIgnoreCase)); if (col is null) return false; @@ -2942,20 +2568,22 @@ public bool DropColumn(string tableName, string columnName) $"Cannot drop column '{columnName}': it is part of an index or key — drop the index/constraint first."); // A long-value column's maps, read from the TDEF before it loses them. - var definition = new TableDefinitionPage(); + var definition = new TableDefinition(); definition.Read(_channel, table.DefinitionPage); var maps = new UsageMap(_channel, table); var owned = new HashSet(); var retire = new List(); QueueLongValueMaps(definition, col, maps, owned, retire); - TdefParts parts = ParseTdef(table.DefinitionPage); // stitches continuation pages for a multi-page TDEF - RemoveColumnFromParts(parts, table.Columns.Count, col.Index, _channel.Format); - RemoveLongValueMapEntry(parts, col.ColumnId); + TableDefinition.Parts parts = TableDefinition.ReadParts(_channel, table.DefinitionPage); // stitches continuation pages for a multi-page TDEF + // The descriptor and its name go; the live column count (0x2D) follows in WriteTdef. The variable-column + // count (0x2B) is deliberately left — ACE keeps it as a high-water mark (verified). + parts.Columns.RemoveAt(col.Index); + TableDefinition.RemoveLongValueMapEntry(_channel.Format, parts, col.ColumnId); WriteTdef(table.DefinitionPage, parts); RetireMapRecords(retire, maps, owned); - var allocator = new PageAllocator(_channel); + var allocator = _channel.Allocator; foreach (int page in owned) allocator.Release(page); // reusable only after this handle closes, as ACE holds them @@ -3025,9 +2653,9 @@ private void DropVersionHistory(string tableName, string memoName, VersionHistor DropTable(history.Template, keepPermissions: true); if (!_catalog.ComplexColumns.Any(c => string.Equals(c.OwnerTable.Name, tableName, StringComparison.OrdinalIgnoreCase)) - && ReadObjectFlags(tdefPage) is int flags && (flags & CatalogFormat.ObjectFlagOwnsComplexColumns) != 0) + && _catalog.RequireTable(tableName).ObjectFlags is var flags && (flags & ObjectAttributes.OwnsComplexColumns) != 0) UpdateCatalogRows("MSysObjects", "Id", tdefPage, required: true, - ("Flags", flags & ~CatalogFormat.ObjectFlagOwnsComplexColumns)); + ("Flags", unchecked((int)(flags & ~ObjectAttributes.OwnsComplexColumns)))); _catalog.Invalidate(); } @@ -3039,176 +2667,46 @@ private void DropVersionHistory(string tableName, string memoName, VersionHistor /// for the table's last append-only memo. private void RemoveColumnProperties(int tdefPage, string columnName, bool alsoTableAppendOnly = false) { - (TableDef msys, Table table, int idIdx, ColumnDef lvProp) = ObjectProperties(); - - foreach ((RowId id, object?[] values) in RowsKeyed(table, idIdx, tdefPage)) - { - if (values[lvProp.Index] is not byte[] { Length: > 0 } blob) return; - - byte[] cleaned = PropertyBlob.RemoveOwner(blob, columnName); - if (alsoTableAppendOnly) cleaned = ReplaceTableProperty(cleaned, AppendOnlyProperty, null); - if (cleaned.Length == blob.Length) return; // nothing to remove + if (ReadObjectProperties(tdefPage) is not { Length: > 0 } blob) return; - byte[] descriptor = new RowInserter(_channel, msys).StorePackedLongValue(lvProp.ColumnId, cleaned); - values[lvProp.Index] = new LongValueDescriptor(descriptor); - table.Update(id, values, new HashSet { lvProp.Index }); - return; - } + byte[] cleaned = PropertyBlob.RemoveOwner(blob, columnName); + if (alsoTableAppendOnly) cleaned = ReplaceTableProperty(cleaned, AppendOnlyProperty, null); + if (cleaned.Length == blob.Length) return; // nothing to remove + WriteObjectProperties(tdefPage, cleaned); } - /// Removes the descriptor + name of the column at from the - /// column region and decrements the header's live ColumnCount (0x2D). VariableColumnCount (0x2B) is - /// deliberately left unchanged — ACE keeps it as a high-water mark (verified). - private static void RemoveColumnFromParts(TdefParts parts, int colCount, int removeIndex, JetFormatBase format) + /// True if is the incoming relationship block that cross-links to the + /// child's outgoing block number on the child's TDEF page. + private bool IsIncomingBlockFor((byte[] Info, byte[] Name) block, int childBlockNum, int childPage) { - int descSize = format.ColumnDescriptorSize; - ReadOnlySpan cols = parts.Columns; - - var descriptors = new List(colCount); - for (int i = 0; i < colCount; i++) - descriptors.Add(cols.Slice(i * descSize, descSize).ToArray()); - - int np = colCount * descSize; - var names = new List(colCount); - for (int i = 0; i < colCount; i++) - { - int len = BinaryPrimitives.ReadUInt16LittleEndian(cols.Slice(np, 2)); - names.Add(cols.Slice(np, 2 + len).ToArray()); - np += 2 + len; - } - - descriptors.RemoveAt(removeIndex); - names.RemoveAt(removeIndex); - - var blob = new List(parts.Columns.Length); - foreach (byte[] d in descriptors) blob.AddRange(d); - foreach (byte[] n in names) blob.AddRange(n); - parts.Columns = [.. blob]; - - BinaryPrimitives.WriteUInt16LittleEndian(parts.Header.AsSpan(format.TdefColumnCountOffset, 2), (ushort)(colCount - 1)); + TableDefinition.LogicalIndexSpec info = TableDefinition.ReadInfoBlock(block.Info, _channel.Format, NameOf(block.Name)); + return info.FkType == ForeignKeyType.Incoming && (int)info.FkNumber == childBlockNum && info.FkTablePage == childPage; } - /// True if is the incoming relationship block that cross-links to the - /// child's outgoing block number on the child's TDEF page (info block layout: +0x0C fk_type, - /// +0x0D child block number, +0x11 child page). - private static bool IsIncomingBlockFor(byte[] info, int childBlockNum, int childPage) => - info[IndexBlockFormat.InfoFkTypeOffset] == FkTypeIncoming && - (int)BinaryPrimitives.ReadUInt32LittleEndian(info.AsSpan(IndexBlockFormat.InfoFkNumberOffset, 4)) == childBlockNum && - BinaryPrimitives.ReadInt32LittleEndian(info.AsSpan(IndexBlockFormat.InfoFkTablePageOffset, 4)) == childPage; - /// Soft-deletes every MSysRelationships row for the named relationship. private void DeleteRelationshipRows(string name) { - TableDef msys = _catalog.RequireTable("MSysRelationships"); + TableDefinition msys = _catalog.RequireTable("MSysRelationships"); int nameIdx = msys.RequireColumn("szRelationship").Index; // A real delete, as the other catalog rows take (and as ACE's own DROP CONSTRAINT leaves the page — // measured by whole-file diff: the row's space back in the page's free count and the table's row count // down). Flagging the slot alone also left the index entries standing, pointing at a dead row. - var table = new Table(_channel, msys); + var table = new Table(_channel, msys, _catalog); var rows = table.RowsWhere([nameIdx], values => string.Equals(values[nameIdx] as string, name, StringComparison.OrdinalIgnoreCase)).ToList(); foreach ((RowId id, object?[] values) in rows) { foreach (IndexDef index in msys.RealIndexes) table.RemoveIndexEntry(index, values, id); - table.Delete(id); - } - } - - /// The name text of a TDEF name entry (2-byte UTF-16 length, then the chars). - private static string NameOf(byte[] nameEntry) => - System.Text.Encoding.Unicode.GetString(nameEntry, 2, BinaryPrimitives.ReadUInt16LittleEndian(nameEntry.AsSpan(0, 2))); - - /// The parsed regions of a table definition, for surgical block removal. A multi-page definition - /// is stitched into one buffer by ; carries its extra - /// pages so can reuse them. - private sealed class TdefParts - { - public required byte[] Header; // [0, TdefRealIndexBlockOffset) - public required List Stats; // one 12-byte stats block per data index - public required byte[] Columns; // column descriptors + names region - public required List DataBlocks; // one 52-byte index-data block per data index - public required List<(byte[] Info, byte[] Name)> Logical; // 28-byte info block + its name, name-sorted - public required byte[] Lval; // §3.3.2 list + terminator - public IReadOnlyList Continuations = []; // continuation-page numbers (multi-page TDEF) - } - - private TdefParts ParseTdef(int tdefPage) - { - JetFormatBase format = _channel.Format; - // Stitch any continuation pages into one contiguous buffer (offsets are absolute from page 1), so the - // surgery below works the same for single- and multi-page definitions. - (LibRed.IO.PageBuffer buf, IReadOnlyList continuations) = ReadDefinition(tdefPage); - - TdefRegions regions = TdefRegions.Of(buf.Span, format); - int dataCount = regions.DataCount; - int logicalCount = regions.LogicalCount; - int statsStart = regions.Stats; - int afterStats = regions.ColumnDescriptors; - int afterColumns = regions.DataBlocks; - int infoStart = regions.InfoBlocks; - int namePos = regions.IndexNames; - int defEnd = buf.ReadInt32(format.TdefLengthOffset); - - var stats = new List(dataCount); - for (int i = 0; i < dataCount; i++) stats.Add(buf.Slice(statsStart + i * format.RealIndexEntrySize, format.RealIndexEntrySize).ToArray()); - var dataBlocks = new List(dataCount); - for (int i = 0; i < dataCount; i++) dataBlocks.Add(buf.Slice(afterColumns + i * IndexBlockFormat.DataBlockSize, IndexBlockFormat.DataBlockSize).ToArray()); - - var logical = new List<(byte[], byte[])>(logicalCount); - int np = namePos; - for (int i = 0; i < logicalCount; i++) - { - byte[] info = buf.Slice(infoStart + i * IndexBlockFormat.InfoBlockSize, IndexBlockFormat.InfoBlockSize).ToArray(); - int len = buf.ReadUInt16(np); - byte[] nm = buf.Slice(np, 2 + len).ToArray(); - np += 2 + len; - logical.Add((info, nm)); + new RowInserter(table.Channel, msys).Delete(id); } - - return new TdefParts - { - Header = buf.Slice(0, statsStart).ToArray(), - Stats = stats, - Columns = buf.Slice(afterStats, afterColumns - afterStats).ToArray(), - DataBlocks = dataBlocks, - Logical = logical, - Lval = buf.Slice(np, defEnd - np).ToArray(), - Continuations = continuations, - }; } - /// Removes a data index (its stats + data block at , - /// decrementing the data-ordinal reference (+0x08) of every remaining info block that pointed past it) - /// and every logical block matching (with its name). - private static void RemoveTdefBlocks(TdefParts parts, int? removeDataOrdinal, Func<(byte[] Info, byte[] Name), bool> removeLogical) - { - // The logical blocks go first, so what is left is exactly what has to survive the renumbering below. - parts.Logical.RemoveAll(b => removeLogical(b)); + /// The name text of a logical index's TDEF name entry. + private string NameOf(byte[] nameEntry) => + TableDefinition.ReadName(new PageBuffer(nameEntry, 0), 0, "logical index", _channel.Format).Name; - if (removeDataOrdinal is int ord) - { - // One data block can be named by more than one logical block — a primary key that also backs a - // relationship is the everyday case — and the renumbering below only moves references PAST the - // block being removed. A surviving reference EQUAL to it would silently come to name whichever - // index slid into the slot: same table, same file, an index quietly pointing at another index's - // B-tree. The callers guard against this by refusing to drop an index a relationship uses; this is - // the structural check behind that, so a route that ever gets here says so instead of writing it. - foreach ((byte[] survivor, byte[] name) in parts.Logical) - if (BinaryPrimitives.ReadInt32LittleEndian(survivor.AsSpan(0x08, 4)) == ord) - throw new InvalidOperationException( - $"Cannot remove index-data block {ord}: logical index '{NameOf(name)}' still refers to it."); - - parts.Stats.RemoveAt(ord); - parts.DataBlocks.RemoveAt(ord); - foreach ((byte[] info, _) in parts.Logical) - { - int num2 = BinaryPrimitives.ReadInt32LittleEndian(info.AsSpan(0x08, 4)); - if (num2 > ord) BinaryPrimitives.WriteInt32LittleEndian(info.AsSpan(0x08, 4), num2 - 1); - } - } - } /// Advances the object's MSysObjects.DateUpdate, leaving DateCreate where it is — /// measured: an ACE ALTER TABLE … ADD COLUMN moves the one and not the other. A no-op for an object @@ -3220,36 +2718,16 @@ private void TouchObject(int objectId) UpdateCatalogRows("MSysObjects", "Id", objectId, required: false, ("DateUpdate", DateTime.Now)); } - private void WriteTdef(int tdefPage, TdefParts parts) + private void WriteTdef(int tdefPage, TableDefinition.Parts parts) { - JetFormatBase format = _channel.Format; - var body = new List(format.PageSize); - body.AddRange(parts.Header); - foreach (byte[] s in parts.Stats) body.AddRange(s); - body.AddRange(parts.Columns); - foreach (byte[] d in parts.DataBlocks) body.AddRange(d); - foreach ((byte[] info, _) in parts.Logical) body.AddRange(info); - foreach ((_, byte[] nm) in parts.Logical) body.AddRange(nm); - body.AddRange(parts.Lval); - byte[] def = [.. body]; - int defEnd = def.Length; - - BinaryPrimitives.WriteInt32LittleEndian(def.AsSpan(format.TdefIndexCountOffset, 4), parts.DataBlocks.Count); - BinaryPrimitives.WriteInt32LittleEndian(def.AsSpan(format.TdefLogicalIndexCountOffset, 4), parts.Logical.Count); - BinaryPrimitives.WriteInt32LittleEndian(def.AsSpan(format.TdefLengthOffset, 4), defEnd); - - // Write across the first page and continuation pages as needed (fresh ones, the old released) — handles a - // definition that shrinks to one page, stays multi-page, or grows past a page (e.g. ADD COLUMN). - WriteDefinition(tdefPage, def, parts.Continuations, rewrite: true); - - // Every edit to an existing table's definition comes through here, which is why the catalog stamp does - // too: a column added, dropped, renamed or retyped, an index or relationship created or removed. + TableDefinition.WriteParts(_channel, _allocator, tdefPage, parts); + // Every definition edit also advances its catalog timestamp. TouchObject(tdefPage); } /// The data-block ordinal of a table's own index over the FK's referenced columns (for a /// self-reference — normally the primary key). - private static int ReferencedOrdinalIn(TableDef table, RelationshipSpec fk) => + private static int ReferencedOrdinalIn(TableDefinition table, RelationshipSpec fk) => FindParentKeyIndex(table.Indexes, fk.Columns.Select(c => c.ReferencedColumn).ToList(), table.Name) .RealIndexOrdinal; @@ -3279,144 +2757,6 @@ bool MatchesColumns(IndexDef ix) => : $"Referenced table '{parentTable}' has no index over ({string.Join(", ", refColumns)})."); } - /// The child (outgoing) end of a relationship: index_num2 = the child's own FK data block, - /// Fk_type = outgoing, Fk_number/Fk_table = the parent's incoming block. Mirrors the inline-FK block - /// TdefBuilder writes at creation time. - private static byte[] BuildOutgoingInfoBlock(int number, int dataOrdinal, byte fkType, int fkNumber, int fkTablePage, byte upd, byte del) - { - var b = new byte[IndexBlockFormat.InfoBlockSize]; - BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoMarkerOffset, 4), JetFormatBase.TdefRecordMarker); - BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoNumberOffset, 4), number); - BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoDataNumberOffset, 4), dataOrdinal); - b[IndexBlockFormat.InfoFkTypeOffset] = fkType; - BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoFkNumberOffset, 4), (uint)fkNumber); - BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoFkTablePageOffset, 4), fkTablePage); - b[IndexBlockFormat.InfoUpdateActionOffset] = upd; - b[IndexBlockFormat.InfoDeleteActionOffset] = del; - b[IndexBlockFormat.InfoTypeOffset] = IndexBlockFormat.TypeForeign; - return b; - } - - - /// Reads a table definition, stitching continuation pages into one contiguous buffer (in - /// the absolute coordinate space the descriptors use), and returns the continuation page numbers. - private (LibRed.IO.PageBuffer Buffer, IReadOnlyList ContinuationPages) ReadDefinition(int firstPage) - => TdefChainReader.Read(_channel, firstPage); - - /// - /// Writes a definition buffer across the first page and, if it overflows, continuation pages (each - /// [0x02][0x01][free:2][next:4] then data). The first page carries the whole definition in its - /// coordinate space; each continuation contributes -offset data. - /// Rewriting an existing definition () is done as ACE does it (verified by - /// whole-file diff, growing and shrinking): the first page is rewritten in place — alone, only the 8-byte - /// reserve past the new end is zeroed and older bytes beyond it are left — and continuation data always goes - /// to freshly allocated pages, while are released untouched. - /// - private void WriteDefinition(int firstPage, byte[] def, IReadOnlyList oldContinuations, bool rewrite) - { - JetFormatBase format = _channel.Format; - int ps = format.PageSize; - int nextOffset = format.TdefNextPageOffset; - - foreach (int old in oldContinuations) - _allocator.Release(old); // reusable only after this handle closes, as ACE holds them - - if (def.Length + JetFormatBase.TdefContinuationHeaderSize <= ps) - { - byte[] only = rewrite ? _channel.ReadPage(firstPage).Span.ToArray() : new byte[ps]; - def.CopyTo(only, 0); - only.AsSpan(def.Length, JetFormatBase.TdefContinuationHeaderSize).Clear(); // the reserve - BinaryPrimitives.WriteInt32LittleEndian(only.AsSpan(nextOffset, 4), 0); - BinaryPrimitives.WriteUInt16LittleEndian(only.AsSpan(format.TdefFreeSpaceOffset, 2), (ushort)(ps - def.Length - JetFormatBase.TdefContinuationHeaderSize)); - _channel.WritePage(firstPage, only); - return; - } - - // The chain holds the definition and then its 8-byte trailing reserve, as ACE lays it out (verified): every - // page is filled before the next begins, and the reserve follows the last definition byte, spilling onto a - // page of its own when it does not fit — so a continuation can hold reserve bytes and no definition. A - // 4,090-byte definition fills page 1 with 4,090 bytes and six of the reserve, and its continuation holds the - // other two, free 4,086. Each page's free space is what it has left once both are placed. - int maxMiddle = ps - JetFormatBase.TdefContinuationHeaderSize; - int stored = def.Length + JetFormatBase.TdefContinuationHeaderSize; - var chunks = new List<(int Offset, int Length, int Free)>(); - for (int consumed = ps; consumed < stored;) - { - int placed = Math.Min(maxMiddle, stored - consumed); - chunks.Add((consumed, Math.Clamp(def.Length - consumed, 0, placed), maxMiddle - placed)); - consumed += placed; - } - - // ACE allocates the last continuation first (verified: from free pages 354.. a two-page continuation became - // first → 355 → 354, and from the end of a file first → n+1 → n). - var pageNumbers = new int[chunks.Count]; - for (int i = chunks.Count - 1; i >= 0; i--) - pageNumbers[i] = _allocator.Allocate(); - - var page1 = new byte[ps]; - Array.Copy(def, 0, page1, 0, Math.Min(ps, def.Length)); // page 1 is completely full in a multi-page definition - BinaryPrimitives.WriteInt32LittleEndian(page1.AsSpan(nextOffset, 4), pageNumbers[0]); - BinaryPrimitives.WriteUInt16LittleEndian(page1.AsSpan(format.TdefFreeSpaceOffset, 2), 0); - _channel.WritePage(firstPage, page1); - - for (int i = 0; i < chunks.Count; i++) - { - var (offset, length, free) = chunks[i]; - var page = new byte[ps]; - PageHeader.WriteType(page, PageType.TableDefinition); - if (length > 0) // a page holding only the reserve starts past the definition's end - Array.Copy(def, offset, page, JetFormatBase.TdefContinuationHeaderSize, length); - int next = i + 1 < pageNumbers.Length ? pageNumbers[i + 1] : 0; - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(nextOffset, 4), next); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefFreeSpaceOffset, 2), (ushort)free); - _channel.WritePage(pageNumbers[i], page); - } - } - - private static byte[] BuildIndexDataBlock(List<(int Id, bool Ascending)> columns, int rootPage, int usageRow, int usagePage, - bool unique, bool required, bool ignoreNulls, bool complexColumn) - { - var b = new byte[IndexBlockFormat.DataBlockSize]; - System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0, 4), IndexBlockFormat.DataMarker); - for (int slot = 0; slot < IndexBlockFormat.MaxColumns; slot++) - { - int entry = IndexBlockFormat.ColumnsOffset + slot * IndexBlockFormat.ColumnSlotSize; - if (slot < columns.Count) - { - System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(b.AsSpan(entry, 2), (short)columns[slot].Id); - b[entry + 2] = columns[slot].Ascending ? IndexBlockFormat.ColumnAscending : (byte)0x00; // 0x00 = descending - } - else System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(b.AsSpan(entry, 2), IndexBlockFormat.ColumnUnused); - } - b[IndexBlockFormat.UsageMapRowOffset] = (byte)usageRow; - b[IndexBlockFormat.UsageMapRowOffset + 1] = (byte)usagePage; - b[IndexBlockFormat.UsageMapRowOffset + 2] = (byte)(usagePage >> 8); - b[IndexBlockFormat.UsageMapRowOffset + 3] = (byte)(usagePage >> 16); - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(IndexBlockFormat.RootPageOffset, 4), rootPage); - ushort flags = IndexFlags.AlwaysSet; - if (unique) flags |= IndexFlags.Unique; - if (ignoreNulls) flags |= IndexFlags.IgnoreNulls; - if (required) flags |= IndexFlags.Required; - if (complexColumn) flags |= IndexFlags.ComplexColumn; - System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(b.AsSpan(IndexBlockFormat.FlagsOffset, 2), flags); - return b; - } - - private static byte[] BuildPlainInfoBlock(int number, int dataOrdinal, bool isPrimary) - { - var b = new byte[IndexBlockFormat.InfoBlockSize]; - System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoMarkerOffset, 4), JetFormatBase.TdefRecordMarker); - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoNumberOffset, 4), number); - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoDataNumberOffset, 4), dataOrdinal); - System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoFkNumberOffset, 4), IndexBlockFormat.NoForeignKey); // no foreign key - b[IndexBlockFormat.InfoUpdateActionOffset] = IndexBlockFormat.PlainAction; - b[IndexBlockFormat.InfoDeleteActionOffset] = IndexBlockFormat.PlainAction; - b[IndexBlockFormat.InfoTypeOffset] = isPrimary ? IndexBlockFormat.TypePrimary : IndexBlockFormat.TypeSecondary; - return b; - } - - private static int ReadInt24(LibRed.IO.PageBuffer buf, int offset) => - buf.ReadByte(offset) | (buf.ReadByte(offset + 1) << 8) | (buf.ReadByte(offset + 2) << 16); /// /// Adds an incoming-relationship logical index-info block (§3.6) to a parent table's already-written @@ -3426,102 +2766,27 @@ private static int ReadInt24(LibRed.IO.PageBuffer buf, int offset) => /// private void AddIncomingRelationshipBlock(IncomingRelationship inc) { - TdefParts parts = ParseTdef(inc.ParentPage); // stitches continuation pages for a multi-page TDEF + TableDefinition.Parts parts = TableDefinition.ReadParts(_channel, inc.ParentPage); // stitches continuation pages for a multi-page TDEF // An incoming relationship adds a logical block and no data block, so this is the path a referenced // table overruns: 0x33 stays where it was while 0x2F climbs with every table that points here. EnsureIndexCapacity( - _catalog.Tables.FirstOrDefault(t => t.DefinitionPage == inc.ParentPage)?.Name ?? $"page {inc.ParentPage}", + _catalog.TableWithId(inc.ParentPage)?.Name ?? $"page {inc.ParentPage}", "an incoming relationship", parts.DataBlocks.Count, parts.Logical.Count + 1); string newName = HiddenRelationshipName(inc.Number); int k = parts.Logical.Count(b => string.Compare(NameOf(b.Name), newName, StringComparison.OrdinalIgnoreCase) < 0); // name-sorted, ignoring case - parts.Logical.Insert(k, (BuildIncomingInfoBlock(inc), EncodeName(newName))); + parts.Logical.Insert(k, (TableDefinition.BuildInfoBlock(_channel.Format, inc.Spec(newName)), TableDefinition.NameEntry(newName, _channel.Format))); WriteTdef(inc.ParentPage, parts); } - private static byte[] BuildIncomingInfoBlock(IncomingRelationship inc) - { - var b = new byte[IndexBlockFormat.InfoBlockSize]; - BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoMarkerOffset, 4), JetFormatBase.TdefRecordMarker); - BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoNumberOffset, 4), inc.Number); // index_num - BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoDataNumberOffset, 4), inc.ReferencedOrdinal); // index_num2 -> referenced-key data block - b[IndexBlockFormat.InfoFkTypeOffset] = FkTypeIncoming; - BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoFkNumberOffset, 4), inc.ChildBlockNumber); // cross-link to child block - BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(IndexBlockFormat.InfoFkTablePageOffset, 4), inc.ChildPage); - b[IndexBlockFormat.InfoUpdateActionOffset] = inc.UpdateAction; - b[IndexBlockFormat.InfoDeleteActionOffset] = inc.DeleteAction; - b[IndexBlockFormat.InfoTypeOffset] = IndexBlockFormat.TypeForeign; - return b; - } - - private static byte[] EncodeName(string name) - { - byte[] chars = System.Text.Encoding.Unicode.GetBytes(name); - var entry = new byte[2 + chars.Length]; - BinaryPrimitives.WriteUInt16LittleEndian(entry.AsSpan(0, 2), (ushort)chars.Length); - chars.CopyTo(entry.AsSpan(2)); - return entry; - } - /// An incoming block's hidden name: .r and the letter 'A' + index_num, as ACE names it /// (verified: 1 → .rB, 2 → .rC, 3 → .rD, in CREATE TABLE and ALTER TABLE alike). The /// name past Z is not measured. private static string HiddenRelationshipName(int number) => number is >= 0 and < 26 ? $".r{(char)('A' + number)}" : $".r{Guid.NewGuid():N}"[..8]; - /// Writes an empty B-tree leaf (no entries) to serve as a fresh index root. - private void WriteEmptyLeafIndexPage(JetFormatBase format, int pageNumber, int owner) - { - const int EntryDataOffset = 0x1E0; - const int OwnerOffset = 0x04; - - var page = new byte[format.PageSize]; - PageHeader.WriteType(page, PageType.LeafIndexPage); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(OwnerOffset, 4), owner); - // No entries: empty mask, no prefix compression, free space is the whole entry region. - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), - (ushort)(format.PageSize - EntryDataOffset)); - _channel.WritePage(pageNumber, page); - } - - /// - /// Writes a data page of empty inline usage-map records — like - /// Access does for a fresh table that has no data page yet. Each record is - /// [0x00][startPage = 0][all-zero bitmap]: row 0 = table owned-pages, row 1 = table - /// free-pages, and (with an index) row 2 = the index's owned-pages. The first insert allocates a - /// data page and sets the corresponding bit. - /// - private void WriteUsageMaps(JetFormatBase format, int pageNumber, int mapCount) - { - // An empty inline usage map: type byte + start page (0) + a bitmap of all-zero bytes. Access - // writes a full-width bitmap; match its record length so the page layout matches byte-for-byte. - const int MapLength = UsageMapRecordLength; - - var page = new byte[format.PageSize]; - PageHeader.WriteType(page, PageType.DataPage); - // Owner of a usage-map page is 0 (it belongs to no table). - - int offset = format.PageSize; - for (int row = 0; row < mapCount; row++) - { - offset -= MapLength; - // page[offset] already 0x00 (inline type), start page already 0, bitmap already zero. - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + row * 2, 2), (ushort)offset); - } - - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2), (ushort)mapCount); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), - (ushort)(offset - format.DataRowDirectoryOffset - mapCount * 2)); - _channel.WritePage(pageNumber, page); - } - - // Jet/ACE hard limit on columns in a table. - private const int MaxColumnsPerTable = 255; - - // An inline usage-map record: type byte + 4-byte start page + a 64-byte all-zero bitmap = 69 bytes. - private const int UsageMapRecordLength = 1 + 4 + 64; // A user table's owner + grantee SIDs — Users owns user objects; Users + Admin get the grants — read from // the file being written, since an on-disk SID is masked per file (page-00 §2.3). Baking in the pair this @@ -3548,19 +2813,8 @@ private void AddCatalogRow(string name, int tdefPage, PropertyBlob.WriteCheckList(checkConstraints))); // A table owning a complex column is flagged so — Access marks every one it writes. - new CatalogWriter(_channel, _catalog).AddObjectRow( - name, tdefPage, CatalogFormat.ObjectTypeTable, CatalogFormat.ObjectContainerParentId, - flags: ownsComplexColumns ? CatalogFormat.ObjectFlagOwnsComplexColumns : 0, props); - } - - // A new table's permission rows: what the Tables container grants what it creates (system-catalog §11). - private void AddPermissionRows(int objectId) => - new CatalogWriter(_channel, _catalog).AddPermissionRows(objectId, CatalogFormat.ObjectContainerParentId); - - private static void WriteInt24(byte[] buffer, int offset, int value) - { - buffer[offset] = (byte)value; - buffer[offset + 1] = (byte)(value >> 8); - buffer[offset + 2] = (byte)(value >> 16); + _catalog.AddObjectRow( + name, tdefPage, ObjectType.Table, JetCatalog.ObjectContainerParentId, + flags: (int)(ownsComplexColumns ? ObjectAttributes.OwnsComplexColumns : ObjectAttributes.None), props); } } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/StoredQuery.cs b/src/LibRed/LibRed.Core/Catalog/StoredQuery.cs new file mode 100644 index 00000000..f5350fb7 --- /dev/null +++ b/src/LibRed/LibRed.Core/Catalog/StoredQuery.cs @@ -0,0 +1,57 @@ +using LibRed.Formats; +using System.Buffers.Binary; + +namespace LibRed.Catalog; + +/// A stored query read back from the catalog. A view ( false) carries its SELECT in +/// , or null when it does not reconstruct. An action query — a CREATE PROCEDURE body that is not a +/// SELECT — carries its reconstructed, executable statement there when LibRed supports the kind; otherwise it is null +/// and explains why executing it throws. +public sealed record StoredQuery(string? Sql, string? UnsupportedReason, bool IsAction = true) +{ + /// The parameters the query declares, in declaration order; empty for one that declares none. + public IReadOnlyList Parameters { get; init; } = []; + + /// Flag value for a SELECT query. + internal const short QueryTypeSelect = 1; + + /// Flag bit marking an appended literal value. + internal const short AppendValueFlag = unchecked((short)0x8000); + + /// The Order value of a row: a 4-byte big-endian counter, kept per attribute. + internal static byte[] PackOrder(int order) + { + var bytes = new byte[sizeof(int)]; + BinaryPrimitives.WriteInt32BigEndian(bytes, order); + return bytes; + } + + /// A row's Order counter back out of its value — the inverse of ; 0 when + /// the row has none. + internal static int UnpackOrder(object? order) => + order is byte[] b && b.Length >= sizeof(int) ? BinaryPrimitives.ReadInt32BigEndian(b) : 0; + + /// + /// The LvExtra value for a declared parameter's facets, or null for a type that carries none. + /// Measured against ACE: a Text(50) parameter stores the length, 50; a Decimal(18,4) packs + /// both into one value, (scale << 16) | precision = 262162; and a sized Binary(10) + /// stores nothing at all, as every type without a declared size does. Access renders the query's + /// PARAMETERS clause from this, so a parameter with no value here reads back as Text(255). + /// + internal static int? PackParameterFacets(JetDataType type, int? size, int? scale) => type switch + { + JetDataType.Text => size, + JetDataType.FixedPoint when size is { } precision => (scale ?? 0) << 16 | precision & 0xFFFF, + _ => null, + }; + + /// A declared parameter's facets back out of its LvExtra — the inverse of + /// . A text parameter reports its length as the size; a decimal reports + /// the precision and scale packed into the one value. + internal static (int? Size, int? Precision, int? Scale) UnpackParameterFacets(JetDataType type, int? lvExtra) => + lvExtra is not { } packed ? (null, null, null) + : type == JetDataType.FixedPoint ? (null, packed & 0xFFFF, packed >> 16) + : type == JetDataType.Text ? (packed, null, null) + : (null, null, null); + +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/TableDef.cs b/src/LibRed/LibRed.Core/Catalog/TableDef.cs deleted file mode 100644 index 6534cf1b..00000000 --- a/src/LibRed/LibRed.Core/Catalog/TableDef.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace LibRed.Catalog; - -/// -/// The resolved definition of a table: its columns and indexes plus the page that -/// anchors its data. Produced by and consumed by the -/// storage layer to open a . -/// -public sealed class TableDef -{ - public required string Name { get; init; } - - /// Page number of the table's TDEF (definition) page. - public required int DefinitionPage { get; init; } - - public IReadOnlyList Columns { get; init; } = []; - public IReadOnlyList Indexes { get; init; } = []; - - /// Every name the TDEF gives an index, including the relationship names that share a real index - /// with a named one — see . holds one entry per real - /// index and so carries only the name that won. - public IReadOnlyList LogicalIndexes { get; init; } = []; - - /// - /// The variable-column count from the TDEF header (0x2B) — a high-water mark, not a live - /// count. It never decrements on DROP COLUMN, so it exceeds the number of variable columns still present - /// once one has been dropped, and it is the number of variable slots a row carries. - /// - /// - /// Carried on the table rather than derived from because the two differ exactly - /// where it matters. max(VariableIndex) + 1 over the live columns equals this until the LAST - /// variable column is dropped, and then falls below it — leaving a row one slot short of what the - /// definition says, which the ALTER COLUMN re-lay then appends onto and lands on the wrong index. - /// - public int VariableColumnCount { get; init; } - - /// The column-id high-water from the TDEF header (0x29) — how many ids the table has handed - /// out, which never decrements either. It is what a row's leading count and null-bitmap width span, so it - /// outlives a dropped column: a two-column table whose second column is dropped keeps writing rows that - /// count 2 (measured vs ACE). - public int ColumnIdHighWater { get; init; } - - /// The complex-type AutoNumber high-water (TDEF header 0x1C) — the next id for a complex - /// (multi-value/attachment) column. Carried for faithful round-trip; 0 for every table LibRed handles. - public int ComplexAutoNumber { get; init; } - - /// CHECK constraints (name, expression), read from the table's extended-properties - /// (LvProp) blob. Set by the catalog after the definition is decoded. - public IReadOnlyList<(string Name, string Expression)> CheckConstraints { get; internal set; } = []; - - /// The table's ValidationRule/ValidationText designer properties, read from the - /// extended-properties (LvProp) blob; null if none. Surfaced through - /// INFORMATION_SCHEMA.TABLES (VALIDATION_RULE/VALIDATION_TEXT), matching EFCore.Jet's - /// AdoxSchema.GetTables (Jet OLEDB:Table Validation Rule/Text). - public string? ValidationRule { get; internal set; } - - /// - public string? ValidationText { get; internal set; } - - /// The row count the TDEF header carries (0x10), which the engine keeps current as rows are - /// inserted and deleted. Reported as the table's cardinality in schema metadata, as Access reports it. - public int RowCount { get; init; } - - /// True for the MSys* system tables. - public bool IsSystem { get; init; } - - /// The object's raw MSysObjects.Flags, kept as read so callers can classify an object the - /// way Access does rather than by name — the system bit (0x80000000), the hidden bit (0x08) - /// and the bits that keep an object out of the schema rowsets altogether are all in here. Set by the - /// catalog after the definition is decoded; zero for a table built without one (a test's synthetic - /// definition, say). - public uint ObjectFlags { get; internal set; } - - /// - /// One per B-tree the table actually has: those with a root page, deduplicated by - /// it. Several logical indexes can name one index-data block — a primary key that also backs a - /// relationship is the everyday case — and they are the same tree, so anything that maintains index - /// entries must visit it once. Doing it per entry writes the same key twice. - /// - public IEnumerable RealIndexes => - Indexes.Where(i => i.RootPage > 0).GroupBy(i => i.RootPage).Select(g => g.First()); - - public ColumnDef? FindColumn(string name) => - Columns.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase)); - - /// The column, or the error naming the table that is missing it — the partner of - /// , for a column the caller's own correctness depends on rather than - /// one the schema may or may not have. - public ColumnDef RequireColumn(string name) => - FindColumn(name) ?? throw new InvalidOperationException($"'{Name}' is missing the '{name}' column."); -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/TableDefinition.cs b/src/LibRed/LibRed.Core/Catalog/TableDefinition.cs new file mode 100644 index 00000000..c24a1b35 --- /dev/null +++ b/src/LibRed/LibRed.Core/Catalog/TableDefinition.cs @@ -0,0 +1,1698 @@ +using LibRed.Formats; +using LibRed.IO; +using LibRed.Pages; +using LibRed.Storage; +using System.Buffers.Binary; +using System.Collections.ObjectModel; +using System.Text; + +namespace LibRed.Catalog; + +/// +/// The table definition: its decoded metadata, raw layout, construction and page-chain read/write rules. +/// Catalog properties enrich this same object; storage and the engine consume it directly. +/// +public sealed class TableDefinition : Page +{ + internal TableDefinition() { } + + public string Name { get; init; } = string.Empty; + + /// Page number of the table's TDEF (definition) page. + public int DefinitionPage + { + get => PageNumber; + init => PageNumber = value; + } + + /// CHECK constraints (name, expression), read from the table's extended-properties + /// (LvProp) blob. Set by the catalog after the definition is decoded. + public IReadOnlyList<(string Name, string Expression)> CheckConstraints + { + get => _checkConstraints; + internal set => _checkConstraints = Array.AsReadOnly(value.ToArray()); + } + + private IReadOnlyList<(string Name, string Expression)> _checkConstraints = []; + + /// The table's ValidationRule/ValidationText designer properties, read from the + /// extended-properties (LvProp) blob; null if none. Surfaced through + /// INFORMATION_SCHEMA.TABLES (VALIDATION_RULE/VALIDATION_TEXT), matching EFCore.Jet's + /// AdoxSchema.GetTables (Jet OLEDB:Table Validation Rule/Text). + public string? ValidationRule { get; internal set; } + + /// + public string? ValidationText { get; internal set; } + + /// True for the MSys* system tables. + public bool IsSystem { get; init; } + + /// The object's raw MSysObjects.Flags, kept as read so callers can classify an object the + /// way Access does rather than by name — the system bit, the hidden bit and the bits that keep an object out + /// of the schema rowsets altogether are all in here. Set by the catalog after the definition is decoded; + /// none for a table built without one (a test's synthetic definition, say). + public Formats.ObjectAttributes ObjectFlags { get; internal set; } + + /// + /// One per B-tree the table actually has: those with a root page, deduplicated by + /// it. Several logical indexes can name one index-data block — a primary key that also backs a + /// relationship is the everyday case — and they are the same tree, so anything that maintains index + /// entries must visit it once. Doing it per entry writes the same key twice. + /// + public IEnumerable RealIndexes => + Indexes.Where(i => i.RootPage > 0).GroupBy(i => i.RootPage).Select(g => g.First()); + + public ColumnDef? FindColumn(string name) => + Columns.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase)); + + /// The column, or the error naming the table that is missing it — the partner of + /// , for a column the caller's own correctness depends on rather than + /// one the schema may or may not have. + public ColumnDef RequireColumn(string name) => + FindColumn(name) ?? throw new InvalidOperationException($"'{Name}' is missing the '{name}' column."); + private static readonly Encoding StrictUnicode = new UnicodeEncoding( + bigEndian: false, byteOrderMark: false, throwOnInvalidBytes: true); + private readonly List _columns = []; + + public override PageType Type => PageType.TableDefinition; + + /// Marks this definition released, preserving its header and definition bytes. + internal static void MarkReleased(Span page) => PageHeader.WriteType(page, PageType.ReleasedTableDefinition); + + public int NextDefinitionPage { get; private set; } + public int RowCount { get; internal set; } + + /// The complex-type AutoNumber high-water (header 0x1C) — the next id for a complex + /// (multi-value/attachment) column. Read and carried for faithful round-trip; 0 for every table without + /// such a column (LibRed neither creates nor consumes complex columns). + public int ComplexAutoNumber { get; internal set; } + + public TableType TableType { get; private set; } + + /// The column-id high-water (header 0x29) — how many ids the table has handed out over its + /// lifetime, which never decrements. It is what sizes a row's leading count and null bitmap, so it differs + /// from for every table that has dropped a column or burned an id on a retype. + public int ColumnIdHighWater { get; internal set; } + + public int VariableColumnCount { get; internal set; } + public int ColumnCount { get; private set; } + public int LogicalIndexCount { get; private set; } + public int IndexCount { get; private set; } + + public IReadOnlyList Columns + { + get => _columnsView ??= _columns.AsReadOnly(); + init => _columns.AddRange(value); + } + + private readonly List _indexes = []; + public IReadOnlyList Indexes + { + get => _indexesView ??= _indexes.AsReadOnly(); + init => _indexes.AddRange(value); + } + + private readonly List _logicalIndexes = []; + /// Every logical index, in the order the TDEF lists them — including the relationship names that + /// share a real index with a named one, which keeps only one of. + public IReadOnlyList LogicalIndexes + { + get => _logicalIndexesView ??= _logicalIndexes.AsReadOnly(); + init => _logicalIndexes.AddRange(value); + } + + private readonly Dictionary _longValueOwnedMaps = []; + /// Per long-value (memo/OLE) column id → its owned-pages usage-map pointer (record row + + /// page), from the §3.3.2 list after the index names. Used to record a newly allocated LVAL page. + public IReadOnlyDictionary LongValueOwnedMaps => + _longValueOwnedView ??= new ReadOnlyDictionary(_longValueOwnedMaps); + + private readonly Dictionary _longValueFreeMaps = []; + /// Per long-value column id → its free-pages usage-map pointer (LVAL pages with spare room). + public IReadOnlyDictionary LongValueFreeMaps => + _longValueFreeView ??= new ReadOnlyDictionary(_longValueFreeMaps); + + private ReadOnlyCollection? _columnsView; + private ReadOnlyCollection? _indexesView; + private ReadOnlyCollection? _logicalIndexesView; + private ReadOnlyDictionary? _longValueOwnedView; + private ReadOnlyDictionary? _longValueFreeView; + + // Index structures follow the column names, in this order: + // IndexCount data blocks : columns, flags, root page + // LogicalIndexCount info blocks : links a name to a data block + // LogicalIndexCount names : 2-byte length + UTF-16 + // A logical index may be a relationship (FK) sharing a data block with a real index. The block layouts are + // the format's (JetFormatBase.IndexData* / IndexInfo*), shared with the writers. + + /// + /// Reads a table definition starting at , transparently + /// stitching continuation pages (wide tables whose definition spans multiple pages) + /// into one contiguous buffer before parsing. + /// + internal void Read(PageChannel channel, int page) + { + (PageBuffer buffer, _) = ReadChain(channel, page); + Read(buffer, channel.Format); + } + + internal override void Read(PageBuffer buffer, JetFormatBase format) + { + if (buffer.Length < format.TdefRealIndexBlockOffset) + throw new InvalidDataException( + $"TDEF buffer is {buffer.Length} bytes; the fixed header requires {format.TdefRealIndexBlockOffset}."); + int declaredLength = ReadLength(buffer.Span, format); + if (declaredLength < format.TdefRealIndexBlockOffset || declaredLength > buffer.Length) + throw new InvalidDataException( + $"TDEF declares length {declaredLength}, outside the available {buffer.Length}-byte buffer."); + if (declaredLength != buffer.Length) + buffer = new PageBuffer(buffer.Data[..declaredLength], buffer.PageNumber); + + PageNumber = buffer.PageNumber; + + NextDefinitionPage = buffer.ReadInt32(format.TdefNextPageOffset); + RowCount = ReadRowCount(buffer.Span, format); + ComplexAutoNumber = ReadComplexAutoNumber(buffer.Span, format); + TableType = (TableType)buffer.ReadByte(format.TdefTableTypeOffset); + (ColumnIdHighWater, VariableColumnCount) = ReadHighWaters(buffer.Span, format); + ColumnCount = buffer.ReadUInt16(format.TdefColumnCountOffset); + LogicalIndexCount = buffer.ReadInt32(format.TdefLogicalIndexCountOffset); + IndexCount = buffer.ReadInt32(format.TdefIndexCountOffset); + + int maxColumns = format.MaxColumnsPerTable, maxIndexes = format.MaxIndexesPerTable; + if (ColumnCount > maxColumns) + throw new InvalidDataException($"TDEF declares {ColumnCount} columns; a table can have at most {maxColumns}."); + if (VariableColumnCount > maxColumns) + throw new InvalidDataException( + $"TDEF declares a variable-column high-water of {VariableColumnCount}; the maximum is {maxColumns}."); + if (IndexCount < 0 || IndexCount > maxIndexes) + throw new InvalidDataException($"TDEF declares {IndexCount} real indexes; the valid range is 0 through {maxIndexes}."); + // Capped exactly as IndexCount is, and this is the check that matters: a table gains a logical block per + // INCOMING relationship without gaining a data block, so it overruns here while 0x33 stays legal. + // Previously only the sign was checked, which let a file written past the limit read back as sound - the + // one shape where LibRed produces a database Access reports as an unrecognized format while seeing + // nothing wrong with it itself. + if (LogicalIndexCount < 0 || LogicalIndexCount > maxIndexes) + throw new InvalidDataException( + $"TDEF declares {LogicalIndexCount} logical indexes; the valid range is 0 through {maxIndexes}."); + + // The column descriptors follow a per-index block sized by the REAL index count at + // 0x33 (IndexCount) — NOT the logical count at 0x2F (LogicalIndexCount). The two are + // equal for MSysObjects but differ for user tables (e.g. logical=2, real=1). + // The buffer here may already be a stitched multi-page definition (see Read(channel, page)). + Regions regions = Regions.Of(buffer.Span, format); + ReadColumns(buffer, format, regions.ColumnDescriptors); + ReadIndexes(buffer, format, regions); + } + + /// + /// Parses the index structures following the column names into s + /// (one per index-data block): columns + sort order, unique/primary flags, root page, + /// and the index name (resolved from the logical-index info blocks). + /// + private void ReadIndexes(PageBuffer buffer, JetFormatBase format, Regions regions) + { + _indexes.Clear(); + var byColumnId = _columns.ToDictionary(c => c.ColumnId); + + // 1. Index-data blocks (one IndexDef each): columns, unique flag, root page. + for (int i = 0; i < IndexCount; i++) + { + int block = regions.DataBlocks + i * format.IndexDataBlockSize; + + // Per-index statistics live in the real-index entries at TdefRealIndexBlockOffset. + int uniqueEntryCount = ReadIndexCounts(buffer.Span, format, i).Unique; + + var columns = new List<(ColumnDef Column, bool Ascending)>(); + for (int slot = 0; slot < format.IndexDataMaxColumns; slot++) + { + (short columnId, IndexColumnOrder order) = + ReadIndexSlot(buffer.Span.Slice(block, format.IndexDataBlockSize), format, slot); + if (columnId == JetFormatBase.IndexDataColumnUnused) continue; + // A used slot naming a column this table does not have is corruption, and a silent skip is the + // worst answer: the index reads back over FEWER columns than it was built on, so its keys are + // encoded differently from the ones on its pages and every seek quietly misses. + if (!byColumnId.TryGetValue(columnId, out ColumnDef? column)) + throw new InvalidDataException( + $"Index {i} of the table at page {buffer.PageNumber} names column id {columnId} in slot " + + $"{slot}, which the table does not have."); + columns.Add((column, (order & IndexColumnOrder.Ascending) != 0)); + } + + var flags = (IndexAttributes)buffer.ReadUInt16(block + format.IndexDataFlagsOffset); + _indexes.Add(new IndexDef + { + Name = string.Empty, + Columns = columns, + IsUnique = (flags & IndexAttributes.Unique) != 0, + IgnoreNulls = (flags & IndexAttributes.IgnoreNulls) != 0, + Required = (flags & IndexAttributes.Required) != 0, + Flags = flags, + IsPrimaryKey = false, + UniqueEntryCount = uniqueEntryCount, + RootPage = buffer.ReadInt32(block + format.IndexDataRootPageOffset), + UsageMap = buffer.ReadRecordPointer(block + format.IndexDataUsageMapOffset), + RealIndexOrdinal = i, + }); + } + + int afterIndexNames = ResolveIndexNames(buffer, regions, format); + ReadLongValueMaps(buffer, afterIndexNames, format); + } + + /// Slot of an index-data block: the column id it holds + /// ( for none) and its order byte, as + /// writes them. + internal static (short ColumnId, IndexColumnOrder Order) ReadIndexSlot(ReadOnlySpan block, JetFormatBase format, int slot) + { + int entry = format.IndexDataColumnsOffset + slot * format.IndexDataColumnSlotSize; + return (BinaryPrimitives.ReadInt16LittleEndian(block.Slice(entry, sizeof(short))), + (IndexColumnOrder)block[entry + format.IndexDataColumnOrderOffset]); + } + + /// Where real index 's entry starts in the real-index block that opens the + /// definition. + private static int RealIndexEntry(JetFormatBase format, int ordinal) => + format.TdefRealIndexBlockOffset + ordinal * format.RealIndexEntrySize; + + /// Real index 's two statistics: its total entry count, and its unique count — + /// cumulative, never decremented by Access on an insert. + internal static (int Total, int Unique) ReadIndexCounts(ReadOnlySpan tdef, JetFormatBase format, int ordinal) + { + int entry = RealIndexEntry(format, ordinal); + return (BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(entry + format.RealIndexRowCountOffset, sizeof(int))), + BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(entry + format.RealIndexUniqueCountOffset, sizeof(int)))); + } + + /// Writes real index 's two statistics — the inverse of + /// . + internal static void WriteIndexCounts(Span tdef, JetFormatBase format, int ordinal, int total, int unique) + { + int entry = RealIndexEntry(format, ordinal); + BinaryPrimitives.WriteInt32LittleEndian(tdef.Slice(entry + format.RealIndexRowCountOffset, sizeof(int)), total); + BinaryPrimitives.WriteInt32LittleEndian(tdef.Slice(entry + format.RealIndexUniqueCountOffset, sizeof(int)), unique); + } + + /// The definition's declared length (), continuation pages + /// included; written by . Unbounded — it comes out of the file. + internal static int ReadLength(ReadOnlySpan tdef, JetFormatBase format) => + BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(format.TdefLengthOffset, sizeof(int))); + + /// The header's two id high-waters, neither of which ever decrements: column ids handed out + /// (0x29, ) and variable-table slots handed out (0x2B, + /// ). + internal static (int ColumnIds, int VariableColumns) ReadHighWaters(ReadOnlySpan tdef, JetFormatBase format) => + (BinaryPrimitives.ReadUInt16LittleEndian(tdef.Slice(format.TdefMaxColumnsOffset, sizeof(ushort))), + BinaryPrimitives.ReadUInt16LittleEndian(tdef.Slice(format.TdefVariableColumnsOffset, sizeof(ushort)))); + + /// Writes the header's two id high-waters — the inverse of . + internal static void WriteHighWaters(Span tdef, JetFormatBase format, int columnIds, int variableColumns) + { + BinaryPrimitives.WriteUInt16LittleEndian(tdef.Slice(format.TdefMaxColumnsOffset, sizeof(ushort)), (ushort)columnIds); + BinaryPrimitives.WriteUInt16LittleEndian(tdef.Slice(format.TdefVariableColumnsOffset, sizeof(ushort)), (ushort)variableColumns); + } + + /// The header's row count (0x10). + internal static int ReadRowCount(ReadOnlySpan tdef, JetFormatBase format) => + BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(format.TdefRowCountOffset, sizeof(int))); + + /// Writes the header's row count — the inverse of . + internal static void WriteRowCount(Span tdef, JetFormatBase format, int count) => + BinaryPrimitives.WriteInt32LittleEndian(tdef.Slice(format.TdefRowCountOffset, sizeof(int)), count); + + /// The header's last-assigned AutoNumber (0x14): the next id is this plus the increment. + internal static int ReadLastAutoNumber(ReadOnlySpan tdef, JetFormatBase format) => + BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(format.TdefLastAutoNumberOffset, sizeof(int))); + + /// Writes the header's last-assigned AutoNumber — the inverse of . + internal static void WriteLastAutoNumber(Span tdef, JetFormatBase format, int value) => + BinaryPrimitives.WriteInt32LittleEndian(tdef.Slice(format.TdefLastAutoNumberOffset, sizeof(int)), value); + + /// The header's complex-type AutoNumber high-water (0x1C). + internal static int ReadComplexAutoNumber(ReadOnlySpan tdef, JetFormatBase format) => + BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(format.TdefComplexAutoNumberOffset, sizeof(int))); + + /// Writes the header's complex-type AutoNumber high-water — the inverse of + /// . + internal static void WriteComplexAutoNumber(Span tdef, JetFormatBase format, int value) => + BinaryPrimitives.WriteInt32LittleEndian(tdef.Slice(format.TdefComplexAutoNumberOffset, sizeof(int)), value); + + /// Bounds one variable-length TDEF region against the assembled definition, in long so a + /// file-sourced count cannot overflow the multiply back into range. bounds every + /// region with it, so a walk that reaches an index-data block to write it cannot land on the wrong block. + internal static int CheckedRegionEnd( + int start, int count, int itemSize, int bufferLength, string section) + { + long end = (long)start + (long)count * itemSize; + if (start < 0 || count < 0 || end < start || end > bufferLength) + throw new InvalidDataException( + $"TDEF {section} extend past the assembled definition ({start} + {count} * {itemSize} > {bufferLength})."); + return (int)end; + } + + /// Parses the §3.3.2 long-value column usage-map list (after the index names): one entry + /// {col_num:2, used_ptr:4, free_ptr:4} per memo/OLE column, then the terminator. Each pointer is a 1-byte + /// record row + 3-byte page. Captures the owned- (used-pages) map pointer. + private void ReadLongValueMaps(PageBuffer buffer, int pos, JetFormatBase format) + { + _longValueOwnedMaps.Clear(); + _longValueFreeMaps.Clear(); + var seen = new HashSet(); + while (true) + { + EnsureAvailable(buffer, pos, sizeof(ushort), "long-value map terminator"); + int colNum = buffer.ReadUInt16(pos); + if (colNum == JetFormatBase.TdefLongValueMapTerminator) + { + pos += sizeof(ushort); + if (pos != buffer.Length) + throw new InvalidDataException( + $"TDEF has {buffer.Length - pos} trailing bytes after the long-value map terminator."); + return; + } + + EnsureAvailable(buffer, pos, format.TdefLongValueMapEntrySize, "long-value map entry"); + ColumnDef? column = _columns.FirstOrDefault(c => c.ColumnId == colNum); + if (column is null) + throw new InvalidDataException($"TDEF long-value map references unknown column id {colNum}."); + // A calculated column also gets a map, whatever its declared type: its cached result is stored + // as an envelope in the variable section and spills to an LVAL page when it outgrows the row. + // ACE writes one for a calculated Memo declared as Text, which this guard used to reject — + // and because the catalog loads every TDEF, that made the whole database unopenable. + if (column.Type is not (JetDataType.Memo or JetDataType.Ole) && !column.IsCalculated) + throw new InvalidDataException( + $"TDEF long-value map references non-long-value column '{column.Name}' ({column.Type})."); + if (!seen.Add(colNum)) + throw new InvalidDataException($"TDEF contains duplicate long-value map entries for column id {colNum}."); + + column.HasLongValueMap = true; + _longValueOwnedMaps[colNum] = buffer.ReadRecordPointer(pos + format.TdefLongValueMapOwnedOffset); + _longValueFreeMaps[colNum] = buffer.ReadRecordPointer(pos + format.TdefLongValueMapFreeOffset); + pos += format.TdefLongValueMapEntrySize; + } + } + + /// + /// Reads the logical-index info blocks and their names, then attaches each name (and the + /// primary-key flag) to the index-data block it references. A data block may be referenced + /// by several logical indexes (e.g. a relationship plus the real index); the real index's + /// name wins over a foreign-key relationship's. + /// + private int ResolveIndexNames(PageBuffer buffer, Regions regions, JetFormatBase format) + { + int namePos = regions.IndexNames; + var priority = new int[_indexes.Count]; + _logicalIndexes.Clear(); + for (int i = 0; i < LogicalIndexCount; i++) // 0x2F — the logical-index (slot) count + { + (string name, namePos) = ReadName(buffer, namePos, $"logical index {i}", format); + LogicalIndexSpec info = ReadInfoBlock( + buffer.Span.Slice(regions.InfoBlocks + i * format.IndexInfoBlockSize, format.IndexInfoBlockSize), format, name); + + int dataNumber = info.DataOrdinal; + bool isRelationship = info.FkTablePage != 0; + // Every logical index names the data block that holds its columns and root page. One that names a + // block the table does not have is corruption; skipping it used to make the index vanish from the + // table's schema while its B-tree stayed on disk, maintained by nobody. + if (dataNumber < 0 || dataNumber >= _indexes.Count) + throw new InvalidDataException( + $"Logical index '{name}' of the table at page {buffer.PageNumber} names index-data block " + + $"{dataNumber}, and the table has {_indexes.Count}."); + + _logicalIndexes.Add(new LogicalIndexDef(name, dataNumber, isRelationship, + !isRelationship && info.Type == IndexInfoType.Primary, info.FkType, info.UpdateAction, info.DeleteAction)); + + // Prefer a real index name over a relationship's; prefer the primary among real ones. + int p = isRelationship ? 1 : info.Type == IndexInfoType.Primary ? 3 : 2; + if (p > priority[dataNumber]) + { + priority[dataNumber] = p; + _indexes[dataNumber] = _indexes[dataNumber] with + { + Name = name, + IsPrimaryKey = !isRelationship && info.Type == IndexInfoType.Primary, + }; + } + } + + return namePos; + } + + /// One index-info block (§3.6), the logical index called — every field + /// writes, so an edit reads a block here and writes it back there. + internal static LogicalIndexSpec ReadInfoBlock(ReadOnlySpan block, JetFormatBase format, string name) => + new(Number: BinaryPrimitives.ReadInt32LittleEndian(block.Slice(format.IndexInfoNumberOffset, sizeof(int))), + DataOrdinal: BinaryPrimitives.ReadInt32LittleEndian(block.Slice(format.IndexInfoDataNumberOffset, sizeof(int))), + FkType: (ForeignKeyType)block[format.IndexInfoFkTypeOffset], + FkNumber: BinaryPrimitives.ReadUInt32LittleEndian(block.Slice(format.IndexInfoFkNumberOffset, sizeof(uint))), + FkTablePage: BinaryPrimitives.ReadInt32LittleEndian(block.Slice(format.IndexInfoFkTablePageOffset, sizeof(int))), + UpdateAction: (RelationshipAction)block[format.IndexInfoUpdateActionOffset], + DeleteAction: (RelationshipAction)block[format.IndexInfoDeleteActionOffset], + Type: (IndexInfoType)block[format.IndexInfoTypeOffset], + Name: name); + + private void ReadColumns(PageBuffer buffer, JetFormatBase format, int columnBlock) + { + _columns.Clear(); + + // Pass 1: fixed-size column descriptors. + var descriptors = new (JetDataType Type, int ColumnId, ColumnFlags Flags, ColumnExtendedFlags ExtFlags, int FixedOffset, int Length, byte Precision, byte Scale, int VariableIndex, Collation Collation)[ColumnCount]; + var columnIds = new HashSet(); + for (int i = 0; i < ColumnCount; i++) + { + int entry = columnBlock + i * format.ColumnDescriptorSize; + var type = (JetDataType)buffer.ReadByte(entry + format.ColumnTypeOffset); + int columnId = buffer.ReadUInt16(entry + format.ColumnNumberOffset); + if (!Enum.IsDefined(type)) + throw new InvalidDataException($"TDEF column {i} has unknown type code 0x{(byte)type:X2}."); + if (columnId >= format.MaxColumnsPerTable) + throw new InvalidDataException( + $"TDEF column {i} has id {columnId}; valid ids are 0 through {format.MaxColumnsPerTable - 1}."); + if (!columnIds.Add(columnId)) + throw new InvalidDataException($"TDEF contains duplicate column id {columnId}."); + + // Bytes 0x0B/0x0C are precision/scale for a Decimal/Numeric column, the MSysComplexColumns key for a + // Complex one (page-02b §3.4), and the text-collation LANGID for everything else; 0x0D is the + // collation's sort id and 0x0E its sort-order version. Read whichever applies — a complex column has + // no collation to read. + bool numeric = type == JetDataType.FixedPoint; + bool complex = type == JetDataType.Complex; + // The variable-table index is **stored** in the descriptor (0x07), not derived. Reading it + // (rather than ranking column ids) is what lets a table with a **dropped column** decode: + // ACE's DROP COLUMN removes a descriptor but does NOT renumber the survivors or rewrite + // rows, so a survivor keeps its original variable index even though ranking would shift it. + // 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). + descriptors[i] = ( + type, + columnId, + (ColumnFlags)buffer.ReadByte(entry + format.ColumnFlagsOffset), + (ColumnExtendedFlags)buffer.ReadByte(entry + format.ColumnExtendedFlagsOffset), + buffer.ReadUInt16(entry + format.ColumnFixedOffsetOffset), + buffer.ReadUInt16(entry + format.ColumnLengthOffset), + numeric ? buffer.ReadByte(entry + format.ColumnPrecisionOffset) : (byte)0, + numeric ? buffer.ReadByte(entry + format.ColumnScaleOffset) : (byte)0, + buffer.ReadUInt16(entry + format.ColumnVariableIndexOffset), + numeric || complex ? Collation.GeneralLegacy + : new Collation((CollatingOrder)buffer.ReadUInt16(entry + format.ColumnLocaleOffset), + buffer.ReadByte(entry + format.ColumnCollationVersionOffset), + buffer.ReadByte(entry + format.ColumnCollationSortIdOffset))); + } + + // Pass 2: column names, in the same order, immediately after the descriptor block. + // Each name is a 2-byte (little-endian) byte length followed by UTF-16LE text. + int namePos = columnBlock + ColumnCount * format.ColumnDescriptorSize; + for (int i = 0; i < ColumnCount; i++) + { + (string name, namePos) = ReadName(buffer, namePos, $"column {i}", format); + + var d = descriptors[i]; + bool isFixed = (d.Flags & ColumnFlags.FixedLength) != 0; + if ((!isFixed && d.VariableIndex >= VariableColumnCount) + || (isFixed && d.VariableIndex > VariableColumnCount)) + throw new InvalidDataException( + $"TDEF column '{name}' has variable-table index {d.VariableIndex}, " + + $"outside high-water {VariableColumnCount}."); + _columns.Add(new ColumnDef + { + Name = name, + Type = d.Type, + Index = i, + ColumnId = d.ColumnId, + Length = d.Length, + FixedOffset = d.FixedOffset, + VariableIndex = isFixed ? -1 : d.VariableIndex, + // Byte 7 is stored on fixed columns too (the running count of preceding variable columns). + VariableTableIndex = d.VariableIndex, + IsFixedLength = isFixed, + IsAutoNumber = (d.Flags & ColumnFlags.AutoNumber) != 0, + // The user-column flag bits (0x0F: updatable/GUID-autonumber/hyperlink; 0x10: compressed-Unicode / + // calculated). The catalog and attachment bits are only ever written, from a created column's spec. + IsUpdatable = (d.Flags & ColumnFlags.Updatable) != 0, + IsGuidAutoNumber = (d.Flags & ColumnFlags.GuidAutoNumber) != 0, + IsHyperlink = (d.Flags & ColumnFlags.Hyperlink) != 0, + SupportsCompressedUnicode = (d.ExtFlags & ColumnExtendedFlags.CompressedUnicode) != 0, + IsCalculated = (d.ExtFlags & ColumnExtendedFlags.Calculated) != 0, + Precision = d.Precision, + Scale = d.Scale, + Collation = d.Collation, + // The descriptor's bytes as read, fields LibRed does not model included. + RawDescriptor = buffer.Slice(columnBlock + i * format.ColumnDescriptorSize, format.ColumnDescriptorSize).ToArray(), + }); + } + + // AutoNumber seed/increment from the TDEF header: 0x18 = increment, 0x14 = last-assigned value. On a + // freshly created table the last value is Seed-Increment, so Seed = last + increment (matching what a + // no-insert scaffold reports). + // + // At most one column draws on THAT pair — but it is not the only counter a table has. A complex column + // carries the very same 0x04 flag and is allocated from 0x1C (ComplexAutoNumber), so a table can hold an + // ordinary counter and any number of complex columns all reading IsAutoNumber (complex1.accdb's Table1 + // has five). Applying the header pair to those too reports a seed and increment that describe a + // different counter, so skip them: their high-water is the table's ComplexAutoNumber. + int increment = buffer.ReadInt32(format.TdefAutoNumberIncrementOffset); + int lastAuto = ReadLastAutoNumber(buffer.Span, format); + foreach (ColumnDef column in _columns) + if (column.IsAutoNumber && column.Type != JetDataType.Complex) + { + // Zero is not a counter ACE will write: its own DDL refuses COUNTER(seed, 0) with "Invalid + // argument", because every row would take the same id. A table carrying one is damaged, and + // reading it as 1 — which this did — hides that behind a counter that looks ordinary. + if (increment == 0) + throw new InvalidDataException( + $"The table at page {buffer.PageNumber} has AutoNumber column '{column.Name}' and an " + + "increment of 0, which would hand every row the same id."); + column.Increment = increment; + column.Seed = lastAuto + increment; + } + } + + /// Where the name entry at ends: its byte length + /// (), then that many bytes of UTF-16LE text. Bounded against + /// , since the length comes out of the file. + internal static int NameEntryEnd(ReadOnlySpan tdef, int pos, JetFormatBase format, string kind) + { + int text = CheckedRegionEnd(pos, 1, format.TdefNameLengthSize, tdef.Length, $"{kind} name length"); + return CheckedRegionEnd(text, 1, + BinaryPrimitives.ReadUInt16LittleEndian(tdef.Slice(pos, format.TdefNameLengthSize)), tdef.Length, $"{kind} name"); + } + + /// Reads the name entry at and returns its text and where the entry + /// ends. + internal static (string Name, int Next) ReadName(PageBuffer buffer, int pos, string kind, JetFormatBase format) + { + int end = NameEntryEnd(buffer.Span, pos, format, kind); + pos += format.TdefNameLengthSize; + int byteLength = end - pos; + if (byteLength == 0 || byteLength > format.MaxNameBytes || (byteLength & 1) != 0) + throw new InvalidDataException( + $"TDEF {kind} name has invalid UTF-16 byte length {byteLength}; expected an even value from 2 through {format.MaxNameBytes}."); + try + { + return (StrictUnicode.GetString(buffer.Slice(pos, byteLength)), end); + } + catch (DecoderFallbackException ex) + { + throw new InvalidDataException($"TDEF {kind} name is not valid UTF-16LE.", ex); + } + } + + private static void EnsureAvailable(PageBuffer buffer, int pos, int length, string section) + { + long end = (long)pos + length; + if (pos < 0 || length < 0 || end > buffer.Length) + throw new InvalidDataException( + $"TDEF {section} extends past the declared definition ({pos} + {length} > {buffer.Length})."); + } + + /// + /// One logical index-info block (§3.6). Several logical indexes may share a data block: a plain + /// index has one, and a relationship adds one that reuses this table's side of the foreign key. + /// is the data-block index (index_num2); + /// is the logical id (index_num). For a relationship, says which end this + /// is, is the other end's index_num, and + /// is the other table's TDEF page. + /// + internal sealed record LogicalIndexSpec( + int Number, + int DataOrdinal, + ForeignKeyType FkType, + uint FkNumber, + int FkTablePage, + RelationshipAction UpdateAction, + RelationshipAction DeleteAction, + IndexInfoType Type, + string Name) + { + /// The block of a plain index — one that is not a relationship's. + public static LogicalIndexSpec Plain(int number, int dataOrdinal, bool isPrimary, string name) => + new(number, dataOrdinal, ForeignKeyType.None, JetFormatBase.IndexInfoNoForeignKey, FkTablePage: 0, + RelationshipAction.NotRelationship, RelationshipAction.NotRelationship, + isPrimary ? IndexInfoType.Primary : IndexInfoType.Secondary, name); + } + + internal sealed record Result(byte[] Page, IReadOnlyList Columns); + + /// The database's collating order, which every non-numeric column this + /// builds inherits — it is what decides how their index keys are encoded. Required, and deliberately not + /// defaulted: the old ?? GeneralLegacy fallback was correct only for a database that happens to use + /// General-Legacy, and silently wrote v0 columns into a v1 file for every caller that forgot it. + /// The on-disk format to build for. + /// User or system. + /// The columns, in creation order. + /// The table's indexes, or null for none. + /// The memo/OLE columns' usage-map pointers. + /// Explicit logical-index blocks, or null to derive them from the indexes. + /// The page holding the table's own usage maps — owned pages at row 0, free pages + /// at row 1 — or null to leave the header's two map pointers zero. + internal static Result Build( + JetFormatBase format, + TableType tableType, + IReadOnlyList specs, + Collation collation, + IReadOnlyList? indexes = null, + IReadOnlyList? longValueColumns = null, + IReadOnlyList? logicalIndexes = null, + int? usageMapPage = null) + { + indexes ??= []; + longValueColumns ??= []; + ValidateColumnSpecs(format, specs); + // Jet/ACE caps a table at 32 indexes, counting those backing primary keys, unique constraints + // and relationships (§3.5 index-data blocks, the 0x33 count). Reject rather than write a bad TDEF. + if (indexes.Count > format.MaxIndexesPerTable) + throw new NotSupportedException( + $"Table has {indexes.Count} indexes; a table can have at most {format.MaxIndexesPerTable} (including those backing keys and relationships)."); + var columns = ResolveColumns(format, specs, collation); + IReadOnlyList logical = logicalIndexes + ?? indexes.Select((ix, i) => LogicalIndexSpec.Plain(i, i, ix.IsPrimaryKey, ix.Name)).ToList(); + ValidateIndexAndLongValueSpecs(format, columns, indexes, logical, longValueColumns); + + int definitionSize = DefinitionSize(format, columns, indexes, logical, longValueColumns); + var page = new byte[Math.Max(format.PageSize, definitionSize)]; + + PageHeader.WriteType(page, PageType.TableDefinition); + BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.TdefRecordMarkerOffset, 4), JetFormatBase.TdefRecordMarker); + // AutoNumber (COUNTER) config lives in the TDEF header: 0x18 = increment (default 1), and 0x14 = + // the last-assigned value initialized to Seed-Increment so the first insert yields Seed. A table has + // at most one AutoNumber column; with none, these stay at the plain-counter defaults (increment 1, + // last 0). Verified vs ACE (COUNTER(1000, 7) → 0x18=7, 0x14=993). + // "At most one" is enforced, not assumed: the header holds a single seed/increment pair, so a second + // AutoNumber column would be written with the 0x04 flag set and no counter configuration of its own — + // two columns then claiming one header counter at insert. ALTER's promote path already refuses this; + // CREATE silently took the first and ignored the rest. + // + // Complex columns are exempt, and are not a second claimant: they carry the same 0x04 flag but are + // allocated from 0x1C, not from this pair. Counting them would refuse a table that has an ordinary + // counter beside a complex column — complex1.accdb's Table1 has one of each kind — and could hand + // `counter` a complex spec whose seed/increment describe nothing. + var counters = specs.Where(s => s.IsAutoNumber && s.Type != JetDataType.Complex).ToList(); + if (counters.Count > 1) + throw new NotSupportedException( + $"A table may have only one AutoNumber column; {string.Join(", ", counters.Select(c => $"'{c.Name}'"))} are all declared as one."); + ColumnSpec? counter = counters.FirstOrDefault(); + if (counter is { Increment: 0 }) throw ZeroIncrement(counter.Name); + WriteCounter(page, format, counter?.Seed ?? 1, counter?.Increment ?? 1); + // Complex-type AutoNumber high-water (0x1C): 0 for a new table, which has no complex column. + WriteComplexAutoNumber(page, format, 0); + BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefNextPageOffset, 4), 0); + WriteRowCount(page, format, 0); + page[format.TdefTableTypeOffset] = (byte)tableType; + if (usageMapPage is int maps) + { + PageBuffer.WriteRecordPointer(page, format.TdefOwnedPagesOffset, row: 0, maps); + PageBuffer.WriteRecordPointer(page, format.TdefFreePagesOffset, row: 1, maps); + } + // The 0x29 high-water is the next column id to hand out = max existing id + 1. For contiguous ids this + // equals the column count. + int maxColumnId = columns.Select(c => c.ColumnId).DefaultIfEmpty(-1).Max(); + WriteHighWaters(page, format, maxColumnId + 1, columns.Count(c => !c.IsFixedLength)); + + // The per-index statistics blocks (12 bytes each, one per data block) precede the columns. + int columnBlock = format.TdefRealIndexBlockOffset + indexes.Count * format.RealIndexEntrySize; + WriteColumnDescriptors(page, format, columns, columnBlock); + int afterNames = WriteColumnNames(page, format, columns, columnBlock + columns.Count * format.ColumnDescriptorSize); + + int definitionEnd = WriteIndexes(page, format, columns, indexes, logical, longValueColumns, afterNames); + if (definitionEnd != definitionSize) + throw new InvalidOperationException( + $"TDEF sizing preflight calculated {definitionSize} bytes but serialization wrote {definitionEnd}."); + + WriteCounts(page, format, columns.Count, indexes.Count, logical.Count, definitionEnd); + // Remaining free space (Access reserves an 8-byte continuation header). For a multi-page definition the + // caller recomputes the first page's free space, so clamp at 0 here. + BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefFreeSpaceOffset, 2), + (ushort)Math.Max(0, format.PageSize - definitionEnd - format.TdefContinuationHeaderSize)); + + return new Result(page, columns); + } + + /// The precision written to the descriptor. A declared 0 resolves to 18 — ACE's default for + /// a bare DECIMAL, and AccessTypeMapper's — because a 0 on disk leaves the value with no + /// declared shape and ACE cannot materialise such a column (AceCoreApiShapeProbeTests, + /// decimal-precision-zero). FixedPoint only: elsewhere those bytes are the LANGID. + internal static byte EffectivePrecision(ColumnSpec spec) => + spec.Type == JetDataType.FixedPoint && spec.Precision == 0 + ? DefaultNumericPrecision + : spec.Precision; + + /// What ACE declares for a bare DECIMAL or NUMERIC — measured, not assumed + /// (AceDecimalDeclarationProbeTest), and the SQL front end's default too. ACE never writes precision + /// 0: none of the 127 FixedPoint columns across 36 fixtures carries one. + private const byte DefaultNumericPrecision = 18; + + /// A NUMERIC/DECIMAL column's declared precision and scale, which nothing else checked — the width + /// check covers only the fixed 17 bytes. AccessTypeMapper enforces this on the SQL path; a direct + /// Core caller bypasses it. Precision 0 means "unspecified" (see ), not zero + /// digits, so only a positively wrong declaration is refused. + internal static void ValidateNumericPrecision(ColumnSpec spec) + { + if (spec.Type is not JetDataType.FixedPoint || spec.Precision == 0) return; + + if (spec.Precision > Storage.Types.JetTypeCodec.MaxNumericPrecision) + throw new NotSupportedException( + $"Column '{spec.Name}' declares NUMERIC precision {spec.Precision}; the maximum precision is " + + $"{Storage.Types.JetTypeCodec.MaxNumericPrecision}."); + + if (spec.Scale > spec.Precision) + throw new NotSupportedException( + $"Column '{spec.Name}' declares NUMERIC scale {spec.Scale} under precision {spec.Precision}; " + + "the scale cannot exceed the precision — that would be more digits after the decimal point " + + "than the column has in total."); + } + + private static void ValidateColumnSpecs(JetFormatBase format, IReadOnlyList specs) + { + if (specs.Count > format.MaxColumnsPerTable) + throw new NotSupportedException( + $"Table has {specs.Count} columns; a table can have at most {format.MaxColumnsPerTable}."); + + var ids = new HashSet(); + var names = new HashSet(StringComparer.OrdinalIgnoreCase); + long fixedBytes = 0; + int variableColumns = 0, highWater = -1; + for (int i = 0; i < specs.Count; i++) + { + ColumnSpec spec = specs[i]; + ValidateNameLength(spec.Name, "Column", format); + if (!names.Add(spec.Name)) + throw new NotSupportedException($"Column name '{spec.Name}' is used more than once."); + int id = spec.ColumnId ?? i; + if (id < 0 || id >= format.MaxColumnsPerTable) + throw new NotSupportedException( + $"Column '{spec.Name}' has id {id}; column ids range from 0 through {format.MaxColumnsPerTable - 1}."); + if (!ids.Add(id)) + throw new NotSupportedException($"Column id {id} is used more than once."); + if (spec.Length is < 0 or > ushort.MaxValue) + throw new NotSupportedException( + $"Column '{spec.Name}' has byte length {spec.Length}, which does not fit the TDEF field."); + RowCodec.ValidateFieldWidth(spec.Name, spec.Type, spec.Length); + JetDataTypeVersions.EnsureStorable(spec.Type, format.Version, spec.Name); + ValidateNumericPrecision(spec); + if (spec.IsFixedLength && spec.Type != JetDataType.Boolean) + fixedBytes += spec.Length; + if (!spec.IsFixedLength) variableColumns++; + highWater = Math.Max(highWater, id); + } + + // The fixed region used to be checked only against the TDEF's 2-byte offset fields (65535). ACE's real + // limit is far tighter — the widest record the declaration allows must still be storable — and it + // subsumes that one, since no column may now exceed 510 bytes. Without this a plain CreateTable of + // 252 GUID columns writes a database Access will not open at all. + RowCodec.ValidateRecordFits(null, (int)fixedBytes, variableColumns, highWater + 1, format); + } + + private static void ValidateIndexAndLongValueSpecs( + JetFormatBase format, + IReadOnlyList columns, + IReadOnlyList indexes, + IReadOnlyList logical, + IReadOnlyList longValueColumns) + { + if (logical.Count > format.MaxIndexesPerTable) + throw new NotSupportedException( + $"Table has {logical.Count} logical indexes; a table can have at most {format.MaxIndexesPerTable}."); + + var columnByName = columns.ToDictionary(c => c.Name, StringComparer.OrdinalIgnoreCase); + foreach (IndexSpec index in indexes) + { + ValidateNameLength(index.Name, "Index", format); + // The index-data block (§3.5) has a fixed array of column slots and no count field, so an index — + // hence any PRIMARY KEY / UNIQUE / FOREIGN KEY — spans at most that many columns. Reject an + // over-wide index rather than silently truncating it. + if (index.Columns.Count > format.IndexDataMaxColumns) + throw new NotSupportedException( + $"Index '{index.Name}' spans {index.Columns.Count} columns; an index, and a key built on one, can span at most {format.IndexDataMaxColumns}."); + foreach (string column in index.Columns) + if (!columnByName.ContainsKey(column)) + throw new NotSupportedException($"Index '{index.Name}' refers to unknown column '{column}'."); + ValidateUsageMapPointer(index.UsageMapRow, index.UsageMapPage, $"index '{index.Name}'", allowNull: true); + } + // Duplicate index names, on the create path. ACE rejects them, and every downstream lookup resolves an + // index by name, so two blocks sharing one would make DROP INDEX remove an arbitrary one of them. The + // column list has had this check all along (see below); the index list had only a length check. + var indexNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (LogicalIndexSpec index in logical) + { + ValidateNameLength(index.Name, "Logical index", format); + if (!indexNames.Add(index.Name)) + throw new NotSupportedException($"Index name '{index.Name}' is used more than once."); + } + + var columnById = columns.ToDictionary(c => c.ColumnId); + var seen = new HashSet(); + foreach (LongValueColumnSpec value in longValueColumns) + { + if (!seen.Add(value.ColumnId)) + throw new NotSupportedException($"Long-value column id {value.ColumnId} has more than one usage-map entry."); + // A calculated column with a Memo result is the exception: ACE declares it Text with length 0 and + // still gives it long-value maps, so the declared type does not decide this. The reader had to + // drop the same assumption — a guard that kept it rejected the TDEF, and since the catalog loads + // every TDEF that made the whole database unopenable (§3.4a). + if (!columnById.TryGetValue(value.ColumnId, out ColumnDef? column) + || (column.Type is not (JetDataType.Memo or JetDataType.Ole) && !column.IsCalculated)) + throw new NotSupportedException( + $"Long-value usage-map entry {value.ColumnId} does not identify a Memo/OLE column."); + ValidateUsageMapPointer(value.UsedRow, value.MapPage, $"long-value column '{column.Name}' owned map", allowNull: false); + ValidateUsageMapPointer(value.FreeRow, value.MapPage, $"long-value column '{column.Name}' free map", allowNull: false); + } + } + + private static void ValidateUsageMapPointer(int row, int page, string owner, bool allowNull) + { + if (allowNull && row == 0 && page == 0) return; + if (row is < 0 or > byte.MaxValue || page is <= 0 or > 0xFFFFFF) + throw new NotSupportedException( + $"The {owner} usage-map pointer ({row}, {page}) does not fit its 1-byte row / 3-byte page fields."); + } + + private static void ValidateNameLength(string name, string kind, JetFormatBase format) + { + int length = Encoding.Unicode.GetByteCount(name); + if (length == 0 || length > format.MaxNameBytes) + throw new NotSupportedException( + $"{kind} name is {length} UTF-16 bytes; a name must be 1 through {format.MaxNameBytes / 2} characters."); + } + + private static int DefinitionSize( + JetFormatBase format, + List columns, + IReadOnlyList indexes, + IReadOnlyList logical, + IReadOnlyList longValueColumns) + { + long size = format.TdefRealIndexBlockOffset + + (long)indexes.Count * format.RealIndexEntrySize + + (long)columns.Count * format.ColumnDescriptorSize + + columns.Sum(c => (long)format.TdefNameLengthSize + Encoding.Unicode.GetByteCount(c.Name)) + + (long)indexes.Count * format.IndexDataBlockSize + + (long)logical.Count * format.IndexInfoBlockSize + + logical.Sum(i => (long)format.TdefNameLengthSize + Encoding.Unicode.GetByteCount(i.Name)) + + (long)longValueColumns.Count * format.TdefLongValueMapEntrySize + + sizeof(ushort); // the terminator + if (size > MaxDefinitionLength) + throw new NotSupportedException( + $"The serialized table definition requires {size} bytes; LibRed's validated TDEF budget is {MaxDefinitionLength}."); + return (int)size; + } + + /// Writes the index structures and returns the offset just past them (the definition end). + private static int WriteIndexes(byte[] page, JetFormatBase format, List columns, IReadOnlyList indexes, IReadOnlyList logical, IReadOnlyList longValueColumns, int dataBlockStart) + { + var columnIdByName = columns.ToDictionary(c => c.Name, c => c.ColumnId, StringComparer.OrdinalIgnoreCase); + + // 1. Index-data blocks: columns, root page, unique flag. + for (int i = 0; i < indexes.Count; i++) + { + IndexSpec index = indexes[i]; + BuildDataBlock(format, [.. index.Columns.Select(c => (columnIdByName[c], Ascending: true))], + index.RootPage, index.UsageMapRow, index.UsageMapPage, + index.IsUnique, ignoreNulls: false, required: index.IsPrimaryKey, complexColumn: false) + .CopyTo(page, dataBlockStart + i * format.IndexDataBlockSize); + } + + // 2. Index-info blocks (one per logical index) and 3. their names. Without explicit logical + // specs each data block maps 1:1 to a plain info block (back-compat); with them, relationship + // blocks are included and stored name-sorted (matching Access). + int infoStart = dataBlockStart + indexes.Count * format.IndexDataBlockSize; + for (int i = 0; i < logical.Count; i++) + BuildInfoBlock(format, logical[i]).CopyTo(page, infoStart + i * format.IndexInfoBlockSize); + + int namePos = infoStart + logical.Count * format.IndexInfoBlockSize; + foreach (LogicalIndexSpec li in logical) + { + byte[] entry = NameEntry(li.Name, format); + entry.CopyTo(page, namePos); + namePos += entry.Length; + } + + // After the index names comes a per-long-value-column (memo/OLE) usage-map list (spec §3.3.2): + // one entry {col_num:2, used_pages:4, free_pages:4} per column, in ascending column order, then the + // terminator. Each pointer is a 1-byte usage-map row + 3-byte page. + foreach (LongValueColumnSpec lv in longValueColumns.OrderBy(l => l.ColumnId)) + { + LongValueMapEntry(format, lv.ColumnId, lv.UsedRow, lv.FreeRow, lv.MapPage).CopyTo(page, namePos); + namePos += format.TdefLongValueMapEntrySize; + } + + BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, 2), JetFormatBase.TdefLongValueMapTerminator); + namePos += sizeof(ushort); + return namePos; + } + + /// A fresh index-data block (§3.5): the marker, a slot per column with its order and the rest unused, + /// the index's B-tree (), and its flags — always , + /// and the bit for each of , , + /// and (an index over a complex column carries it; every one Access writes does). + /// + internal static byte[] BuildDataBlock(JetFormatBase format, IReadOnlyList<(int Id, bool Ascending)> columns, + int rootPage, int usageRow, int usagePage, bool unique, bool ignoreNulls, bool required, bool complexColumn) + { + var block = new byte[format.IndexDataBlockSize]; + BinaryPrimitives.WriteUInt32LittleEndian(block, JetFormatBase.IndexDataMarker); + for (int slot = 0; slot < format.IndexDataMaxColumns; slot++) + { + if (slot < columns.Count) + WriteIndexSlot(block, format, slot, columns[slot].Id, + columns[slot].Ascending ? IndexColumnOrder.Ascending : IndexColumnOrder.Descending); + else + WriteIndexSlot(block, format, slot, JetFormatBase.IndexDataColumnUnused, order: 0); + } + WriteIndexTree(block, format, rootPage, usageRow, usagePage); + IndexAttributes flags = IndexAttributes.AlwaysSet + | (unique ? IndexAttributes.Unique : IndexAttributes.None) + | (ignoreNulls ? IndexAttributes.IgnoreNulls : IndexAttributes.None) + | (required ? IndexAttributes.Required : IndexAttributes.None) + | (complexColumn ? IndexAttributes.ComplexColumn : IndexAttributes.None); + BinaryPrimitives.WriteUInt16LittleEndian(block.AsSpan(format.IndexDataFlagsOffset, sizeof(ushort)), (ushort)flags); + return block; + } + + /// Writes slot of an index-data block: the column it holds + /// ( for none) and its order byte — what + /// reads. + internal static void WriteIndexSlot(Span block, JetFormatBase format, int slot, int columnId, IndexColumnOrder order) + { + int entry = format.IndexDataColumnsOffset + slot * format.IndexDataColumnSlotSize; + BinaryPrimitives.WriteInt16LittleEndian(block.Slice(entry, sizeof(short)), (short)columnId); + block[entry + format.IndexDataColumnOrderOffset] = (byte)order; + } + + /// Writes where an index-data block's B-tree is: its root page, and the (row, page) pointer to the index's + /// own pages usage map — in a fresh block, or over an existing one when an index is rebuilt onto a fresh root. + internal static void WriteIndexTree(Span block, JetFormatBase format, int rootPage, int usageRow, int usagePage) + { + PageBuffer.WriteRecordPointer(block, format.IndexDataUsageMapOffset, usageRow, usagePage); + BinaryPrimitives.WriteInt32LittleEndian(block.Slice(format.IndexDataRootPageOffset, sizeof(int)), rootPage); + } + + /// A fresh index-info block (§3.6): the record marker, then every field of . + internal static byte[] BuildInfoBlock(JetFormatBase format, LogicalIndexSpec info) + { + var block = new byte[format.IndexInfoBlockSize]; + BinaryPrimitives.WriteUInt32LittleEndian(block.AsSpan(format.IndexInfoMarkerOffset, sizeof(uint)), JetFormatBase.TdefRecordMarker); + WriteInfoBlock(block, format, info); + return block; + } + + /// Writes every field of into an index-info block but its name, which the TDEF + /// stores apart, and the marker, which a file may carry as 0 (page-02d) and an edit must leave as it + /// found it — as it does every byte the fields do not cover. + /// reads them back. + internal static void WriteInfoBlock(Span block, JetFormatBase format, LogicalIndexSpec info) + { + BinaryPrimitives.WriteInt32LittleEndian(block.Slice(format.IndexInfoNumberOffset, sizeof(int)), info.Number); + BinaryPrimitives.WriteInt32LittleEndian(block.Slice(format.IndexInfoDataNumberOffset, sizeof(int)), info.DataOrdinal); + block[format.IndexInfoFkTypeOffset] = (byte)info.FkType; + BinaryPrimitives.WriteUInt32LittleEndian(block.Slice(format.IndexInfoFkNumberOffset, sizeof(uint)), info.FkNumber); + BinaryPrimitives.WriteInt32LittleEndian(block.Slice(format.IndexInfoFkTablePageOffset, sizeof(int)), info.FkTablePage); + block[format.IndexInfoUpdateActionOffset] = (byte)info.UpdateAction; + block[format.IndexInfoDeleteActionOffset] = (byte)info.DeleteAction; + block[format.IndexInfoTypeOffset] = (byte)info.Type; + } + + + private static List ResolveColumns(JetFormatBase format, IReadOnlyList specs, Collation collation) + { + // A column's id is its declaration position unless the spec pins one explicitly (the catalog tables + // JetDatabase writes). Variable columns are addressed in ascending column-id order. + int EffectiveId(int i) => specs[i].ColumnId ?? i; + + var variableRank = new Dictionary(); + int rank = 0; + foreach (int i in Enumerable.Range(0, specs.Count).Where(i => !specs[i].IsFixedLength).OrderBy(EffectiveId)) + variableRank[i] = rank++; + + // Descriptor offset 7 ("variable-table index") = number of variable columns with a smaller column-id. + // Access stores this on EVERY column, fixed ones included, where it is NOT 0 — measured on ACE's own + // ADD COLUMN, which writes 2 for a LONG added to (K LONG, A TEXT, B TEXT). ACE does still read a file + // that has 0 there, so this is parity rather than a hard requirement. + int VarTableIndex(int i) => + Enumerable.Range(0, specs.Count).Count(j => !specs[j].IsFixedLength && EffectiveId(j) < EffectiveId(i)); + + // Fixed-data offsets are assigned in ascending column-id order, NOT declaration order — Access lays the + // fixed columns out by column id (e.g. MSysACEs stores ObjectId(id0) at offset 0 even though its + // descriptor is written after ACM). Only differs from declaration order when ids are reordered (MSys*). + bool Occupies(int i) => specs[i].IsFixedLength && specs[i].Type != JetDataType.Boolean; + var fixedOffsets = new Dictionary(); + int running = 0; + foreach (int i in Enumerable.Range(0, specs.Count).Where(Occupies).OrderBy(EffectiveId)) + { + fixedOffsets[i] = running; + running += specs[i].Length; + } + + var columns = new List(specs.Count); + for (int i = 0; i < specs.Count; i++) + { + ColumnSpec s = specs[i]; + // Booleans live in the null bitmap and occupy no fixed-data bytes, so they don't + // advance the fixed offset (matching how the row codec skips them). + bool occupiesFixedData = s.IsFixedLength && s.Type != JetDataType.Boolean; + columns.Add(new ColumnDef + { + Name = s.Name, + Type = s.Type, + Index = i, + ColumnId = EffectiveId(i), + Length = s.Length, + FixedOffset = occupiesFixedData ? fixedOffsets[i] : 0, + VariableIndex = s.IsFixedLength ? -1 : variableRank[i], + VariableTableIndex = VarTableIndex(i), + IsFixedLength = s.IsFixedLength, + IsAutoNumber = s.IsAutoNumber, + SupportsCompressedUnicode = s.SupportsCompressedUnicode, + IsCalculated = s.CalculatedExpression is not null, + CalculatedExpression = s.CalculatedExpression, + CalculatedResultType = s.CalculatedResultType, + SystemFlags = s.SystemFlags, + IsEngineColumn = s.IsEngineColumn || s.SystemFlags != 0, + ExtendedFlags = s.ExtendedFlags, + Precision = EffectivePrecision(s), + Scale = s.Scale, + // Numeric columns carry no collation (their 0x0B/0x0C bytes are precision/scale); every + // other column inherits the database's collating order. + Collation = s.Type == JetDataType.FixedPoint ? Collation.GeneralLegacy : collation, + }); + } + return columns; + } + + private static void WriteColumnDescriptors(byte[] page, JetFormatBase format, List columns, int columnBlock) + { + for (int i = 0; i < columns.Count; i++) + BuildColumnDescriptor(columns[i], format).CopyTo(page.AsSpan(columnBlock + i * format.ColumnDescriptorSize)); + } + + /// Writes the TDEF header's counts of what the definition holds — columns (0x2D), index-data + /// blocks (0x33) and logical indexes (0x2F) — and its length. The logical count may exceed the + /// data-block count: a relationship adds a logical block that shares a data block. The one writer of these, for + /// a definition built whole and for one reassembled after an ALTER. The 0x29 and 0x2B high-waters + /// are not counts — they never go down — and stay with whatever hands out ids. + internal static void WriteCounts(Span tdef, JetFormatBase format, int columns, int dataIndexes, + int logicalIndexes, int length) + { + BinaryPrimitives.WriteUInt16LittleEndian(tdef.Slice(format.TdefColumnCountOffset, sizeof(ushort)), (ushort)columns); + BinaryPrimitives.WriteInt32LittleEndian(tdef.Slice(format.TdefIndexCountOffset, sizeof(int)), dataIndexes); + BinaryPrimitives.WriteInt32LittleEndian(tdef.Slice(format.TdefLogicalIndexCountOffset, sizeof(int)), logicalIndexes); + BinaryPrimitives.WriteInt32LittleEndian(tdef.Slice(format.TdefLengthOffset, sizeof(int)), length); + } + + /// Writes the TDEF header's AutoNumber pair so the next id assigned is : the + /// last-assigned value (0x14) as − , and the increment + /// (0x18). A table with no AutoNumber column carries seed 1, increment 1 — last 0. The one writer of the + /// pair, on create and on every ALTER that changes a counter. + internal static void WriteCounter(Span tdef, JetFormatBase format, int seed, int increment) + { + WriteLastAutoNumber(tdef, format, seed - increment); + BinaryPrimitives.WriteInt32LittleEndian(tdef.Slice(format.TdefAutoNumberIncrementOffset, sizeof(int)), increment); + } + + /// An AutoNumber counting by zero would hand every row the same id, and ACE's own DDL refuses it + /// ("Invalid argument" to COUNTER(1, 0)). An omitted increment is 1, all the way from the SQL + /// layer's IdentitySpec, so a zero here is one the caller asked for. + internal static NotSupportedException ZeroIncrement(string column) => + new($"AutoNumber column '{column}' cannot have an increment of 0: every row would take the same id. " + + "ACE refuses the same COUNTER, and an omitted increment is 1."); + + /// Builds one column's fixed-size (25-byte Jet4) descriptor. Shared by CREATE TABLE and + /// ALTER TABLE ADD COLUMN. + internal static byte[] BuildColumnDescriptor(ColumnDef c, JetFormatBase format) + { + byte[] d = new byte[format.ColumnDescriptorSize]; + d[format.ColumnTypeOffset] = (byte)c.Type; + BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnRecordMarkerOffset, 2), (ushort)JetFormatBase.TdefRecordMarker); + BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnNumberOffset, 2), (ushort)c.ColumnId); + // Offset 0x09 repeats the id on a user column. Every creator does it — ACE's SQL DDL, DAO's object + // model and DAO-executed SQL — and every user table in every fixture carries it, while only the + // engine's own bootstrap tables (MSysObjects and friends) leave it zero, which is what a system + // column keeps here. An earlier comment claimed real files store zero; that had been read off the + // system tables alone. + BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnSecondaryNumberOffset, 2), + (ushort)(c.IsEngineColumn ? 0 : c.ColumnId)); + // Offset 7 = variable-table index (count of variable columns with a smaller id), stored on fixed columns + // too. Prefer the precomputed value; fall back to the legacy rule (0 for fixed) when unset (ADD COLUMN). + BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnVariableIndexOffset, 2), + (ushort)(c.VariableTableIndex >= 0 ? c.VariableTableIndex : (c.IsFixedLength ? 0 : c.VariableIndex))); + WriteLocaleUnion(d, c.Type, c.Precision, c.Scale, c.Collation, format); + // Compose the flag byte (0x0F) from every bit a user column models, plus the catalog bits a created system + // column asks for; likewise the extended-flag byte (0x10), plus the unmodelled bits an attachment's + // value columns ask for. + ColumnFlags flags = + (c.IsUpdatable ? ColumnFlags.Updatable : ColumnFlags.None) + | (c.IsFixedLength ? ColumnFlags.FixedLength : ColumnFlags.None) + | (c.IsAutoNumber ? ColumnFlags.AutoNumber : ColumnFlags.None) + | (c.IsGuidAutoNumber ? ColumnFlags.GuidAutoNumber : ColumnFlags.None) + | (c.IsHyperlink ? ColumnFlags.Hyperlink : ColumnFlags.None) + | (ColumnFlags)c.SystemFlags; + d[format.ColumnFlagsOffset] = (byte)flags; + + ColumnExtendedFlags extFlags = + (c.SupportsCompressedUnicode ? ColumnExtendedFlags.CompressedUnicode : ColumnExtendedFlags.None) + | (c.IsCalculated ? ColumnExtendedFlags.Calculated : ColumnExtendedFlags.None) + | (ColumnExtendedFlags)c.ExtendedFlags; + d[format.ColumnExtendedFlagsOffset] = (byte)extFlags; + + BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnFixedOffsetOffset, 2), (ushort)c.FixedOffset); + BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnLengthOffset, 2), (ushort)c.Length); + return d; + } + + /// Writes descriptor bytes 0x0B–0x0E, which are a union keyed by column type. + /// Shared with the in-place ALTER (SchemaEditor.EditTargetDescriptor), which edits an existing + /// descriptor in place: it must rewrite the whole union on every retype, or a column that stops being + /// DECIMAL keeps its precision and scale sitting where the LANGID belongs — read back as a + /// nonexistent collating order on what is now a text column. + internal static void WriteLocaleUnion( + Span d, JetDataType type, byte precision, byte scale, Collation collation, JetFormatBase format) + { + if (type == JetDataType.FixedPoint) + { + // Decimal/Numeric: precision and scale take the LANGID bytes. 0x0D/0x0E are left as they stand — + // ACE does not clear them on a retype INTO decimal, so neither does LibRed. + d[format.ColumnPrecisionOffset] = precision; + d[format.ColumnScaleOffset] = scale; + } + else if (type == JetDataType.Complex) + { + // A complex (multi-value / attachment) column stores its MSysComplexColumns key here (page-02b + // §3.4) — there is nothing to collate, since the values are rows of the flat table and carry their + // own collations. Writing a LANGID over it severs the column from its values, so the bytes stand. + } + else if (type == JetDataType.DateTimeExtended) + { + // Date/Time Extended is 42 bytes of ASCII with nothing to collate, and ACE writes only the LOW + // byte of the LANGID here, clearing the sublanguage half — the primary language id on its own, + // with sort id and version zero. Measured across five collating orders: 0x0409 and 0x0809 both + // give 0x0009, 0x0407 gives 0x0007, 0x040E 0x000E, 0x041D 0x001D, while a Text column in the + // same table carries the full LANGID each time. (On an en-US database this looks like a + // constant 0x0009, which is how it was first mis-read.) + BinaryPrimitives.WriteUInt16LittleEndian( + d.Slice(format.ColumnLocaleOffset, 2), (ushort)((ushort)collation.Order & 0x00FF)); + d[format.ColumnCollationSortIdOffset] = 0; + d[format.ColumnCollationVersionOffset] = 0; + } + else + { + // 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.Slice(format.ColumnLocaleOffset, 2), (ushort)collation.Order); + d[format.ColumnCollationSortIdOffset] = collation.SortId; + d[format.ColumnCollationVersionOffset] = collation.Version; + } + } + + private static int WriteColumnNames(byte[] page, JetFormatBase format, List columns, int namePos) + { + foreach (ColumnDef c in columns) + { + byte[] entry = NameEntry(c.Name, format); + entry.CopyTo(page, namePos); + namePos += entry.Length; + } + return namePos; + } + + /// One entry of the long-value usage-map list (§3.3.2): the column id, then the pointers to its owned- + /// and free-pages maps, both on . Read back by + /// and . + internal static byte[] LongValueMapEntry(JetFormatBase format, int columnId, int usedRow, int freeRow, int mapPage) + { + var entry = new byte[format.TdefLongValueMapEntrySize]; + BinaryPrimitives.WriteUInt16LittleEndian(entry, (ushort)columnId); + PageBuffer.WriteRecordPointer(entry, format.TdefLongValueMapOwnedOffset, usedRow, mapPage); + PageBuffer.WriteRecordPointer(entry, format.TdefLongValueMapFreeOffset, freeRow, mapPage); + return entry; + } + + /// One TDEF name entry, a column's or an index's: the text's byte length + /// (), then the UTF-16LE text. Read back by + /// . + internal static byte[] NameEntry(string name, JetFormatBase format) + { + int size = format.TdefNameLengthSize; + int length = Encoding.Unicode.GetByteCount(name); + var entry = new byte[size + length]; + BinaryPrimitives.WriteUInt16LittleEndian(entry.AsSpan(0, size), (ushort)length); + Encoding.Unicode.GetBytes(name, entry.AsSpan(size)); + return entry; + } + // A valid Jet/ACE table is constrained to 255 columns, 32 real indexes, and 64-character names. + // One MiB is deliberately generous while still preventing a hostile 32-bit length from driving + // process-scale allocation. This is an implementation safety budget, not an on-disk field width. + internal const int MaxDefinitionLength = 1024 * 1024; + + internal static (PageBuffer Buffer, IReadOnlyList ContinuationPages) ReadChain( + PageChannel channel, int firstPage) + { + JetFormatBase format = channel.Format; + ValidatePageNumber(channel, firstPage, "TDEF root"); + PageBuffer first = channel.ReadPage(firstPage); + if (PageHeader.ReadType(first.Span) != PageType.TableDefinition) + throw new InvalidDataException( + $"TDEF root page {firstPage} is type 0x{(ushort)PageHeader.ReadType(first.Span):X4}, expected 0x0102."); + + int definitionLength = ReadLength(first.Span, format); + if (definitionLength < format.TdefRealIndexBlockOffset || definitionLength > MaxDefinitionLength) + throw new InvalidDataException( + $"TDEF page {firstPage} declares length {definitionLength}; supported validated range is " + + $"{format.TdefRealIndexBlockOffset} through {MaxDefinitionLength} bytes."); + + // The chain holds the definition AND its 8-byte trailing reserve, which follows the last definition byte + // and spills onto a page of its own when it does not fit — so a continuation page can carry no definition + // bytes at all (verified vs ACE: a 4,090-byte definition has a continuation holding two reserve bytes). + int pageSize = format.PageSize; + int headerSize = format.TdefContinuationHeaderSize; + int bodySize = pageSize - headerSize; + int stored = definitionLength + headerSize; + int continuationCount = stored <= pageSize + ? 0 + : (stored - pageSize + bodySize - 1) / bodySize; + + int next = first.ReadInt32(format.TdefNextPageOffset); + var continuationPages = new List(continuationCount); + var continuationBuffers = new List(continuationCount); + var visited = new HashSet { firstPage }; + + for (int i = 0; i < continuationCount; i++) + { + if (next == 0) + throw new InvalidDataException( + $"TDEF page {firstPage} ends after {i} continuation pages but its declared length requires {continuationCount}."); + ValidatePageNumber(channel, next, "TDEF continuation"); + if (!visited.Add(next)) + throw new InvalidDataException($"TDEF page {firstPage} contains a continuation cycle at page {next}."); + + PageBuffer continuation = channel.ReadPage(next); + if (PageHeader.ReadType(continuation.Span) != PageType.TableDefinition) + throw new InvalidDataException( + $"TDEF continuation page {next} is type 0x{(ushort)PageHeader.ReadType(continuation.Span):X4}, expected 0x0102."); + + continuationPages.Add(next); + continuationBuffers.Add(continuation); + next = continuation.ReadInt32(format.TdefNextPageOffset); + } + + if (next != 0) + throw new InvalidDataException( + $"TDEF page {firstPage} has more continuation pages than its declared length permits."); + + var assembled = new byte[definitionLength]; + int written = Math.Min(pageSize, definitionLength); + first.Span[..written].CopyTo(assembled); + foreach (PageBuffer continuation in continuationBuffers) + { + int take = Math.Min(bodySize, definitionLength - written); + continuation.Span.Slice(headerSize, take) + .CopyTo(assembled.AsSpan(written)); + written += take; + } + + return (new PageBuffer(assembled, firstPage), continuationPages); + } + + private static void ValidatePageNumber(PageChannel channel, int pageNumber, string role) + { + if (pageNumber <= 0 || pageNumber >= channel.PageCount) + throw new InvalidDataException( + $"{role} page {pageNumber} is outside the database's {channel.PageCount} pages."); + } + /// + /// Where each variable-length region of a table definition begins (page-02a §3.3). Only the first has a fixed + /// offset; every later one is found by stepping over all of its predecessors, including the column names, + /// whose lengths come out of the file. Anything reaching into a TDEF it did not itself parse — adding an index + /// or a relationship block, repointing a B-tree root, splitting the definition apart to rewrite it — needs the + /// same walk, so they share this one. + /// + /// Header 0x2D: descriptors and names alike. + /// Header 0x33: the real index count, sizing both the statistics and the + /// index-data blocks. + /// Header 0x2F: info blocks and index names. + /// Start of the per-index statistics — the one fixed offset, 0x3F. + /// Start of the column descriptors; the names follow them. + /// Start of the index-data blocks, past the column names. + /// Start of the logical index-info blocks. + /// Start of the index names, and so the end of the info blocks. + internal readonly record struct Regions( + int ColumnCount, + int DataCount, + int LogicalCount, + int Stats, + int ColumnDescriptors, + int DataBlocks, + int InfoBlocks, + int IndexNames) + { + /// Walks — the whole definition, continuation pages already stitched in, + /// since the regions run straight across a page boundary. + /// + /// Every region is bounded as it is crossed. The counts and name lengths are all file-sourced, and + /// unchecked they carry the walk past the buffer — or, when a multiply overflows, back inside it at the + /// wrong place, which lands a write on some other index's block with no error raised at all. + /// + public static Regions Of(ReadOnlySpan tdef, JetFormatBase format) + { + int columnCount = BinaryPrimitives.ReadUInt16LittleEndian(tdef.Slice(format.TdefColumnCountOffset, 2)); + int dataCount = BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(format.TdefIndexCountOffset, 4)); + int logicalCount = BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(format.TdefLogicalIndexCountOffset, 4)); + + int stats = format.TdefRealIndexBlockOffset; + int descriptors = CheckedRegionEnd( + stats, dataCount, format.RealIndexEntrySize, tdef.Length, "index statistics"); + int pos = CheckedRegionEnd( + descriptors, columnCount, format.ColumnDescriptorSize, tdef.Length, "column descriptors"); + + for (int i = 0; i < columnCount; i++) + pos = NameEntryEnd(tdef, pos, format, $"column {i}"); + + int dataBlocks = pos; + int infoBlocks = CheckedRegionEnd( + dataBlocks, dataCount, format.IndexDataBlockSize, tdef.Length, "index-data blocks"); + int indexNames = CheckedRegionEnd( + infoBlocks, logicalCount, format.IndexInfoBlockSize, tdef.Length, "logical-index blocks"); + + return new Regions( + columnCount, dataCount, logicalCount, stats, descriptors, dataBlocks, infoBlocks, indexNames); + } + } + internal sealed class Parts + { + public required byte[] Header; // [0, TdefRealIndexBlockOffset) + public required List Stats; // one statistics entry per data index + public required List<(byte[] Descriptor, byte[] Name)> Columns; // descriptor + its name, in descriptor order + public required List DataBlocks; // one index-data block per data index + public required List<(byte[] Info, byte[] Name)> Logical; // info block + its name, name-sorted + public required byte[] Lval; // §3.3.2 list + terminator + public IReadOnlyList Continuations = []; // continuation-page numbers (multi-page TDEF) + } + + internal static Parts ReadParts(PageChannel channel, int tdefPage) + { + JetFormatBase format = channel.Format; + // Stitch any continuation pages into one contiguous buffer (offsets are absolute from page 1), so the + // surgery below works the same for single- and multi-page definitions. + (PageBuffer buf, IReadOnlyList continuations) = ReadChain(channel, tdefPage); + + Regions regions = Regions.Of(buf.Span, format); + int defEnd = ReadLength(buf.Span, format); + + var stats = new List(regions.DataCount); + for (int i = 0; i < regions.DataCount; i++) + stats.Add(buf.Slice(regions.Stats + i * format.RealIndexEntrySize, format.RealIndexEntrySize).ToArray()); + + // Each descriptor with its name: the names follow the descriptors in the same order. + var columns = new List<(byte[], byte[])>(regions.ColumnCount); + int np = regions.ColumnDescriptors + regions.ColumnCount * format.ColumnDescriptorSize; + for (int i = 0; i < regions.ColumnCount; i++) + { + byte[] descriptor = buf.Slice(regions.ColumnDescriptors + i * format.ColumnDescriptorSize, format.ColumnDescriptorSize).ToArray(); + int end = NameEntryEnd(buf.Span, np, format, $"column {i}"); + columns.Add((descriptor, buf.Slice(np, end - np).ToArray())); + np = end; + } + + var dataBlocks = new List(regions.DataCount); + for (int i = 0; i < regions.DataCount; i++) + dataBlocks.Add(buf.Slice(regions.DataBlocks + i * format.IndexDataBlockSize, format.IndexDataBlockSize).ToArray()); + + // Each info block with its name, likewise. + var logical = new List<(byte[], byte[])>(regions.LogicalCount); + np = regions.IndexNames; + for (int i = 0; i < regions.LogicalCount; i++) + { + byte[] info = buf.Slice(regions.InfoBlocks + i * format.IndexInfoBlockSize, format.IndexInfoBlockSize).ToArray(); + int end = NameEntryEnd(buf.Span, np, format, $"logical index {i}"); + logical.Add((info, buf.Slice(np, end - np).ToArray())); + np = end; + } + + return new Parts + { + Header = buf.Slice(0, regions.Stats).ToArray(), + Stats = stats, + Columns = columns, + DataBlocks = dataBlocks, + Logical = logical, + Lval = buf.Slice(np, defEnd - np).ToArray(), + Continuations = continuations, + }; + } + internal static void WriteParts(PageChannel channel, PageAllocator allocator, int tdefPage, Parts parts) + { + JetFormatBase format = channel.Format; + var body = new List(format.PageSize); + body.AddRange(parts.Header); + foreach (byte[] s in parts.Stats) body.AddRange(s); + foreach ((byte[] descriptor, _) in parts.Columns) body.AddRange(descriptor); + foreach ((_, byte[] name) in parts.Columns) body.AddRange(name); + foreach (byte[] d in parts.DataBlocks) body.AddRange(d); + foreach ((byte[] info, _) in parts.Logical) body.AddRange(info); + foreach ((_, byte[] nm) in parts.Logical) body.AddRange(nm); + body.AddRange(parts.Lval); + byte[] def = [.. body]; + int defEnd = def.Length; + + // The live column count, like the two index counts, is whatever the parts now hold. + WriteCounts(def, format, parts.Columns.Count, parts.DataBlocks.Count, parts.Logical.Count, defEnd); + + // Write across the first page and continuation pages as needed (fresh ones, the old released) — handles a + // definition that shrinks to one page, stays multi-page, or grows past a page (e.g. ADD COLUMN). + WriteChain(channel, allocator, tdefPage, def, parts.Continuations, rewrite: true); + + } + + /// + /// Writes a definition buffer across the first page and, if it overflows, continuation pages (each + /// [0x02][0x01][free:2][next:4] then data). The first page carries the whole definition in its + /// coordinate space; each continuation contributes -offset data. + /// Rewriting an existing definition () is done as ACE does it (verified by + /// whole-file diff, growing and shrinking): the first page is rewritten in place — alone, only the 8-byte + /// reserve past the new end is zeroed and older bytes beyond it are left — and continuation data always goes + /// to freshly allocated pages, while are released untouched. + /// + internal static void WriteChain(PageChannel channel, PageAllocator allocator, int firstPage, byte[] def, + IReadOnlyList oldContinuations, bool rewrite) + { + JetFormatBase format = channel.Format; + int ps = format.PageSize; + int nextOffset = format.TdefNextPageOffset; + int headerSize = format.TdefContinuationHeaderSize; + + foreach (int old in oldContinuations) + allocator.Release(old); // reusable only after this handle closes, as ACE holds them + + if (def.Length + headerSize <= ps) + { + byte[] only = rewrite ? channel.ReadPage(firstPage).Span.ToArray() : new byte[ps]; + def.CopyTo(only, 0); + only.AsSpan(def.Length, headerSize).Clear(); // the reserve + BinaryPrimitives.WriteInt32LittleEndian(only.AsSpan(nextOffset, 4), 0); + BinaryPrimitives.WriteUInt16LittleEndian(only.AsSpan(format.TdefFreeSpaceOffset, 2), (ushort)(ps - def.Length - headerSize)); + channel.WritePage(firstPage, only); + return; + } + + // The chain holds the definition and then its 8-byte trailing reserve, as ACE lays it out (verified): every + // page is filled before the next begins, and the reserve follows the last definition byte, spilling onto a + // page of its own when it does not fit — so a continuation can hold reserve bytes and no definition. A + // 4,090-byte definition fills page 1 with 4,090 bytes and six of the reserve, and its continuation holds the + // other two, free 4,086. Each page's free space is what it has left once both are placed. + int maxMiddle = ps - headerSize; + int stored = def.Length + headerSize; + var chunks = new List<(int Offset, int Length, int Free)>(); + for (int consumed = ps; consumed < stored;) + { + int placed = Math.Min(maxMiddle, stored - consumed); + chunks.Add((consumed, Math.Clamp(def.Length - consumed, 0, placed), maxMiddle - placed)); + consumed += placed; + } + + // ACE allocates the last continuation first (verified: from free pages 354.. a two-page continuation became + // first → 355 → 354, and from the end of a file first → n+1 → n). + var pageNumbers = new int[chunks.Count]; + for (int i = chunks.Count - 1; i >= 0; i--) + pageNumbers[i] = allocator.Allocate(); + + var page1 = new byte[ps]; + Array.Copy(def, 0, page1, 0, Math.Min(ps, def.Length)); // page 1 is completely full in a multi-page definition + BinaryPrimitives.WriteInt32LittleEndian(page1.AsSpan(nextOffset, 4), pageNumbers[0]); + BinaryPrimitives.WriteUInt16LittleEndian(page1.AsSpan(format.TdefFreeSpaceOffset, 2), 0); + channel.WritePage(firstPage, page1); + + for (int i = 0; i < chunks.Count; i++) + { + var (offset, length, free) = chunks[i]; + var page = new byte[ps]; + PageHeader.WriteType(page, PageType.TableDefinition); + if (length > 0) // a page holding only the reserve starts past the definition's end + Array.Copy(def, offset, page, headerSize, length); + int next = i + 1 < pageNumbers.Length ? pageNumbers[i + 1] : 0; + BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(nextOffset, 4), next); + BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefFreeSpaceOffset, 2), (ushort)free); + channel.WritePage(pageNumbers[i], page); + } + } + + /// Inserts a long-value (memo/OLE) column's §3.3.2 usage-map entry + /// ({col_num:2}{used row+page:4}{free row+page:4}) just before the list's terminator. + /// The new column has the highest id, so appending keeps the list in ascending column order. + internal static void AddLongValueMapEntry(JetFormatBase format, Parts parts, int columnId, int usedRow, int freeRow, int mapPage) + { + int entrySize = format.TdefLongValueMapEntrySize; + byte[] lval = parts.Lval; + int at = lval.Length - sizeof(ushort); // before the terminator + + byte[] entry = LongValueMapEntry(format, columnId, usedRow, freeRow, mapPage); + + var result = new byte[lval.Length + entrySize]; + Array.Copy(lval, 0, result, 0, at); + entry.CopyTo(result, at); + Array.Copy(lval, at, result, at + entrySize, sizeof(ushort)); // the terminator + parts.Lval = result; + } + + /// Removes a long-value column's §3.3.2 usage-map entry from the list, keeping the other entries + /// and the terminator. A no-op for a column without one. + internal static void RemoveLongValueMapEntry(JetFormatBase format, Parts parts, int columnId) + { + int entrySize = format.TdefLongValueMapEntrySize; + byte[] lval = parts.Lval; + for (int at = 0; at + sizeof(ushort) < lval.Length; at += entrySize) + { + if (BinaryPrimitives.ReadUInt16LittleEndian(lval.AsSpan(at, 2)) != columnId) continue; + var result = new byte[lval.Length - entrySize]; + Array.Copy(lval, 0, result, 0, at); + Array.Copy(lval, at + entrySize, result, at, lval.Length - at - entrySize); + parts.Lval = result; + return; + } + } + + /// Applies ACE's in-place column retype to the target descriptor within + /// (no page write — the caller writes the TDEF once): the target becomes a NEW column with a fresh id from the + /// 0x29 high-water and its fixed data appended to the END of the current fixed region (its old slot left + /// as dead space — ACE does not compact); 0x29 bumps, and 0x2B too for a variable retype. Only + /// the target descriptor changes; every other descriptor stays byte-identical. Returns the burned new id. + internal static int EditTargetDescriptor(Parts parts, ColumnDef target, ColumnSpec newSpec, int fixedEnd, + Collation collation, JetFormatBase format) + { + // ACE-only probe: with 254 columns one retype succeeds, the next fails; with 255 + // columns the first retype fails. A same-type ALTER does not reach this id-burning path. + (int columnId, int varCount) = TakeColumnId(parts, newSpec.IsFixedLength, + $"Cannot change the type of '{target.Name}'", format); + + Span d = parts.Columns[target.Index].Descriptor; + d[format.ColumnTypeOffset] = (byte)newSpec.Type; + BinaryPrimitives.WriteUInt16LittleEndian(d[format.ColumnNumberOffset..], (ushort)columnId); // +0x05 id burned + // The target's var-index (+0x07) becomes the old variable-column count — the next var slot — for BOTH a + // fixed and a variable retype (verified vs ACE); a variable retype also bumps the 0x2B var-column count. + BinaryPrimitives.WriteUInt16LittleEndian(d[format.ColumnVariableIndexOffset..], (ushort)varCount); + // +0x09 is deliberately left unchanged: it is the column's ordinal position, which a retype does not + // move (verified: ACE does not update it). + var flags = (ColumnFlags)d[format.ColumnFlagsOffset]; + flags = newSpec.IsFixedLength ? flags | ColumnFlags.FixedLength : flags & ~ColumnFlags.FixedLength; + flags = newSpec.IsAutoNumber ? flags | ColumnFlags.AutoNumber : flags & ~ColumnFlags.AutoNumber; + d[format.ColumnFlagsOffset] = (byte)flags; + BinaryPrimitives.WriteUInt16LittleEndian(d[format.ColumnFixedOffsetOffset..], (ushort)(newSpec.IsFixedLength ? fixedEnd : 0)); // +0x15 + BinaryPrimitives.WriteUInt16LittleEndian(d[format.ColumnLengthOffset..], (ushort)newSpec.Length); // +0x17 + // 0x0B–0x0E is a union keyed by type, so the WHOLE union is rewritten, not just the decimal arm. + // Writing precision/scale on the way in but nothing on the way out left a former DECIMAL(12,3) with + // 0x0C 0x03 in its LANGID bytes, which reads back as collating order 0x030C on a text column. + WriteLocaleUnion(d, newSpec.Type, EffectivePrecision(newSpec), newSpec.Scale, collation, format); + return columnId; + } + + /// Hands out the next column id from the 0x29 high-water and the next variable slot from the + /// 0x2B one, bumping 0x29 — and 0x2B too for a variable column — in + /// ' header; returns both as they were. Neither ever decrements on DROP COLUMN, so once + /// every id has been handed out no column can be added or retyped, even when the live count is lower, until the + /// database is compacted (which renumbers and reclaims dropped ids). ACE refuses exactly this, "Too many fields + /// defined", rather than write an id it can't represent (verified); says what was + /// refused. + internal static (int ColumnId, int VariableIndex) TakeColumnId(Parts parts, bool isFixedLength, string refusal, + JetFormatBase format) + { + (int columnId, int variableIndex) = ReadHighWaters(parts.Header, format); + if (columnId >= format.MaxColumnsPerTable) + throw new NotSupportedException( + $"{refusal}: too many fields defined — {format.MaxColumnsPerTable} column ids have been used over this " + + "table's lifetime, and dropped ids are only reclaimed by compacting the database."); + + WriteHighWaters(parts.Header, format, columnId + 1, isFixedLength ? variableIndex : variableIndex + 1); + return (columnId, variableIndex); + } + + /// Removes a data index (its stats + data block at , + /// decrementing the data-ordinal reference of every remaining info block that pointed past it) + /// and every logical block matching (with its name). + internal static void RemoveIndexBlocks(JetFormatBase format, Parts parts, int? removeDataOrdinal, Func<(byte[] Info, byte[] Name), bool> removeLogical) + { + // The logical blocks go first, so what is left is exactly what has to survive the renumbering below. + parts.Logical.RemoveAll(b => removeLogical(b)); + + if (removeDataOrdinal is int ord) + { + // One data block can be named by more than one logical block — a primary key that also backs a + // relationship is the everyday case — and the renumbering below only moves references PAST the + // block being removed. A surviving reference EQUAL to it would silently come to name whichever + // index slid into the slot: same table, same file, an index quietly pointing at another index's + // B-tree. The callers guard against this by refusing to drop an index a relationship uses; this is + // the structural check behind that, so a route that ever gets here says so instead of writing it. + List<(byte[] Block, LogicalIndexSpec Info)> survivors = + [.. parts.Logical.Select(b => (b.Info, ReadInfoBlock(b.Info, format, ReadName(new PageBuffer(b.Name, 0), 0, "logical index", format).Name)))]; + foreach ((_, LogicalIndexSpec info) in survivors) + if (info.DataOrdinal == ord) + throw new InvalidOperationException( + $"Cannot remove index-data block {ord}: logical index '{info.Name}' still refers to it."); + + parts.Stats.RemoveAt(ord); + parts.DataBlocks.RemoveAt(ord); + foreach ((byte[] block, LogicalIndexSpec info) in survivors) + if (info.DataOrdinal > ord) + WriteInfoBlock(block, format, info with { DataOrdinal = info.DataOrdinal - 1 }); + } + } + + /// Maps an absolute definition offset to the page holding it and the offset within that page. + private (int Page, int Offset) MapDefinitionOffset(PageChannel channel, IReadOnlyList continuations, int offset) + { + int pageSize = channel.Format.PageSize; + if (offset < pageSize) return (DefinitionPage, offset); + + int headerSize = channel.Format.TdefContinuationHeaderSize; + int body = pageSize - headerSize; + int relative = offset - pageSize; + int index = relative / body; + if (index >= continuations.Count) + throw new InvalidOperationException( + $"Definition offset {offset} lies past the end of table '{Name}'s definition chain."); + return (continuations[index], headerSize + relative % body); + } + + + /// Writes at an absolute definition offset, splitting the write where it + /// straddles a continuation-page boundary. + internal void WriteIntoDefinition(PageChannel channel, IReadOnlyList continuations, int offset, ReadOnlySpan bytes) + { + for (int i = 0; i < bytes.Length;) + { + int pageNumber = MapDefinitionOffset(channel, continuations, offset + i).Page; + byte[] page = channel.ReadPage(pageNumber).Span.ToArray(); + + int j = i; + for (; j < bytes.Length; j++) + { + (int target, int within) = MapDefinitionOffset(channel, continuations, offset + j); + if (target != pageNumber) break; + page[within] = bytes[j]; + } + + channel.WritePage(pageNumber, page); + i = j; + } + } + + +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs b/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs deleted file mode 100644 index 3adab88a..00000000 --- a/src/LibRed/LibRed.Core/Catalog/TdefBuilder.cs +++ /dev/null @@ -1,682 +0,0 @@ -using LibRed.Formats; -using LibRed.Pages; -using System.Buffers.Binary; -using System.Text; - -namespace LibRed.Catalog; - -/// A column to create: its name, type, and (for fixed/text) byte length. -public sealed record ColumnSpec( - string Name, - JetDataType Type, - int Length, - bool IsFixedLength, - bool IsAutoNumber = false, - byte Precision = 0, - byte Scale = 0, - bool IsNullable = true, - // AutoNumber (COUNTER) seed and increment — the first generated id is Seed, then +Increment each row. - // Default 1/1 (a plain COUNTER). Stored in the TDEF header: last-value 0x14 = Seed-Increment, 0x18 = Increment. - int Seed = 1, - int Increment = 1, - // An explicit column id, else the column's position is used. Access's own catalog tables declare their - // columns in an order that is not their id order (DatabaseCreator); null for an ordinary CREATE/ADD column. - int? ColumnId = null, - // The catalog flag bits (0x0F) Access sets on the columns of its own catalog — MSysObjects, MSysACEs, - // MSysQueries, MSysRelationships and MSysComplexColumns: 0x10 on every one, and 0x20 as well on the two that - // hold a security identifier (MSysObjects.Owner, MSysACEs.SID). Every other column leaves them clear, the - // other MSys* tables' included (verified; see page-02b-columns.md). - byte SystemFlags = 0, - // WITH COMPRESSION on a Text/Memo column: the 0x10 extended flag bit 0x01. Off unless asked for, which - // is what ACE does for a column declared without it (LongTextStorageAccessTests). - bool SupportsCompressedUnicode = false, - // A calculated column (§3.4a): the expression, and the type the caller DECLARED for it. Both null for an - // ordinary column. They are separate from Type because Type carries the descriptor's *promoted* storage - // type, which need not be the result type at all — build one with Calculated() rather than by hand. - string? CalculatedExpression = null, - JetDataType? CalculatedResultType = null, - // A column of a table the engine creates for itself, which leaves 0x09 — the second copy of the column id — - // zero. A catalog column (SystemFlags) is one already; this marks the rest, such as the MSysComplexType_* - // templates, which carry no catalog flag. - bool IsEngineColumn = false, - // Extended flag bits (0x10) LibRed does not model, to set on a created column: 0x10 on an attachment value - // column (FileData, FileFlags, FileName, FileTimeStamp, FileType, FileURL). - byte ExtendedFlags = 0) -{ - /// A calculated column of computing . - /// ACE stores one **always variable-length**, with the descriptor carrying the *promoted* type and a - /// constant declared length, and the real result type living in the table's property blob — get any of - /// those wrong and Access reads the payload at the wrong width, so this is the only supported way to - /// build one. - public static ColumnSpec Calculated(string name, JetDataType resultType, string expression) => - new(name, PromotedType(resultType), CalculatedLength(resultType), IsFixedLength: false, - // `Backtick` quoting is SQL's, not the expression service's, and ACE cannot read it — normalise - // here so every route in (SQL, or a direct caller) stores something ACE can evaluate. - CalculatedExpression: Storage.Calculated.CalculatedExpression.NormaliseIdentifierQuoting(expression), - CalculatedResultType: resultType); - - /// The descriptor's storage type: ACE widens the result type to the next in its family, which is - /// why the payload's own length is what says how to decode it (§3.4a). - internal static JetDataType PromotedType(JetDataType resultType) => resultType switch - { - JetDataType.Boolean => JetDataType.Int16, - JetDataType.Byte or JetDataType.Int16 => JetDataType.Int32, - JetDataType.Single => JetDataType.Double, - // Memo, Guid and Binary all land on Text — only their declared LENGTH tells them apart. - JetDataType.Memo or JetDataType.Guid or JetDataType.Binary => JetDataType.Text, - _ => resultType, - }; - - /// ACE's constant declared length for a calculated column, measured across every type DAO will - /// create: 0 for a Memo result, whose value lives on long-value pages; 509 for Text - /// whatever size was asked for; 510 for Binary; and 39 for everything else, GUID included. - /// None is a payload ceiling — the requested size is discarded and ACE does not truncate — and nothing - /// moves them: not the requested size, not the length of the expression. - internal static int CalculatedLength(JetDataType resultType) => resultType switch - { - JetDataType.Memo => 0, - JetDataType.Text => 509, - JetDataType.Binary => 510, - _ => 39, - }; -} - -/// An index to create over the named columns, anchored at an already-allocated root page. -public sealed record IndexSpec( - string Name, - IReadOnlyList Columns, - bool IsPrimaryKey, - bool IsUnique, - int RootPage, - int UsageMapRow = 0, - int UsageMapPage = 0); - -/// -/// The trailing §3.3.2 column-usage-map entry for a long-value (memo/OLE) column: its column id and -/// pointers to the owned- and free-pages usage maps that will track the column's LVAL pages. -/// -public sealed record LongValueColumnSpec(int ColumnId, int UsedRow, int FreeRow, int MapPage); - -/// -/// Serializes a table schema into a Jet 4 / ACE table-definition (TDEF) page — the inverse of -/// , covering the whole definition: column descriptors and names, index -/// statistics, index-data and logical index-info blocks with their relationship linkage, and the trailing -/// long-value column-usage list. The result is one contiguous buffer in the absolute coordinate space the -/// descriptors use, which may exceed a page — splitting it across continuation pages is the caller's job -/// (TableCreator.WriteDefinition). Fixed columns are packed in declaration order; variable columns -/// are ranked by column id for the row var-offset table. -/// -public static class TdefBuilder -{ - // TDEF header offsets + the record marker / continuation-header size live on JetFormatBase (shared, - // version-aware); the column-descriptor sub-offsets Access needs but the reader ignores are below. - private const int ColumnRecordMarkerOffset = 0x01; // 0x0659 - - // Index-data and index-info block layout + flags are shared with the reader via IndexBlockFormat / IndexFlags. - private const int MaxIndexesPerTable = 32; // Jet/ACE limit, counting keys- and relationship-backing indexes - private const int MaxColumnsPerTable = 255; - private const int MaxNameBytes = 128; // verified Access limit: 64 UTF-16 code units - - /// - /// One logical index-info block (§3.6). Several logical indexes may share a data block: a plain - /// index has one, and a relationship adds one that reuses this table's side of the foreign key. - /// is the data-block index (index_num2); - /// is the logical id (index_num). For a relationship, is 1 (incoming) - /// or 2 (outgoing), is the other end's index_num, and - /// is the other table's TDEF page. - /// - public sealed record LogicalIndexSpec( - int Number, - int DataOrdinal, - byte FkType, - uint FkNumber, - int FkTablePage, - byte UpdateAction, - byte DeleteAction, - byte Type, - string Name); - - public sealed record Result(byte[] Page, IReadOnlyList Columns); - - /// The database's collating order, which every non-numeric column this - /// builds inherits — it is what decides how their index keys are encoded. Required, and deliberately not - /// defaulted: the old ?? GeneralLegacy fallback was correct only for a database that happens to use - /// General-Legacy, and silently wrote v0 columns into a v1 file for every caller that forgot it. - /// The on-disk format to build for. - /// User or system. - /// The columns, in creation order. - /// The table's indexes, or null for none. - /// The memo/OLE columns' usage-map pointers. - /// Explicit logical-index blocks, or null to derive them from the indexes. - public static Result Build( - JetFormatBase format, - TableType tableType, - IReadOnlyList specs, - Collation collation, - IReadOnlyList? indexes = null, - IReadOnlyList? longValueColumns = null, - IReadOnlyList? logicalIndexes = null) - { - indexes ??= []; - longValueColumns ??= []; - ValidateColumnSpecs(format, specs); - // Jet/ACE caps a table at 32 indexes, counting those backing primary keys, unique constraints - // and relationships (§3.5 index-data blocks, the 0x33 count). Reject rather than write a bad TDEF. - if (indexes.Count > MaxIndexesPerTable) - throw new NotSupportedException( - $"Table has {indexes.Count} indexes; a table can have at most {MaxIndexesPerTable} (including those backing keys and relationships)."); - var columns = ResolveColumns(format, specs, collation); - IReadOnlyList logical = logicalIndexes ?? indexes.Select((ix, i) => new LogicalIndexSpec( - Number: i, DataOrdinal: i, FkType: 0, FkNumber: IndexBlockFormat.NoForeignKey, FkTablePage: 0, - UpdateAction: IndexBlockFormat.PlainAction, DeleteAction: IndexBlockFormat.PlainAction, - Type: ix.IsPrimaryKey ? IndexBlockFormat.TypePrimary : IndexBlockFormat.TypeSecondary, Name: ix.Name)).ToList(); - ValidateIndexAndLongValueSpecs(columns, indexes, logical, longValueColumns); - - int definitionSize = DefinitionSize(format, columns, indexes, logical, longValueColumns); - var page = new byte[Math.Max(format.PageSize, definitionSize)]; - - PageHeader.WriteType(page, PageType.TableDefinition); - BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.TdefRecordMarkerOffset, 4), JetFormatBase.TdefRecordMarker); - // AutoNumber (COUNTER) config lives in the TDEF header: 0x18 = increment (default 1), and 0x14 = - // the last-assigned value initialized to Seed-Increment so the first insert yields Seed. A table has - // at most one AutoNumber column; with none, these stay at the plain-counter defaults (increment 1, - // last 0). Verified vs ACE (COUNTER(1000, 7) → 0x18=7, 0x14=993). - // "At most one" is enforced, not assumed: the header holds a single seed/increment pair, so a second - // AutoNumber column would be written with the 0x04 flag set and no counter configuration of its own — - // two columns then claiming one header counter at insert. ALTER's promote path already refuses this; - // CREATE silently took the first and ignored the rest. - // - // Complex columns are exempt, and are not a second claimant: they carry the same 0x04 flag but are - // allocated from 0x1C, not from this pair. Counting them would refuse a table that has an ordinary - // counter beside a complex column — complex1.accdb's Table1 has one of each kind — and could hand - // `counter` a complex spec whose seed/increment describe nothing. - var counters = specs.Where(s => s.IsAutoNumber && s.Type != JetDataType.Complex).ToList(); - if (counters.Count > 1) - throw new NotSupportedException( - $"A table may have only one AutoNumber column; {string.Join(", ", counters.Select(c => $"'{c.Name}'"))} are all declared as one."); - ColumnSpec? counter = counters.FirstOrDefault(); - if (counter is { Increment: 0 }) throw ZeroIncrement(counter.Name); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefAutoNumberIncrementOffset, 4), counter?.Increment ?? 1); - // Complex-type AutoNumber high-water (0x1C): 0 for a new table, which has no complex column. - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefComplexAutoNumberOffset, 4), 0); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefNextPageOffset, 4), 0); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefRowCountOffset, 4), 0); - if (counter is not null) - BinaryPrimitives.WriteInt32LittleEndian( - page.AsSpan(format.TdefLastAutoNumberOffset, 4), counter.Seed - counter.Increment); - page[format.TdefTableTypeOffset] = (byte)tableType; - // The 0x29 high-water is the next column id to hand out = max existing id + 1. For contiguous ids this - // equals the column count. - int maxColumnId = columns.Select(c => c.ColumnId).DefaultIfEmpty(-1).Max(); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefMaxColumnsOffset, 2), (ushort)(maxColumnId + 1)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefVariableColumnsOffset, 2), - (ushort)columns.Count(c => !c.IsFixedLength)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefColumnCountOffset, 2), (ushort)columns.Count); - // Logical index count (0x2F) may exceed the real data-block count (0x33): a relationship adds - // a logical block that shares a data block. Without explicit logical specs the two are equal. - int logicalCount = logical.Count; - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefLogicalIndexCountOffset, 4), logicalCount); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefIndexCountOffset, 4), indexes.Count); - - // The per-index statistics blocks (12 bytes each, one per data block) precede the columns. - int columnBlock = format.TdefRealIndexBlockOffset + indexes.Count * format.RealIndexEntrySize; - WriteColumnDescriptors(page, format, columns, columnBlock); - int afterNames = WriteColumnNames(page, format, columns, columnBlock + columns.Count * format.ColumnDescriptorSize); - - int definitionEnd = WriteIndexes(page, format, columns, indexes, logical, longValueColumns, afterNames); - if (definitionEnd != definitionSize) - throw new InvalidOperationException( - $"TDEF sizing preflight calculated {definitionSize} bytes but serialization wrote {definitionEnd}."); - - // Definition length and remaining free space (Access reserves an 8-byte continuation header). For a - // multi-page definition the caller recomputes the first page's free space, so clamp at 0 here. - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefLengthOffset, 4), definitionEnd); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefFreeSpaceOffset, 2), - (ushort)Math.Max(0, format.PageSize - definitionEnd - JetFormatBase.TdefContinuationHeaderSize)); - - return new Result(page, columns); - } - - /// The precision written to the descriptor. A declared 0 resolves to 18 — ACE's default for - /// a bare DECIMAL, and AccessTypeMapper's — because a 0 on disk leaves the value with no - /// declared shape and ACE cannot materialise such a column (AceCoreApiShapeProbeTests, - /// decimal-precision-zero). FixedPoint only: elsewhere those bytes are the LANGID. - private static byte EffectivePrecision(ColumnSpec spec) => - spec.Type == JetDataType.FixedPoint && spec.Precision == 0 - ? DefaultNumericPrecision - : spec.Precision; - - /// What ACE declares for a bare DECIMAL or NUMERIC — measured, not assumed - /// (AceDecimalDeclarationProbeTest), and the SQL front end's default too. ACE never writes precision - /// 0: none of the 127 FixedPoint columns across 36 fixtures carries one. - private const byte DefaultNumericPrecision = 18; - - /// A NUMERIC/DECIMAL column's declared precision and scale, which nothing else checked — the width - /// check covers only the fixed 17 bytes. AccessTypeMapper enforces this on the SQL path; a direct - /// Core caller bypasses it. Precision 0 means "unspecified" (see ), not zero - /// digits, so only a positively wrong declaration is refused. - private static void ValidateNumericPrecision(ColumnSpec spec) - { - if (spec.Type is not JetDataType.FixedPoint || spec.Precision == 0) return; - - if (spec.Precision > Storage.Types.JetTypeCodec.MaxNumericPrecision) - throw new NotSupportedException( - $"Column '{spec.Name}' declares NUMERIC precision {spec.Precision}; the maximum precision is " - + $"{Storage.Types.JetTypeCodec.MaxNumericPrecision}."); - - if (spec.Scale > spec.Precision) - throw new NotSupportedException( - $"Column '{spec.Name}' declares NUMERIC scale {spec.Scale} under precision {spec.Precision}; " - + "the scale cannot exceed the precision — that would be more digits after the decimal point " - + "than the column has in total."); - } - - private static void ValidateColumnSpecs(JetFormatBase format, IReadOnlyList specs) - { - if (specs.Count > MaxColumnsPerTable) - throw new NotSupportedException( - $"Table has {specs.Count} columns; a table can have at most {MaxColumnsPerTable}."); - - var ids = new HashSet(); - var names = new HashSet(StringComparer.OrdinalIgnoreCase); - long fixedBytes = 0; - int variableColumns = 0, highWater = -1; - for (int i = 0; i < specs.Count; i++) - { - ColumnSpec spec = specs[i]; - ValidateNameLength(spec.Name, "Column"); - if (!names.Add(spec.Name)) - throw new NotSupportedException($"Column name '{spec.Name}' is used more than once."); - int id = spec.ColumnId ?? i; - if (id is < 0 or >= MaxColumnsPerTable) - throw new NotSupportedException( - $"Column '{spec.Name}' has id {id}; column ids range from 0 through {MaxColumnsPerTable - 1}."); - if (!ids.Add(id)) - throw new NotSupportedException($"Column id {id} is used more than once."); - if (spec.Length is < 0 or > ushort.MaxValue) - throw new NotSupportedException( - $"Column '{spec.Name}' has byte length {spec.Length}, which does not fit the TDEF field."); - RecordLayout.ValidateFieldWidth(spec.Name, spec.Type, spec.Length); - JetDataTypeVersions.EnsureStorable(spec.Type, format.Version, spec.Name); - ValidateNumericPrecision(spec); - if (spec.IsFixedLength && spec.Type != JetDataType.Boolean) - fixedBytes += spec.Length; - if (!spec.IsFixedLength) variableColumns++; - highWater = Math.Max(highWater, id); - } - - // The fixed region used to be checked only against the TDEF's 2-byte offset fields (65535). ACE's real - // limit is far tighter — the widest record the declaration allows must still be storable — and it - // subsumes that one, since no column may now exceed 510 bytes. Without this a plain CreateTable of - // 252 GUID columns writes a database Access will not open at all. - RecordLayout.ValidateRecordFits(null, (int)fixedBytes, variableColumns, highWater + 1, format); - } - - private static void ValidateIndexAndLongValueSpecs( - IReadOnlyList columns, - IReadOnlyList indexes, - IReadOnlyList logical, - IReadOnlyList longValueColumns) - { - if (logical.Count > MaxIndexesPerTable) - throw new NotSupportedException( - $"Table has {logical.Count} logical indexes; a table can have at most {MaxIndexesPerTable}."); - - var columnByName = columns.ToDictionary(c => c.Name, StringComparer.OrdinalIgnoreCase); - foreach (IndexSpec index in indexes) - { - ValidateNameLength(index.Name, "Index"); - if (index.Columns.Count > IndexBlockFormat.MaxColumns) - throw new NotSupportedException( - $"Index '{index.Name}' spans {index.Columns.Count} columns; an index can span at most {IndexBlockFormat.MaxColumns}."); - foreach (string column in index.Columns) - if (!columnByName.ContainsKey(column)) - throw new NotSupportedException($"Index '{index.Name}' refers to unknown column '{column}'."); - ValidateUsageMapPointer(index.UsageMapRow, index.UsageMapPage, $"index '{index.Name}'", allowNull: true); - } - // Duplicate index names, on the create path. ACE rejects them, and every downstream lookup resolves an - // index by name, so two blocks sharing one would make DROP INDEX remove an arbitrary one of them. The - // column list has had this check all along (see below); the index list had only a length check. - var indexNames = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (LogicalIndexSpec index in logical) - { - ValidateNameLength(index.Name, "Logical index"); - if (!indexNames.Add(index.Name)) - throw new NotSupportedException($"Index name '{index.Name}' is used more than once."); - } - - var columnById = columns.ToDictionary(c => c.ColumnId); - var seen = new HashSet(); - foreach (LongValueColumnSpec value in longValueColumns) - { - if (!seen.Add(value.ColumnId)) - throw new NotSupportedException($"Long-value column id {value.ColumnId} has more than one usage-map entry."); - // A calculated column with a Memo result is the exception: ACE declares it Text with length 0 and - // still gives it long-value maps, so the declared type does not decide this. The reader had to - // drop the same assumption — a guard that kept it rejected the TDEF, and since the catalog loads - // every TDEF that made the whole database unopenable (§3.4a). - if (!columnById.TryGetValue(value.ColumnId, out ColumnDef? column) - || (column.Type is not (JetDataType.Memo or JetDataType.Ole) && !column.IsCalculated)) - throw new NotSupportedException( - $"Long-value usage-map entry {value.ColumnId} does not identify a Memo/OLE column."); - ValidateUsageMapPointer(value.UsedRow, value.MapPage, $"long-value column '{column.Name}' owned map", allowNull: false); - ValidateUsageMapPointer(value.FreeRow, value.MapPage, $"long-value column '{column.Name}' free map", allowNull: false); - } - } - - private static void ValidateUsageMapPointer(int row, int page, string owner, bool allowNull) - { - if (allowNull && row == 0 && page == 0) return; - if (row is < 0 or > byte.MaxValue || page is <= 0 or > 0xFFFFFF) - throw new NotSupportedException( - $"The {owner} usage-map pointer ({row}, {page}) does not fit its 1-byte row / 3-byte page fields."); - } - - private static void ValidateNameLength(string name, string kind) - { - int length = Encoding.Unicode.GetByteCount(name); - if (length is 0 or > MaxNameBytes) - throw new NotSupportedException( - $"{kind} name is {length} UTF-16 bytes; a name must be 1 through {MaxNameBytes / 2} characters."); - } - - private static int DefinitionSize( - JetFormatBase format, - List columns, - IReadOnlyList indexes, - IReadOnlyList logical, - IReadOnlyList longValueColumns) - { - long size = format.TdefRealIndexBlockOffset - + (long)indexes.Count * format.RealIndexEntrySize - + (long)columns.Count * format.ColumnDescriptorSize - + columns.Sum(c => 2L + Encoding.Unicode.GetByteCount(c.Name)) - + (long)indexes.Count * IndexBlockFormat.DataBlockSize - + (long)logical.Count * IndexBlockFormat.InfoBlockSize - + logical.Sum(i => 2L + Encoding.Unicode.GetByteCount(i.Name)) - + (long)longValueColumns.Count * 10 - + 2; - if (size > TdefChainReader.MaxDefinitionLength) - throw new NotSupportedException( - $"The serialized table definition requires {size} bytes; LibRed's validated TDEF budget is {TdefChainReader.MaxDefinitionLength}."); - return (int)size; - } - - /// Writes the index structures and returns the offset just past them (the definition end). - private static int WriteIndexes(byte[] page, JetFormatBase format, List columns, IReadOnlyList indexes, IReadOnlyList logical, IReadOnlyList longValueColumns, int dataBlockStart) - { - var columnIdByName = columns.ToDictionary(c => c.Name, c => c.ColumnId, StringComparer.OrdinalIgnoreCase); - - // The index-data block (§3.5) has a fixed array of exactly IndexBlockFormat.MaxColumns column slots and no - // count field, so an index — hence any PRIMARY KEY / UNIQUE / FOREIGN KEY — spans at most that - // many columns. Reject an over-wide index rather than silently truncating it. - foreach (IndexSpec ix in indexes) - if (ix.Columns.Count > IndexBlockFormat.MaxColumns) - throw new NotSupportedException( - $"Index '{ix.Name}' spans {ix.Columns.Count} columns; an index, and a key built on one, can span at most {IndexBlockFormat.MaxColumns}."); - - // 1. Index-data blocks: columns, root page, unique flag. - for (int i = 0; i < indexes.Count; i++) - { - IndexSpec index = indexes[i]; - int block = dataBlockStart + i * IndexBlockFormat.DataBlockSize; - - BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(block, 4), IndexBlockFormat.DataMarker); - for (int slot = 0; slot < IndexBlockFormat.MaxColumns; slot++) - { - int entry = block + IndexBlockFormat.ColumnsOffset + slot * IndexBlockFormat.ColumnSlotSize; - if (slot < index.Columns.Count) - { - BinaryPrimitives.WriteInt16LittleEndian(page.AsSpan(entry, 2), (short)columnIdByName[index.Columns[slot]]); - page[entry + 2] = IndexBlockFormat.ColumnAscending; - } - else - { - BinaryPrimitives.WriteInt16LittleEndian(page.AsSpan(entry, 2), IndexBlockFormat.ColumnUnused); - } - } - page[block + IndexBlockFormat.UsageMapRowOffset] = (byte)index.UsageMapRow; - page[block + IndexBlockFormat.UsageMapRowOffset + 1] = (byte)index.UsageMapPage; - page[block + IndexBlockFormat.UsageMapRowOffset + 2] = (byte)(index.UsageMapPage >> 8); - page[block + IndexBlockFormat.UsageMapRowOffset + 3] = (byte)(index.UsageMapPage >> 16); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(block + IndexBlockFormat.RootPageOffset, 4), index.RootPage); - ushort flags = IndexFlags.AlwaysSet; - if (index.IsUnique) flags |= IndexFlags.Unique; - if (index.IsPrimaryKey) flags |= IndexFlags.Required; - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(block + IndexBlockFormat.FlagsOffset, 2), flags); - } - - // 2. Index-info blocks (one per logical index) and 3. their names. Without explicit logical - // specs each data block maps 1:1 to a plain info block (back-compat); with them, relationship - // blocks are included and stored name-sorted (matching Access). - int infoStart = dataBlockStart + indexes.Count * IndexBlockFormat.DataBlockSize; - for (int i = 0; i < logical.Count; i++) - { - LogicalIndexSpec li = logical[i]; - int block = infoStart + i * IndexBlockFormat.InfoBlockSize; - BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(block + IndexBlockFormat.InfoMarkerOffset, 4), JetFormatBase.TdefRecordMarker); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(block + IndexBlockFormat.InfoNumberOffset, 4), li.Number); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(block + IndexBlockFormat.InfoDataNumberOffset, 4), li.DataOrdinal); - page[block + IndexBlockFormat.InfoFkTypeOffset] = li.FkType; - BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(block + IndexBlockFormat.InfoFkNumberOffset, 4), li.FkNumber); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(block + IndexBlockFormat.InfoFkTablePageOffset, 4), li.FkTablePage); - page[block + IndexBlockFormat.InfoUpdateActionOffset] = li.UpdateAction; - page[block + IndexBlockFormat.InfoDeleteActionOffset] = li.DeleteAction; - page[block + IndexBlockFormat.InfoTypeOffset] = li.Type; - } - - int namePos = infoStart + logical.Count * IndexBlockFormat.InfoBlockSize; - foreach (LogicalIndexSpec li in logical) - { - byte[] name = System.Text.Encoding.Unicode.GetBytes(li.Name); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, 2), (ushort)name.Length); - namePos += 2; - name.CopyTo(page.AsSpan(namePos)); - namePos += name.Length; - } - - // After the index names comes a per-long-value-column (memo/OLE) usage-map list (spec §3.3.2): - // one 10-byte entry {col_num:2, used_pages:4, free_pages:4} per column, in ascending column - // order, terminated by col_num 0xFFFF. Each pointer is a 1-byte usage-map row + 3-byte page. - // The terminator is mandatory even when the list is empty; the definition length includes it. - foreach (LongValueColumnSpec lv in longValueColumns.OrderBy(l => l.ColumnId)) - { - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, 2), (ushort)lv.ColumnId); - WriteUsageMapPointer(page, namePos + 2, lv.UsedRow, lv.MapPage); - WriteUsageMapPointer(page, namePos + 6, lv.FreeRow, lv.MapPage); - namePos += 10; - } - - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, 2), 0xFFFF); - namePos += 2; - return namePos; - } - - /// Writes a 4-byte usage-map pointer: a 1-byte record row followed by a 3-byte page. - private static void WriteUsageMapPointer(byte[] page, int offset, int row, int mapPage) - { - page[offset] = (byte)row; - page[offset + 1] = (byte)mapPage; - page[offset + 2] = (byte)(mapPage >> 8); - page[offset + 3] = (byte)(mapPage >> 16); - } - - private static List ResolveColumns(JetFormatBase format, IReadOnlyList specs, Collation collation) - { - // A column's id is its declaration position unless the spec pins one explicitly (the catalog tables - // DatabaseCreator writes). Variable columns are addressed in ascending column-id order. - int EffectiveId(int i) => specs[i].ColumnId ?? i; - - var variableRank = new Dictionary(); - int rank = 0; - foreach (int i in Enumerable.Range(0, specs.Count).Where(i => !specs[i].IsFixedLength).OrderBy(EffectiveId)) - variableRank[i] = rank++; - - // Descriptor offset 7 ("variable-table index") = number of variable columns with a smaller column-id. - // Access stores this on EVERY column, fixed ones included, where it is NOT 0 — measured on ACE's own - // ADD COLUMN, which writes 2 for a LONG added to (K LONG, A TEXT, B TEXT). ACE does still read a file - // that has 0 there, so this is parity rather than a hard requirement. - int VarTableIndex(int i) => - Enumerable.Range(0, specs.Count).Count(j => !specs[j].IsFixedLength && EffectiveId(j) < EffectiveId(i)); - - // Fixed-data offsets are assigned in ascending column-id order, NOT declaration order — Access lays the - // fixed columns out by column id (e.g. MSysACEs stores ObjectId(id0) at offset 0 even though its - // descriptor is written after ACM). Only differs from declaration order when ids are reordered (MSys*). - bool Occupies(int i) => specs[i].IsFixedLength && specs[i].Type != JetDataType.Boolean; - var fixedOffsets = new Dictionary(); - int running = 0; - foreach (int i in Enumerable.Range(0, specs.Count).Where(Occupies).OrderBy(EffectiveId)) - { - fixedOffsets[i] = running; - running += specs[i].Length; - } - - var columns = new List(specs.Count); - for (int i = 0; i < specs.Count; i++) - { - ColumnSpec s = specs[i]; - // Booleans live in the null bitmap and occupy no fixed-data bytes, so they don't - // advance the fixed offset (matching how the row codec skips them). - bool occupiesFixedData = s.IsFixedLength && s.Type != JetDataType.Boolean; - columns.Add(new ColumnDef - { - Name = s.Name, - Type = s.Type, - Index = i, - ColumnId = EffectiveId(i), - Length = s.Length, - FixedOffset = occupiesFixedData ? fixedOffsets[i] : 0, - VariableIndex = s.IsFixedLength ? -1 : variableRank[i], - VariableTableIndex = VarTableIndex(i), - IsFixedLength = s.IsFixedLength, - IsAutoNumber = s.IsAutoNumber, - SupportsCompressedUnicode = s.SupportsCompressedUnicode, - IsCalculated = s.CalculatedExpression is not null, - CalculatedExpression = s.CalculatedExpression, - CalculatedResultType = s.CalculatedResultType, - SystemFlags = s.SystemFlags, - IsEngineColumn = s.IsEngineColumn || s.SystemFlags != 0, - ExtendedFlags = s.ExtendedFlags, - Precision = EffectivePrecision(s), - Scale = s.Scale, - // Numeric columns carry no collation (their 0x0B/0x0C bytes are precision/scale); every - // other column inherits the database's collating order. - Collation = s.Type == JetDataType.FixedPoint ? Collation.GeneralLegacy : collation, - }); - } - return columns; - } - - private static void WriteColumnDescriptors(byte[] page, JetFormatBase format, List columns, int columnBlock) - { - for (int i = 0; i < columns.Count; i++) - BuildColumnDescriptor(columns[i], format).CopyTo(page.AsSpan(columnBlock + i * format.ColumnDescriptorSize)); - } - - /// An AutoNumber counting by zero would hand every row the same id, and ACE's own DDL refuses it - /// ("Invalid argument" to COUNTER(1, 0)). An omitted increment is 1, all the way from the SQL - /// layer's IdentitySpec, so a zero here is one the caller asked for. - internal static NotSupportedException ZeroIncrement(string column) => - new($"AutoNumber column '{column}' cannot have an increment of 0: every row would take the same id. " - + "ACE refuses the same COUNTER, and an omitted increment is 1."); - - /// Builds one column's fixed-size (25-byte Jet4) descriptor. Shared by CREATE TABLE and - /// ALTER TABLE ADD COLUMN. - public static byte[] BuildColumnDescriptor(ColumnDef c, JetFormatBase format) - { - byte[] d = new byte[format.ColumnDescriptorSize]; - d[format.ColumnTypeOffset] = (byte)c.Type; - BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(ColumnRecordMarkerOffset, 2), (ushort)JetFormatBase.TdefRecordMarker); - BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnNumberOffset, 2), (ushort)c.ColumnId); - // Offset 0x09 repeats the id on a user column. Every creator does it — ACE's SQL DDL, DAO's object - // model and DAO-executed SQL — and every user table in every fixture carries it, while only the - // engine's own bootstrap tables (MSysObjects and friends) leave it zero, which is what a system - // column keeps here. An earlier comment claimed real files store zero; that had been read off the - // system tables alone. - BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnSecondaryNumberOffset, 2), - (ushort)(c.IsEngineColumn ? 0 : c.ColumnId)); - // Offset 7 = variable-table index (count of variable columns with a smaller id), stored on fixed columns - // too. Prefer the precomputed value; fall back to the legacy rule (0 for fixed) when unset (ADD COLUMN). - BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnVariableIndexOffset, 2), - (ushort)(c.VariableTableIndex >= 0 ? c.VariableTableIndex : (c.IsFixedLength ? 0 : c.VariableIndex))); - WriteLocaleUnion(d, c.Type, c.Precision, c.Scale, c.Collation, format); - // Compose the flag byte (0x0F) from every bit a user column models, plus the catalog bits a created system - // column asks for; likewise the extended-flag byte (0x10), plus the unmodelled bits an attachment's - // value columns ask for. - byte flags = (byte)( - (c.IsUpdatable ? JetFormatBase.ColumnFlagUpdatable : 0) - | (c.IsFixedLength ? JetFormatBase.ColumnFlagFixedLength : 0) - | (c.IsAutoNumber ? JetFormatBase.ColumnFlagAutoNumber : 0) - | (c.IsGuidAutoNumber ? JetFormatBase.ColumnFlagGuidAutoNumber : 0) - | (c.IsHyperlink ? JetFormatBase.ColumnFlagHyperlink : 0)); - d[format.ColumnFlagsOffset] = (byte)(flags | c.SystemFlags); - - byte extFlags = (byte)( - (c.SupportsCompressedUnicode ? JetFormatBase.ColumnExtFlagCompressedUnicode : 0) - | (c.IsCalculated ? JetFormatBase.ColumnExtFlagCalculated : 0)); - d[format.ColumnExtendedFlagsOffset] = (byte)(extFlags | c.ExtendedFlags); - - BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnFixedOffsetOffset, 2), (ushort)c.FixedOffset); - BinaryPrimitives.WriteUInt16LittleEndian(d.AsSpan(format.ColumnLengthOffset, 2), (ushort)c.Length); - return d; - } - - /// Writes descriptor bytes 0x0B–0x0E, which are a union keyed by column type. - /// Shared with the in-place ALTER (TableCreator.EditTargetDescriptor), which edits an existing - /// descriptor in place: it must rewrite the whole union on every retype, or a column that stops being - /// DECIMAL keeps its precision and scale sitting where the LANGID belongs — read back as a - /// nonexistent collating order on what is now a text column. - internal static void WriteLocaleUnion( - Span d, JetDataType type, byte precision, byte scale, Collation collation, JetFormatBase format) - { - if (type == JetDataType.FixedPoint) - { - // Decimal/Numeric: precision and scale take the LANGID bytes. 0x0D/0x0E are left as they stand — - // ACE does not clear them on a retype INTO decimal, so neither does LibRed. - d[format.ColumnPrecisionOffset] = precision; - d[format.ColumnScaleOffset] = scale; - } - else if (type == JetDataType.Complex) - { - // A complex (multi-value / attachment) column stores its MSysComplexColumns key here (page-02b - // §3.4) — there is nothing to collate, since the values are rows of the flat table and carry their - // own collations. Writing a LANGID over it severs the column from its values, so the bytes stand. - } - else if (type == JetDataType.DateTimeExtended) - { - // Date/Time Extended is 42 bytes of ASCII with nothing to collate, and ACE writes only the LOW - // byte of the LANGID here, clearing the sublanguage half — the primary language id on its own, - // with sort id and version zero. Measured across five collating orders: 0x0409 and 0x0809 both - // give 0x0009, 0x0407 gives 0x0007, 0x040E 0x000E, 0x041D 0x001D, while a Text column in the - // same table carries the full LANGID each time. (On an en-US database this looks like a - // constant 0x0009, which is how it was first mis-read.) - BinaryPrimitives.WriteUInt16LittleEndian( - d.Slice(format.ColumnLocaleOffset, 2), (ushort)((ushort)collation.Order & 0x00FF)); - d[format.ColumnCollationSortIdOffset] = 0; - d[format.ColumnCollationVersionOffset] = 0; - } - else - { - // 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.Slice(format.ColumnLocaleOffset, 2), (ushort)collation.Order); - d[format.ColumnCollationSortIdOffset] = collation.SortId; - d[format.ColumnCollationVersionOffset] = collation.Version; - } - } - - private static int WriteColumnNames(byte[] page, JetFormatBase format, List columns, int namePos) - { - _ = format; - foreach (ColumnDef c in columns) - { - byte[] name = Encoding.Unicode.GetBytes(c.Name); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, 2), (ushort)name.Length); - namePos += 2; - name.CopyTo(page.AsSpan(namePos)); - namePos += name.Length; - } - return namePos; - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs b/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs index d02eedf3..2c897850 100644 --- a/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs +++ b/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs @@ -20,7 +20,7 @@ public sealed record ViewColumnSpec(string Expression, string? Alias); /// A declared parameter of a stored (procedure) query: its name and Jet type code, stored as an MSysQueries /// Attribute=2 row (Name1 = name, Flag = ). The row's LvExtra carries /// the declared facets, and Access renders the query's PARAMETERS clause from it — see -/// for which types have one and how a decimal's precision +/// for which types have one and how a decimal's precision /// and scale pack into the single value. /// public sealed record ViewParameterSpec(string Name, byte TypeCode, int? Size = null, int? Scale = null); @@ -73,11 +73,6 @@ public sealed record ActionQuerySpec( string? DeleteTarget = null, bool OwnerAccess = false); -/// A stored action query read back from the catalog. is the reconstructed, -/// executable statement when LibRed supports the kind; otherwise it is null and -/// explains why executing it throws. -public sealed record StoredActionQuery(string? Sql, string? UnsupportedReason); - /// A parameter a stored query declares, in declaration order. is the Jet type /// Access recorded for it, or null for its untyped parameter — the one Access renders as the keyword /// Value. is a text parameter's declared length and diff --git a/src/LibRed/LibRed.Core/Crypto/AgileEncryption.cs b/src/LibRed/LibRed.Core/Crypto/AgileEncryption.cs index 45c261bb..682ef82e 100644 --- a/src/LibRed/LibRed.Core/Crypto/AgileEncryption.cs +++ b/src/LibRed/LibRed.Core/Crypto/AgileEncryption.cs @@ -1,3 +1,5 @@ +using LibRed.Formats; +using LibRed.Pages; using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; @@ -14,18 +16,23 @@ namespace LibRed.Crypto; /// /// The EncryptionInfo XML descriptor lives in page 0 (after the masked header). The data key is /// recovered from the password + descriptor; each page N is then decrypted with -/// IV = Hash(keyDataSalt ‖ (LE32(N) ⊕ dbEncodingKey)). The ⊕ dbEncodingKey (the 4-byte key at +/// IV = Hash(keyDataSalt ‖ LE32(N ⊕ databaseKey)). The ⊕ databaseKey (the 4-byte key at /// page-0 0x3E) is Access's deviation from stock Agile. Verified byte-for-byte against a real /// password-protected file (decrypted pages match the unencrypted twin). /// public sealed class AgileEncryption : IPageCodec { - private const int EncryptionInfoLengthOffset = 0x299; - private const int EncryptionInfoOffset = 0x29B; + /// An Agile EncryptionInfo opens with version 4.4 and , then the XML + /// (verified against a real file). + internal static ReadOnlySpan AgilePrefix => [0x04, 0x00, 0x04, 0x00, (byte)EncryptionInfoFlags.Agile, 0x00, 0x00, 0x00]; + + + // The one profile Access writes and this reads: AES-256-CBC (16-byte blocks), SHA-512, 16-byte salts and + // 100000 spins. Create emits exactly these, and ValidateSupportedProfile refuses anything else. private const int SupportedBlockSize = 16; private const int SupportedKeyBits = 256; private const int SupportedSpinCount = 100000; - private static ReadOnlySpan AgilePrefix => [0x04, 0x00, 0x04, 0x00, 0x40, 0x00, 0x00, 0x00]; + private const int SupportedSaltSize = 16; // Block keys that salt the key-derivation for each purpose (MS-OFFCRYPTO §2.3.4.13/§2.3.4.14). private static readonly byte[] BlockVerifierHashInput = [0xFE, 0xA7, 0xD2, 0x76, 0x3B, 0x4B, 0x9E, 0x79]; @@ -35,15 +42,15 @@ public sealed class AgileEncryption : IPageCodec private readonly byte[] _secretKey; // the data-encryption key private readonly byte[] _keyDataSalt; private readonly int _blockSize; - private readonly byte[] _encodingKey; // 4-byte database key (page-0 0x3E) - private readonly HashKind _hash; // the descriptor's hashAlgorithm (also used per-page for the IV) + private readonly int _databaseKey; // page-0 0x3E + private readonly HashAlgorithmName _hash; // the descriptor's hashAlgorithm (also used per-page for the IV) - private AgileEncryption(byte[] secretKey, byte[] keyDataSalt, int blockSize, byte[] encodingKey, HashKind hash) + private AgileEncryption(byte[] secretKey, byte[] keyDataSalt, int blockSize, int databaseKey, HashAlgorithmName hash) { _secretKey = secretKey; _keyDataSalt = keyDataSalt; _blockSize = blockSize; - _encodingKey = encodingKey; + _databaseKey = databaseKey; _hash = hash; } @@ -52,15 +59,17 @@ private AgileEncryption(byte[] secretKey, byte[] keyDataSalt, int blockSize, byt /// (the file is not encrypted). Throws if the file is encrypted but no/incorrect password is supplied, or if /// the scheme is not the verified Agile (AES + SHA-512) configuration. /// - public static AgileEncryption? TryCreate(ReadOnlySpan page0, int databaseKey, string? password) + public static AgileEncryption? TryCreate(ReadOnlySpan page0, int databaseKey, string? password, + JetFormatBase format) { + ArgumentNullException.ThrowIfNull(format); if (databaseKey == 0) return null; // unencrypted // Detection keys off the actual Agile EncryptionInfo descriptor, not merely the nonzero key byte: // a legacy RC4 scheme (or a synthetic file with an incidental nonzero 0x3E) has no such descriptor // and is treated as unencrypted here rather than mis-flagged. - XElement? enc = LocateEncryptionInfo(page0); + XElement? enc = LocateEncryptionInfo(page0, format); if (enc is null) return null; if (password is null) @@ -72,7 +81,7 @@ private AgileEncryption(byte[] secretKey, byte[] keyDataSalt, int blockSize, byt XElement encKey = enc.Descendants().First(e => e.Name.LocalName == "encryptedKey"); RequireAes(keyData, encKey); - HashKind hash = ParseHash((string)encKey.Attribute("hashAlgorithm")!); + HashAlgorithmName hash = ParseHash((string)encKey.Attribute("hashAlgorithm")!); byte[] keyDataSalt = B64(keyData, "saltValue"); int blockSize = IntAttribute(keyData, "blockSize"); @@ -83,8 +92,8 @@ private AgileEncryption(byte[] secretKey, byte[] keyDataSalt, int blockSize, byt int keyBytes = keyBits / 8; ValidateSupportedProfile(keyData, encKey, hash, blockSize, keyBits, spinCount); - RequireLength(keyDataSalt, 16, "keyData saltValue"); - RequireLength(pwdSalt, 16, "encryptedKey saltValue"); + RequireLength(keyDataSalt, SupportedSaltSize, "keyData saltValue"); + RequireLength(pwdSalt, SupportedSaltSize, "encryptedKey saltValue"); // Cross-check the descriptor's declared sizes against reality: the salt bytes must be saltSize long, the // hash must match hashSize, and both elements must name the same hash. A disagreement means a malformed or @@ -93,7 +102,7 @@ private AgileEncryption(byte[] secretKey, byte[] keyDataSalt, int blockSize, byt VerifyDeclaredSizes(encKey, hash, pwdSalt.Length); byte[] hspin = SpinHash(hash, pwdSalt, password, spinCount); - byte[] DeriveKey(byte[] blockKey) => Fit(Hash(hash, hspin, blockKey), keyBytes); + byte[] DeriveKey(byte[] blockKey) => Fit(CryptographicOperations.HashData(hash, [.. hspin, .. blockKey]), keyBytes); // Verify the password before trusting anything: SHA(verifierInput) must equal verifierValue. byte[] encryptedVerifierInput = B64(encKey, "encryptedVerifierHashInput"); @@ -103,18 +112,16 @@ private AgileEncryption(byte[] secretKey, byte[] keyDataSalt, int blockSize, byt RequireLength(encryptedVerifierValue, HashSize(hash), "encryptedVerifierHashValue"); RequireLength(encryptedKeyValue, keyBytes, "encryptedKeyValue"); - byte[] verifierInput = AesCbcDecrypt(DeriveKey(BlockVerifierHashInput), pwdSalt, encryptedVerifierInput); - byte[] verifierValue = AesCbcDecrypt(DeriveKey(BlockVerifierHashValue), pwdSalt, encryptedVerifierValue); - byte[] check = Hash(hash, verifierInput); + byte[] verifierInput = AesCbc(DeriveKey(BlockVerifierHashInput), pwdSalt, encryptedVerifierInput, encrypt: false); + byte[] verifierValue = AesCbc(DeriveKey(BlockVerifierHashValue), pwdSalt, encryptedVerifierValue, encrypt: false); + byte[] check = CryptographicOperations.HashData(hash, verifierInput); if (!CryptographicOperations.FixedTimeEquals(verifierValue, check)) throw new UnauthorizedAccessException("Incorrect database password."); // Recover the data-encryption key. - byte[] secretKey = AesCbcDecrypt(DeriveKey(BlockKeyValue), pwdSalt, encryptedKeyValue); + byte[] secretKey = AesCbc(DeriveKey(BlockKeyValue), pwdSalt, encryptedKeyValue, encrypt: false); - byte[] encodingKey = new byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(encodingKey, databaseKey); - return new AgileEncryption(secretKey, keyDataSalt, blockSize, encodingKey, hash); + return new AgileEncryption(secretKey, keyDataSalt, blockSize, databaseKey, hash); } catch (NotSupportedException) { throw; } catch (UnauthorizedAccessException) { throw; } @@ -131,49 +138,40 @@ private AgileEncryption(byte[] secretKey, byte[] keyDataSalt, int blockSize, byt /// run forward — generate salts + a random data key, wrap it, and emit the descriptor Access writes. internal static (byte[] Descriptor, AgileEncryption Codec) Create(string password, int databaseKey) { - const HashKind hash = HashKind.Sha512; - const int keyBytes = 32, blockSize = 16, spinCount = 100000; - byte[] keyDataSalt = RandomBytes(16), pwdSalt = RandomBytes(16), secretKey = RandomBytes(keyBytes); + HashAlgorithmName hash = HashAlgorithmName.SHA512; + const int keyBytes = SupportedKeyBits / 8; + byte[] keyDataSalt = RandomNumberGenerator.GetBytes(SupportedSaltSize); + byte[] pwdSalt = RandomNumberGenerator.GetBytes(SupportedSaltSize); + byte[] secretKey = RandomNumberGenerator.GetBytes(keyBytes); - byte[] hspin = SpinHash(hash, pwdSalt, password, spinCount); - byte[] DeriveKey(byte[] blockKey) => Fit(Hash(hash, hspin, blockKey), keyBytes); + byte[] hspin = SpinHash(hash, pwdSalt, password, SupportedSpinCount); + byte[] DeriveKey(byte[] blockKey) => Fit(CryptographicOperations.HashData(hash, [.. hspin, .. blockKey]), keyBytes); - byte[] verifierInput = RandomBytes(16); - byte[] encVerifierInput = AesCbcEncrypt(DeriveKey(BlockVerifierHashInput), pwdSalt, verifierInput); - byte[] encVerifierValue = AesCbcEncrypt(DeriveKey(BlockVerifierHashValue), pwdSalt, Hash(hash, verifierInput)); - byte[] encKeyValue = AesCbcEncrypt(DeriveKey(BlockKeyValue), pwdSalt, secretKey); + byte[] verifierInput = RandomNumberGenerator.GetBytes(SupportedBlockSize); + byte[] encVerifierInput = AesCbc(DeriveKey(BlockVerifierHashInput), pwdSalt, verifierInput, encrypt: true); + byte[] encVerifierValue = AesCbc(DeriveKey(BlockVerifierHashValue), pwdSalt, + CryptographicOperations.HashData(hash, verifierInput), encrypt: true); + byte[] encKeyValue = AesCbc(DeriveKey(BlockKeyValue), pwdSalt, secretKey, encrypt: true); string b64K = Convert.ToBase64String(keyDataSalt), b64P = Convert.ToBase64String(pwdSalt); + string profile = $"saltSize=\"{SupportedSaltSize}\" blockSize=\"{SupportedBlockSize}\" keyBits=\"{SupportedKeyBits}\" " + + $"hashSize=\"{HashSize(hash)}\" cipherAlgorithm=\"AES\" cipherChaining=\"ChainingModeCBC\" hashAlgorithm=\"SHA512\""; string xml = "" + "" + - $"" + + $"" + "" + - "" + ""; - // EncryptionInfo = version 4.4 + flags 0x40 + the UTF-8 XML (verified against a real file). - byte[] descriptor = Concat([0x04, 0x00, 0x04, 0x00, 0x40, 0x00, 0x00, 0x00], Encoding.UTF8.GetBytes(xml)); - byte[] encodingKey = BitConverter.GetBytes(databaseKey); - return (descriptor, new AgileEncryption(secretKey, keyDataSalt, blockSize, encodingKey, hash)); + byte[] descriptor = [.. AgilePrefix, .. Encoding.UTF8.GetBytes(xml)]; + return (descriptor, new AgileEncryption(secretKey, keyDataSalt, SupportedBlockSize, databaseKey, hash)); } - private static byte[] AesCbcEncrypt(byte[] key, byte[] iv, byte[] data) - { - using var aes = Aes.Create(); - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.None; - aes.Key = key; - aes.IV = iv.Length == 16 ? iv : Fit(iv, 16); - return aes.EncryptCbc(data, aes.IV, PaddingMode.None); - } - - private static byte[] RandomBytes(int n) { byte[] b = new byte[n]; RandomNumberGenerator.Fill(b); return b; } - public void DecryptPage(int pageNumber, Span page) => Transform(pageNumber, page, decrypt: true); /// Encrypts a page — the inverse of (AES-CBC encrypt with the same @@ -185,39 +183,31 @@ private void Transform(int pageNumber, Span page, bool decrypt) if (pageNumber == 0) return; // header page is not encrypted - // blockKey = LE32(pageNumber) XOR dbEncodingKey; IV = Hash(keyDataSalt ‖ blockKey), truncated to blockSize. - Span blockKey = stackalloc byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(blockKey, pageNumber); - for (int i = 0; i < 4; i++) blockKey[i] ^= _encodingKey[i]; - - byte[] iv = Fit(Hash(_hash, _keyDataSalt, blockKey.ToArray()), _blockSize); + // blockKey = LE32(pageNumber XOR databaseKey); IV = Hash(keyDataSalt ‖ blockKey), truncated to blockSize. + byte[] blockKey = new byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(blockKey, pageNumber ^ _databaseKey); + byte[] iv = Fit(CryptographicOperations.HashData(_hash, [.. _keyDataSalt, .. blockKey]), _blockSize); - using var aes = Aes.Create(); - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.None; - aes.Key = _secretKey; - aes.IV = iv; // Transform in place (length is a whole number of AES blocks — a page). - byte[] result = decrypt ? aes.DecryptCbc(page, iv, PaddingMode.None) : aes.EncryptCbc(page, iv, PaddingMode.None); - result.CopyTo(page); + AesCbc(_secretKey, iv, page, encrypt: !decrypt).CopyTo(page); } // --- helpers --- - private enum HashKind { Sha1, Sha256, Sha384, Sha512 } - - private static XElement? LocateEncryptionInfo(ReadOnlySpan page0) + private static XElement? LocateEncryptionInfo(ReadOnlySpan page0, JetFormatBase format) { - if (page0.Length < EncryptionInfoOffset) + int descriptorOffset = format.EncryptionInfoOffset; + if (page0.Length < descriptorOffset) return null; - int length = BinaryPrimitives.ReadUInt16LittleEndian(page0.Slice(EncryptionInfoLengthOffset, 2)); + int length = DatabaseDefinitionPage.ReadEncryptionInfoLength(page0, format); if (length == 0) return null; - if (length > page0.Length - EncryptionInfoOffset) + if (length > page0.Length - descriptorOffset) throw new NotSupportedException("The Agile EncryptionInfo descriptor extends past page 0."); - ReadOnlySpan descriptor = page0.Slice(EncryptionInfoOffset, length); - if (descriptor.Length < AgilePrefix.Length || !descriptor[..AgilePrefix.Length].SequenceEqual(AgilePrefix)) + ReadOnlySpan descriptor = page0.Slice(descriptorOffset, length); + ReadOnlySpan prefix = AgilePrefix; + if (descriptor.Length < prefix.Length || !descriptor[..prefix.Length].SequenceEqual(prefix)) return null; // a binary Office-Standard descriptor may occupy the same framed field try @@ -243,17 +233,17 @@ private static int IntAttribute(XElement element, string name) } private static void ValidateSupportedProfile( - XElement keyData, XElement encKey, HashKind hash, int blockSize, int keyBits, int spinCount) + XElement keyData, XElement encKey, HashAlgorithmName hash, int blockSize, int keyBits, int spinCount) { - if (hash != HashKind.Sha512 - || ParseHash((string)keyData.Attribute("hashAlgorithm")!) != HashKind.Sha512) + if (hash != HashAlgorithmName.SHA512 + || ParseHash((string)keyData.Attribute("hashAlgorithm")!) != HashAlgorithmName.SHA512) throw new NotSupportedException("Only the Access Agile SHA-512 profile is supported."); if (blockSize != SupportedBlockSize || IntAttribute(encKey, "blockSize") != SupportedBlockSize) - throw new NotSupportedException("Only the Access Agile 16-byte AES block size is supported."); + throw new NotSupportedException($"Only the Access Agile {SupportedBlockSize}-byte AES block size is supported."); if (keyBits != SupportedKeyBits || IntAttribute(keyData, "keyBits") != SupportedKeyBits) - throw new NotSupportedException("Only the Access Agile AES-256 profile is supported."); + throw new NotSupportedException($"Only the Access Agile AES-{SupportedKeyBits} profile is supported."); if (spinCount != SupportedSpinCount) - throw new NotSupportedException("Only the Access Agile 100000-spin profile is supported."); + throw new NotSupportedException($"Only the Access Agile {SupportedSpinCount}-spin profile is supported."); } private static void RequireLength(byte[] value, int expected, string name) @@ -273,18 +263,18 @@ private static void RequireAes(XElement keyData, XElement encKey) } } - private static int HashSize(HashKind kind) => kind switch + private static int HashSize(HashAlgorithmName hash) => hash.Name switch { - HashKind.Sha1 => 20, - HashKind.Sha256 => 32, - HashKind.Sha384 => 48, - HashKind.Sha512 => 64, + "SHA1" => SHA1.HashSizeInBytes, + "SHA256" => SHA256.HashSizeInBytes, + "SHA384" => SHA384.HashSizeInBytes, + "SHA512" => SHA512.HashSizeInBytes, _ => throw new NotSupportedException(), }; // Asserts the element's declared saltSize/hashSize/hashAlgorithm agree with the actual salt length and the // hash we're using. These attributes are redundant with the data, so a mismatch signals corruption/misparse. - private static void VerifyDeclaredSizes(XElement el, HashKind hash, int actualSaltLength) + private static void VerifyDeclaredSizes(XElement el, HashAlgorithmName hash, int actualSaltLength) { int saltSize = (int)el.Attribute("saltSize")!; if (saltSize != actualSaltLength) @@ -298,54 +288,31 @@ private static void VerifyDeclaredSizes(XElement el, HashKind hash, int actualSa throw new NotSupportedException("Agile descriptor hashAlgorithm differs between keyData and encryptedKey."); } - private static HashKind ParseHash(string name) => name.Replace("-", "").ToUpperInvariant() switch + // The descriptor names its own hash, and SHA-1 is what Access writes for many files; reading them back means + // honouring it. + private static HashAlgorithmName ParseHash(string name) => name.Replace("-", "").ToUpperInvariant() switch { - "SHA1" => HashKind.Sha1, - "SHA256" => HashKind.Sha256, - "SHA384" => HashKind.Sha384, - "SHA512" => HashKind.Sha512, + "SHA1" => HashAlgorithmName.SHA1, + "SHA256" => HashAlgorithmName.SHA256, + "SHA384" => HashAlgorithmName.SHA384, + "SHA512" => HashAlgorithmName.SHA512, _ => throw new NotSupportedException($"Unsupported Agile hash algorithm '{name}' (SHA1/256/384/512 supported).") }; - private static byte[] Hash(HashKind kind, params byte[][] parts) - { - byte[] all = parts.Length == 1 ? parts[0] : Concat(parts); - // CA5350: the agile descriptor names its own hash, and SHA-1 is what Access writes for many files; - // reading them back means using it. Scoped to the dispatch that honours the descriptor. -#pragma warning disable CA5350 - return kind switch - { - HashKind.Sha1 => SHA1.HashData(all), - HashKind.Sha256 => SHA256.HashData(all), - HashKind.Sha384 => SHA384.HashData(all), - HashKind.Sha512 => SHA512.HashData(all), - _ => throw new NotSupportedException() - }; -#pragma warning restore CA5350 - } - /// H_spin: Hash(salt ‖ UTF16LE(password)), then /// iterations of Hash(LE32(i) ‖ H). Opening a file and creating one both start here. - private static byte[] SpinHash(HashKind hash, byte[] salt, string password, int spinCount) + private static byte[] SpinHash(HashAlgorithmName hash, byte[] salt, string password, int spinCount) { - byte[] hspin = Hash(hash, salt, Encoding.Unicode.GetBytes(password)); - byte[] iter = new byte[4]; + byte[] hspin = CryptographicOperations.HashData(hash, [.. salt, .. Encoding.Unicode.GetBytes(password)]); + byte[] iter = new byte[sizeof(int)]; for (int i = 0; i < spinCount; i++) { BinaryPrimitives.WriteInt32LittleEndian(iter, i); - hspin = Hash(hash, iter, hspin); + hspin = CryptographicOperations.HashData(hash, [.. iter, .. hspin]); } return hspin; } - private static byte[] Concat(params byte[][] parts) - { - var result = new byte[parts.Sum(p => p.Length)]; - int o = 0; - foreach (var p in parts) { p.CopyTo(result, o); o += p.Length; } - return result; - } - /// Truncates a hash to , or pads with 0x36 if it is shorter. private static byte[] Fit(byte[] hash, int length) { @@ -356,13 +323,15 @@ private static byte[] Fit(byte[] hash, int length) return key; } - private static byte[] AesCbcDecrypt(byte[] key, byte[] iv, byte[] data) + /// AES-CBC with no padding — over the key-encryptor fields and every data page; the IV (a salt, or + /// the page's hash) is fitted to the block. + private static byte[] AesCbc(byte[] key, byte[] iv, ReadOnlySpan data, bool encrypt) { using var aes = Aes.Create(); - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.None; aes.Key = key; - aes.IV = iv.Length == 16 ? iv : Fit(iv, 16); - return aes.DecryptCbc(data, aes.IV, PaddingMode.None); + byte[] fitted = iv.Length == SupportedBlockSize ? iv : Fit(iv, SupportedBlockSize); + return encrypt + ? aes.EncryptCbc(data, fitted, PaddingMode.None) + : aes.DecryptCbc(data, fitted, PaddingMode.None); } } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs b/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs index 2166e06f..bac46460 100644 --- a/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs +++ b/src/LibRed/LibRed.Core/Crypto/DatabaseEncryption.cs @@ -1,5 +1,6 @@ using LibRed.Formats; using LibRed.IO; +using LibRed.Pages; using LibRed.Storage; using System.Buffers.Binary; using System.Security.Cryptography; @@ -37,19 +38,6 @@ namespace LibRed.Crypto; /// public static class DatabaseEncryption { - private const int KeyOffset = 0x3E; // 4-byte database (encoding) key, XOR-masked by the page-0 header mask - private const int LengthOffset = 0x299; // 2-byte EncryptionInfo blob length (Access's "is encrypted" signal) - private const int DescriptorOffset = 0x29B; // the EncryptionInfo blob itself - - // The descriptor shares page 0 with the user commit-byte table at 0xE00, which a fresh file seeds with the - // neutral 00 01 pairs — an all-zero table reads to Access as "every user is mid-write", i.e. corrupt. So a - // descriptor may only occupy the zero padding that ends at 0xDFF, not merely fit within the page. - private const int DescriptorPaddingEnd = 0xE00; - - private const int JetPasswordOffset = 0x42; // 40-byte legacy Jet database-password field (header-masked) - private const int JetPasswordSize = 40; // 20 UTF-16LE chars - private const int HeaderDateOffset = 0x72; // 8-byte creation-date OLE double (header-masked) - /// How a page is re-encoded on its way into the replacement file. The page arrives decrypted, /// whatever the database was stored under, so this is the new encoding alone. private delegate void PageTransform(int page, Span bytes); @@ -63,9 +51,9 @@ public static void SetPassword(JetDatabase database, string password, AccessEncr Rewrite(database, (page0, format) => { ValidateScheme(scheme, format); - if (DecodeDatabaseKey(page0) != 0) + if (DatabaseDefinitionPage.ReadDatabaseKey(page0, format) != 0) throw new InvalidOperationException("Database is already encrypted; use ChangePassword."); - return Encrypt(page0, password, scheme); + return Encrypt(page0, password, scheme, format); }); } @@ -74,11 +62,11 @@ public static void SetPassword(JetDatabase database, string password, AccessEncr /// encrypted. public static void RemovePassword(JetDatabase database) { - Rewrite(database, (page0, _) => + Rewrite(database, (page0, format) => { - if (DecodeDatabaseKey(page0) == 0) + if (DatabaseDefinitionPage.ReadDatabaseKey(page0, format) == 0) throw new InvalidOperationException("Database is not encrypted."); - ClearEncryption(page0); + ClearEncryption(page0, format); return null; // the pages arrive decrypted, so writing them through unchanged is the removal }); } @@ -95,9 +83,9 @@ public static void SetPasswordRc4(JetDatabase database, string password, int key { if (!format.IsAccdb) throw new ArgumentException("Office-Standard encryption requires an .accdb (ACE) database.", nameof(database)); - if (DecodeDatabaseKey(page0) != 0) + if (DatabaseDefinitionPage.ReadDatabaseKey(page0, format) != 0) throw new InvalidOperationException("Database is already encrypted; use ChangePassword."); - return EncryptRc4(page0, password, keyBits, hash); + return EncryptRc4(page0, password, keyBits, hash, format); }); } @@ -110,10 +98,10 @@ public static void ChangePassword(JetDatabase database, string newPassword, Acce Rewrite(database, (page0, format) => { ValidateScheme(scheme, format); - if (DecodeDatabaseKey(page0) == 0) + if (DatabaseDefinitionPage.ReadDatabaseKey(page0, format) == 0) throw new InvalidOperationException("Database is not encrypted; use SetPassword."); - ClearEncryption(page0); // the old descriptor may be longer than the new one - return Encrypt(page0, newPassword, scheme); + ClearEncryption(page0, format); // the old descriptor may be longer than the new one + return Encrypt(page0, newPassword, scheme, format); }); } @@ -127,10 +115,10 @@ public static void ChangePasswordRc4(JetDatabase database, string newPassword, i { if (!format.IsAccdb) throw new ArgumentException("Office-Standard encryption requires an .accdb (ACE) database.", nameof(database)); - if (DecodeDatabaseKey(page0) == 0) + if (DatabaseDefinitionPage.ReadDatabaseKey(page0, format) == 0) throw new InvalidOperationException("Database is not encrypted; use SetPasswordRc4."); - ClearEncryption(page0); - return EncryptRc4(page0, newPassword, keyBits, hash); + ClearEncryption(page0, format); + return EncryptRc4(page0, newPassword, keyBits, hash, format); }); } @@ -150,8 +138,10 @@ private static void ValidateRc4Options(int keyBits, StandardHash hash) public static void SetJetPassword(JetDatabase database, string password) { ArgumentException.ThrowIfNullOrEmpty(password); - if (password.Length > JetPasswordSize / 2) - throw new ArgumentException($"A Jet database password is at most {JetPasswordSize / 2} characters.", nameof(password)); + ArgumentNullException.ThrowIfNull(database); + int maxLength = database.Format.PasswordSize / 2; // UTF-16LE + if (password.Length > maxLength) + throw new ArgumentException($"A Jet database password is at most {maxLength} characters.", nameof(password)); RewriteJetPassword(database, password, "The legacy Jet password applies to .mdb, not .accdb — use SetPassword."); @@ -181,7 +171,7 @@ private static void RewriteJetPassword(JetDatabase database, string password, st var page0 = new byte[channel.PageSize]; channel.ReadPage(0, page0); byte[] before = (byte[])page0.Clone(); - WritePasswordField(page0, Encoding.Unicode.GetBytes(password)); + DatabaseDefinitionPage.WritePassword(page0, Encoding.Unicode.GetBytes(password), channel.Format); database.BeginTransaction(); try @@ -195,7 +185,7 @@ private static void RewriteJetPassword(JetDatabase database, string password, st database.Rollback(); throw; } - database.Catalog.Invalidate(); // it holds the SIDs it read from MSysObjects' own row + database.Catalog.Invalidate(); // its catalog tables' rows changed under it } /// @@ -213,7 +203,8 @@ private static void RemaskSids(JetDatabase database, byte[] before, byte[] after if (rows.Count == 0) continue; int longest = rows.Max(r => ((byte[])r.Values[column]!).Length); - byte[] oldStream = SidKeystream.For(before, longest), newStream = SidKeystream.For(after, longest); + byte[] oldStream = SidKeystream.For(before, longest, database.Format); + byte[] newStream = SidKeystream.For(after, longest, database.Format); var changed = new HashSet { column }; foreach ((RowId id, object?[] values) in rows) { @@ -224,27 +215,6 @@ private static void RemaskSids(JetDatabase database, byte[] before, byte[] after } } - // Encodes the header-masked 0x42 field: plaintext = value zero-padded to 40 bytes, XORed with the 4-byte - // little-endian (int)creationDateDouble mask (cycled); the on-disk bytes are that plaintext XORed with the page-0 - // header mask. Reading (jackcess/LibRed) is the exact inverse. An .mdb's value is its Jet password, UTF-16LE; - // an .accdb's is the low byte of its database key, 40 times over (zero when unencrypted). - private static void WritePasswordField(byte[] page0, ReadOnlySpan value) - { - ReadOnlySpan hmask = JetFormatBase.PageZeroHeaderMask; - int start = JetFormatBase.PageZeroHeaderMaskStart; - - Span date = stackalloc byte[8]; - for (int i = 0; i < 8; i++) date[i] = (byte)(page0[HeaderDateOffset + i] ^ hmask[HeaderDateOffset - start + i]); - Span dateMask = stackalloc byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(dateMask, (int)BitConverter.ToDouble(date)); - - Span field = stackalloc byte[JetPasswordSize]; - field.Clear(); - value.CopyTo(field); - for (int i = 0; i < JetPasswordSize; i++) - page0[JetPasswordOffset + i] = (byte)(field[i] ^ dateMask[i % 4] ^ hmask[JetPasswordOffset - start + i]); - } - /// Applies legacy Jet 4 (.mdb) page encoding — the "Encode Database" feature — RC4-encrypting /// every data page with a fresh random database key stored at 0x3E. This is independent of the /// database password (): a file may carry both (encoding scrambles the pages, the @@ -257,11 +227,11 @@ internal static void SetJetEncoding(JetDatabase database, int dbKey) { Rewrite(database, (page0, format) => { - RequireJet4Mdb(page0, format); - if (DecodeDatabaseKey(page0) != 0) + RequireJet4Mdb(format); + if (DatabaseDefinitionPage.ReadDatabaseKey(page0, format) != 0) throw new InvalidOperationException("Database is already encoded."); - WriteDatabaseKey(page0, dbKey); // page 0 (the header) is never encoded + DatabaseDefinitionPage.WriteDatabaseKey(page0, dbKey, format); // page 0 (the header) is never encoded return new JetLegacyEncryption(dbKey).EncryptPage; }); } @@ -272,20 +242,20 @@ public static void RemoveJetEncoding(JetDatabase database) { Rewrite(database, (page0, format) => { - RequireJet4Mdb(page0, format); - if (DecodeDatabaseKey(page0) == 0) + RequireJet4Mdb(format); + if (DatabaseDefinitionPage.ReadDatabaseKey(page0, format) == 0) throw new InvalidOperationException("Database is not encoded."); - WriteDatabaseKey(page0, 0); + DatabaseDefinitionPage.WriteDatabaseKey(page0, 0, format); return null; // the channel decoded them on the way in }); } - private static void RequireJet4Mdb(byte[] page0, JetFormatBase format) + private static void RequireJet4Mdb(JetFormatBase format) { if (format.IsAccdb) throw new ArgumentException("Legacy Jet encoding applies to .mdb, not .accdb.", nameof(format)); - if (page0[0x14] == 0) // version byte: 0 = Jet 3 (2048-byte pages), 1 = Jet 4 + if (format.Version == JetVersion.Version3) throw new NotSupportedException("Jet 3 (Access 97) page encoding is not supported."); } @@ -301,7 +271,7 @@ private static void Rewrite(JetDatabase database, Func DescriptorPaddingEnd) + DatabaseDefinitionPage.WriteDatabaseKey(page0, dbKey, format); + // The descriptor shares page 0 with the user commit-byte table, which a fresh file seeds with the neutral + // 00 01 pairs — an all-zero table reads to Access as "every user is mid-write", i.e. corrupt. So it may + // only occupy the zero padding that ends where the table begins, not merely fit within the page. + int descriptorOffset = format.EncryptionInfoOffset, paddingEnd = format.CommitByteTableOffset; + if (descriptorOffset + descriptor.Length > paddingEnd) throw new NotSupportedException( $"The {descriptor.Length}-byte EncryptionInfo descriptor does not fit page 0's padding " - + $"({DescriptorPaddingEnd - DescriptorOffset} bytes); writing it would overrun the user " - + "commit-byte table at 0xE00, which Access reads as corruption."); - BinaryPrimitives.WriteUInt16LittleEndian(page0.AsSpan(LengthOffset, 2), (ushort)descriptor.Length); - descriptor.CopyTo(page0, DescriptorOffset); + + $"({paddingEnd - descriptorOffset} bytes); writing it would overrun the user " + + $"commit-byte table at 0x{paddingEnd:X3}, which Access reads as corruption."); + BinaryPrimitives.WriteUInt16LittleEndian(page0.AsSpan(format.EncryptionInfoLengthOffset, 2), (ushort)descriptor.Length); + descriptor.CopyTo(page0, descriptorOffset); return codec.EncryptPage; } @@ -426,16 +405,18 @@ private static PageTransform ApplyEncryption(byte[] page0, int dbKey, byte[] des _ => throw new ArgumentOutOfRangeException(nameof(hash)), }; - private static void ClearEncryption(byte[] page0) + private static void ClearEncryption(byte[] page0, JetFormatBase format) { // Clamp to the padding window rather than trusting the stored length. On an .accdb the frame has been // through Agile's or Standard's bound check by now, but a legacy .mdb never uses this frame at all, so - // those two bytes hold whatever they hold, and 2 + blobLen could reach 65537, wiping page-0 structures - // past the padding (the commit-byte table at 0xE00 among them) or throwing outright. - int blobLen = BinaryPrimitives.ReadUInt16LittleEndian(page0.AsSpan(LengthOffset, 2)); - int clearLength = Math.Min(2 + blobLen, DescriptorPaddingEnd - LengthOffset); - Array.Clear(page0, LengthOffset, clearLength); // the length signal + the descriptor - WriteDatabaseKey(page0, 0); // decodes back to 0 = unencrypted + // those two bytes hold whatever they hold, and the frame they declare could reach 65537 bytes, wiping + // page-0 structures past the padding (the commit-byte table among them) or throwing outright. + int lengthOffset = format.EncryptionInfoLengthOffset; + int blobLen = DatabaseDefinitionPage.ReadEncryptionInfoLength(page0, format); + int clearLength = Math.Min(format.EncryptionInfoOffset - lengthOffset + blobLen, + format.CommitByteTableOffset - lengthOffset); + Array.Clear(page0, lengthOffset, clearLength); // the length signal + the descriptor + DatabaseDefinitionPage.WriteDatabaseKey(page0, 0, format); // decodes back to 0 = unencrypted } private static void ValidateScheme(AccessEncryption scheme, JetFormatBase format) @@ -467,24 +448,4 @@ private static void ValidateScheme(AccessEncryption scheme, JetFormatBase format throw new ArgumentOutOfRangeException(nameof(scheme)); } } - - private static int DecodeDatabaseKey(byte[] page0) - { - ReadOnlySpan mask = JetFormatBase.PageZeroHeaderMask; - int start = JetFormatBase.PageZeroHeaderMaskStart; - Span key = stackalloc byte[4]; - for (int i = 0; i < 4; i++) key[i] = (byte)(page0[KeyOffset + i] ^ mask[KeyOffset - start + i]); - return BinaryPrimitives.ReadInt32LittleEndian(key); - } - - private static void WriteDatabaseKey(byte[] page0, int dbKey) - { - ReadOnlySpan mask = JetFormatBase.PageZeroHeaderMask; - int start = JetFormatBase.PageZeroHeaderMaskStart; - Span k = stackalloc byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(k, dbKey); - for (int i = 0; i < 4; i++) page0[KeyOffset + i] = (byte)(k[i] ^ mask[KeyOffset - start + i]); - } - - private static byte[] RandomBytes(int n) { byte[] b = new byte[n]; RandomNumberGenerator.Fill(b); return b; } -} +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Crypto/JetLegacyEncryption.cs b/src/LibRed/LibRed.Core/Crypto/JetLegacyEncryption.cs index f2d0e748..2c29ca29 100644 --- a/src/LibRed/LibRed.Core/Crypto/JetLegacyEncryption.cs +++ b/src/LibRed/LibRed.Core/Crypto/JetLegacyEncryption.cs @@ -12,9 +12,9 @@ namespace LibRed.Crypto; /// /// /// Verified against a real System.mdw (databaseKey 0xABBB315C): with this key every page decrypts -/// to a valid page type (page 1 → 0x0101 data, page 2/3 → 0x0102 TDEF, index pages → 0x0104), +/// to a valid page type (the global map holder → 0x0101 data, the catalog TDEFs → 0x0102, index pages → 0x0104), /// and the XOR (not ADD) page-number mixing is the one that yields valid types on pages where the two differ. -/// Same per-page key derivation as (LE32(pageNumber) XOR encodingKey), just +/// Same per-page key derivation as (LE32(pageNumber XOR databaseKey)), just /// feeding RC4 directly rather than an AES IV. /// public sealed class JetLegacyEncryption : IPageCodec diff --git a/src/LibRed/LibRed.Core/Crypto/OfficeStandardEncryption.cs b/src/LibRed/LibRed.Core/Crypto/OfficeStandardEncryption.cs index 825fd366..acacd108 100644 --- a/src/LibRed/LibRed.Core/Crypto/OfficeStandardEncryption.cs +++ b/src/LibRed/LibRed.Core/Crypto/OfficeStandardEncryption.cs @@ -1,3 +1,5 @@ +using LibRed.Formats; +using LibRed.Pages; using System.Buffers.Binary; using System.Security.Cryptography; using System.Text; @@ -19,7 +21,7 @@ namespace LibRed.Crypto; /// AES "non-standard" variant; 50000 for ECMA-standard AES); H = SHA1(iterHash ‖ block); then AES uses the /// 0x36/0x5C expansion key = (SHA1(0x36pad⊕H) ‖ SHA1(0x5Cpad⊕H))[0..keyLen] while RC4 uses /// key = H[0..keyLen] (a 40-bit RC4 key is then zero-padded to 16 bytes). -/// verifier block = LE32(0); per-page block = LE32(pageNumber) XOR encodingKey (the 4-byte +/// verifier block = LE32(0); per-page block = pageNumber XOR databaseKey (the 4-byte /// database key at page-0 0x3E). /// cipher: RC4 (stream, re-keyed per page; the verifier + verifier-hash decrypt as one continuous stream) /// or AES-ECB. @@ -27,7 +29,67 @@ namespace LibRed.Crypto; /// public sealed class OfficeStandardEncryption : IPageCodec { - private const uint AlgIdRc4 = 0x6801; + + internal const int MajorVersionOffset = 0; + internal const int MinorVersionOffset = 2; + internal const int FlagsOffset = 4; + + /// Binary form: the EncryptionHeader's size, and then the header itself. + internal const int HeaderSizeOffset = 8; + internal const int HeaderOffset = 12; + + /// The minor version of every binary (Standard/CryptoAPI) EncryptionInfo; the major is 2, 3 or 4. + internal const ushort StandardMinorVersion = 2; + internal const ushort StandardMinMajorVersion = 2; + internal const ushort StandardMaxMajorVersion = 4; + + /// The major version Access writes on a binary EncryptionInfo it creates (verified against real files). + internal const ushort StandardCreatedMajorVersion = 4; + + /// The largest EncryptionHeader a binary EncryptionInfo is taken to have when one is searched for. + internal const int MaxHeaderSize = 512; + + // --- EncryptionHeader (MS-OFFCRYPTO §2.3.2), from its own start --- + + internal const int HeaderFlagsOffset = 0; + internal const int HeaderSizeExtraOffset = 4; + internal const int HeaderAlgIdOffset = 8; + internal const int HeaderAlgIdHashOffset = 12; + internal const int HeaderKeySizeOffset = 16; + internal const int HeaderProviderTypeOffset = 20; + + /// The fixed fields — Flags, SizeExtra, AlgID, AlgIDHash, KeySize, ProviderType and two reserved words — + /// ahead of the null-terminated UTF-16LE CSP name. + internal const int HeaderFixedSize = 32; + + // --- EncryptionVerifier (MS-OFFCRYPTO §2.3.3), from its own start --- + + internal const int VerifierSaltSizeOffset = 0; + internal const int VerifierSaltOffset = 4; + + /// The salt size every Standard verifier carries. + internal const int VerifierSaltSize = 16; + + /// The encrypted verifier that follows the salt; the verifier-hash size and the encrypted hash come next. + internal const int VerifierSize = 16; + + // --- Field values --- + + /// The cipher AlgIDs (CALG_*): RC4, and AES with a 128-, 192- or 256-bit key. + internal const uint AlgIdRc4 = 0x6801; + internal const uint AlgIdAes128 = 0x660E; + internal const uint AlgIdAes192 = 0x660F; + internal const uint AlgIdAes256 = 0x6610; + + /// The CALG_* class of every block cipher (0x66xx) and stream cipher (0x68xx), in the AlgID's second byte. + internal const uint AlgIdClassMask = 0xFF00; + internal const uint AlgIdBlockCipherClass = 0x6600; + internal const uint AlgIdStreamCipherClass = 0x6800; + + /// The ProviderType of the Base and the Enhanced RSA/AES cryptographic providers. + internal const uint ProviderTypeBase = 0x01; + internal const uint ProviderTypeEnhancedAes = 0x18; + private const int VerifierBlock = 0; private readonly bool _rc4; @@ -36,60 +98,41 @@ public sealed class OfficeStandardEncryption : IPageCodec private readonly int _iterations; private readonly int _truncateLen; // logical key length in bytes derived from the hash (5 for RC4-40, 32 for AES-256) private readonly int _finalLen; // actual cipher key length (RC4 <128-bit keys are zero-padded to 16 bytes) - private readonly byte[] _encodingKey; // 4-byte database key (page-0 0x3E) + private readonly int _databaseKey; // page-0 0x3E private readonly bool _aesExpand; // AES: apply the CryptDeriveKey 0x36/0x5C expansion vs. use the truncated hash - private OfficeStandardEncryption(bool rc4, HashAlgorithmName hashName, byte[] baseHash, int iterations, int truncateLen, int finalLen, bool aesExpand, byte[] encodingKey) + private OfficeStandardEncryption(bool rc4, HashAlgorithmName hashName, byte[] baseHash, int iterations, int truncateLen, int finalLen, bool aesExpand, int databaseKey) { - _rc4 = rc4; _hashName = hashName; _baseHash = baseHash; _iterations = iterations; _truncateLen = truncateLen; _finalLen = finalLen; _aesExpand = aesExpand; _encodingKey = encodingKey; + _rc4 = rc4; _hashName = hashName; _baseHash = baseHash; _iterations = iterations; _truncateLen = truncateLen; _finalLen = finalLen; _aesExpand = aesExpand; _databaseKey = databaseKey; } - // AlgIDHash → (.NET hash algorithm, unencrypted-hash length in bytes). Access/the CryptoAPI let the encrypting - // tool pick the hashing algorithm independently of the cipher; MD2/MD4 (0x8001/0x8002) have no managed - // implementation and are not produced by Access, so they surface as "unsupported" rather than a crash. - private static (HashAlgorithmName Name, int Len)? MapHash(uint algIdHash) => algIdHash switch - { - 0x8003 => (HashAlgorithmName.MD5, 16), - 0x8004 => (HashAlgorithmName.SHA1, 20), - 0x800c => (HashAlgorithmName.SHA256, 32), - 0x800d => (HashAlgorithmName.SHA384, 48), - 0x800e => (HashAlgorithmName.SHA512, 64), - _ => null, - }; - - // Reverse of MapHash: the CALG_* AlgIDHash to write into a descriptor for a given .NET hash. - private static uint HashAlgId(HashAlgorithmName name) => name.Name switch - { - "MD5" => 0x8003, - "SHA1" => 0x8004, - "SHA256" => 0x800c, - "SHA384" => 0x800d, - "SHA512" => 0x800e, - _ => throw new NotSupportedException($"Unsupported Office-Standard hash {name}."), - }; - - // CA5350/CA5351 (weak hashing): MD5 and SHA-1 are not a choice here. The Office "Standard" encryption - // descriptor NAMES the algorithm, and a file Access wrote with one cannot be opened with anything else, - // so refusing them would mean refusing the documents. Scoped to the dispatch that honours the descriptor. -#pragma warning disable CA5350, CA5351 - private static byte[] Hash(HashAlgorithmName name, byte[] data) => name.Name switch - { - "MD5" => MD5.HashData(data), - "SHA1" => SHA1.HashData(data), - "SHA256" => SHA256.HashData(data), - "SHA384" => SHA384.HashData(data), - "SHA512" => SHA512.HashData(data), - _ => throw new NotSupportedException($"Unsupported hash algorithm {name}."), - }; -#pragma warning restore CA5350, CA5351 + // AlgIDHash ↔ (.NET hash algorithm, unencrypted-hash length in bytes), read by MapHash and written by HashAlgId. + // Access/the CryptoAPI let the encrypting tool pick the hashing algorithm independently of the cipher; MD2/MD4 + // (0x8001/0x8002) have no managed implementation and are not produced by Access, so they surface as + // "unsupported" rather than a crash. + private static readonly (uint AlgIdHash, HashAlgorithmName Name, int Len)[] Hashes = + [ + (0x8003, HashAlgorithmName.MD5, 16), + (0x8004, HashAlgorithmName.SHA1, 20), + (0x800c, HashAlgorithmName.SHA256, 32), + (0x800d, HashAlgorithmName.SHA384, 48), + (0x800e, HashAlgorithmName.SHA512, 64), + ]; + + private static (HashAlgorithmName Name, int Len)? MapHash(uint algIdHash) => + Array.Find(Hashes, h => h.AlgIdHash == algIdHash) is { Name.Name: not null } h ? (h.Name, h.Len) : null; + + // The CALG_* AlgIDHash to write into a descriptor for a given .NET hash. + private static uint HashAlgId(HashAlgorithmName name) => + Array.Find(Hashes, h => h.Name == name) is { Name.Name: not null } h + ? h.AlgIdHash + : throw new NotSupportedException($"Unsupported Office-Standard hash {name}."); // The CryptoAPI CryptDeriveKey 0x36/0x5C expansion uses a fixed 64-byte pad buffer for every hash algorithm // (it is not HMAC, so SHA-384/512 do NOT switch to their 128-byte block size). Verified: AES-256 + SHA-512. private const int DeriveKeyPadSize = 64; - /// Builds a codec for an .accdb that uses a binary (non-Agile) EncryptionInfo descriptor, or - /// null if the file is not encrypted / carries no such descriptor. Throws if a password is required/incorrect. /// Asserts that bytes are readable at within /// the descriptor, so a length taken from the file cannot index past it. private static void Require(ReadOnlySpan descriptor, int offset, int count, string field) @@ -99,23 +142,26 @@ private static void Require(ReadOnlySpan descriptor, int offset, int count $"Office-Standard {field} needs {count} bytes at offset {offset}, past the {descriptor.Length}-byte descriptor."); } - public static OfficeStandardEncryption? TryCreate(ReadOnlySpan page0, int databaseKey, string? password) + /// Builds a codec for an .accdb that uses a binary (non-Agile) EncryptionInfo descriptor, or + /// null if the file is not encrypted / carries no such descriptor. Throws if a password is required/incorrect. + public static OfficeStandardEncryption? TryCreate(ReadOnlySpan page0, int databaseKey, string? password, + JetFormatBase format) { + ArgumentNullException.ThrowIfNull(format); if (databaseKey == 0) return null; - const int descriptorOffset = 0x29B; + int descriptorOffset = format.EncryptionInfoOffset; 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)); + int descriptorLength = DatabaseDefinitionPage.ReadEncryptionInfoLength(page0, format); 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) + if (LocateBinaryEncryptionInfo(page0) is not var (ei, headerSize, algId)) return null; if (password is null) throw new InvalidOperationException("This database is password-encrypted; a password is required to open it."); @@ -125,18 +171,16 @@ private static void Require(ReadOnlySpan descriptor, int offset, int count // frame with a large headerSize walked `v` past the end — throwing ArgumentOutOfRangeException out of // PageChannel.Open, where the contract for a damaged file is InvalidDataException. The Agile sibling // bounds every field it reads; this is that, for the fields this descriptor has. - int headerSize = BinaryPrimitives.ReadInt32LittleEndian(page0.Slice(ei + 8, 4)); - int h = ei + 12; - Require(page0, h, 20, "EncryptionHeader"); - uint algId = BinaryPrimitives.ReadUInt32LittleEndian(page0.Slice(h + 8, 4)); - uint algIdHash = BinaryPrimitives.ReadUInt32LittleEndian(page0.Slice(h + 12, 4)); - int keyBits = BinaryPrimitives.ReadInt32LittleEndian(page0.Slice(h + 16, 4)); + int h = ei + HeaderOffset; + Require(page0, h, HeaderKeySizeOffset + sizeof(int), "EncryptionHeader"); + uint algIdHash = BinaryPrimitives.ReadUInt32LittleEndian(page0.Slice(h + HeaderAlgIdHashOffset, sizeof(uint))); + int keyBits = BinaryPrimitives.ReadInt32LittleEndian(page0.Slice(h + HeaderKeySizeOffset, sizeof(int))); bool rc4 = algId == AlgIdRc4; // We've committed to a binary EncryptionInfo descriptor (dbKey != 0 + located header). Anything we can't // honour is a genuinely unsupported/invalid file (e.g. Access rejects AES/3DES combos with certain hashes), // so throw a clear error rather than falling through to read ciphertext as plaintext. - if (!rc4 && algId is not (0x660E or 0x660F or 0x6610)) + if (!rc4 && algId is not (AlgIdAes128 or AlgIdAes192 or AlgIdAes256)) throw new NotSupportedException($"Unsupported Office-Standard cipher AlgID 0x{algId:X4}."); if (MapHash(algIdHash) is not var (hashName, hashLength)) throw new NotSupportedException($"Unsupported Office-Standard hash AlgID 0x{algIdHash:X4}."); @@ -149,23 +193,29 @@ private static void Require(ReadOnlySpan descriptor, int offset, int count if (headerSize < 0) throw new InvalidDataException($"Office-Standard EncryptionHeader declares a negative size ({headerSize})."); int v = h + headerSize; // EncryptionVerifier - Require(page0, v, 4, "EncryptionVerifier"); - int saltSize = BinaryPrimitives.ReadInt32LittleEndian(page0.Slice(v, 4)); - if (saltSize != 16) - throw new NotSupportedException($"Unsupported Office-Standard salt size {saltSize}; expected 16 bytes."); - Require(page0, v + 4, saltSize + 16 + 4, "EncryptionVerifier salt and verifier"); - byte[] salt = page0.Slice(v + 4, saltSize).ToArray(); - byte[] encVerifier = page0.Slice(v + 4 + saltSize, 16).ToArray(); - int verifierHashSize = BinaryPrimitives.ReadInt32LittleEndian(page0.Slice(v + 4 + saltSize + 16, 4)); + Require(page0, v, VerifierSaltOffset, "EncryptionVerifier"); + int saltSize = BinaryPrimitives.ReadInt32LittleEndian(page0.Slice(v + VerifierSaltSizeOffset, sizeof(int))); + if (saltSize != VerifierSaltSize) + throw new NotSupportedException( + $"Unsupported Office-Standard salt size {saltSize}; expected {VerifierSaltSize} bytes."); + int verifier = v + VerifierSaltOffset + saltSize; + int hashSizeField = verifier + VerifierSize; + int encHash = hashSizeField + sizeof(int); + Require(page0, v + VerifierSaltOffset, encHash - (v + VerifierSaltOffset), + "EncryptionVerifier salt and verifier"); + byte[] salt = page0.Slice(v + VerifierSaltOffset, saltSize).ToArray(); + byte[] encVerifier = page0.Slice(verifier, VerifierSize).ToArray(); + int verifierHashSize = BinaryPrimitives.ReadInt32LittleEndian(page0.Slice(hashSizeField, sizeof(int))); if (verifierHashSize != hashLength) throw new NotSupportedException( $"Office-Standard verifier hash size {verifierHashSize} does not match {hashName.Name} ({hashLength} bytes)."); int encHashLen = rc4 ? verifierHashSize : (verifierHashSize + 15) / 16 * 16; // RC4: raw hash; AES: padded to block - Require(page0, v + 4 + saltSize + 16 + 4, encHashLen, "EncryptionVerifier hash"); - byte[] encVerifierHash = page0.Slice(v + 4 + saltSize + 16 + 4, encHashLen).ToArray(); + Require(page0, encHash, encHashLen, "EncryptionVerifier hash"); + byte[] encVerifierHash = page0.Slice(encHash, encHashLen).ToArray(); - byte[] baseHash = Hash(hashName, Concat(salt, Encoding.Unicode.GetBytes(password))); - byte[] encodingKey = BitConverter.GetBytes(databaseKey); + // MD5 and SHA-1 are not a choice here: the descriptor NAMES the algorithm, and a file Access wrote with one + // cannot be opened with anything else. + byte[] baseHash = CryptographicOperations.HashData(hashName, [.. salt, .. Encoding.Unicode.GetBytes(password)]); // Try each plausible (key length, RC4 pad, iteration count) combination and keep whichever authenticates — // KeySize == 0 means "the algorithm default" (which the descriptor doesn't spell out), RC4 keys shorter than @@ -175,7 +225,7 @@ private static void Require(ReadOnlySpan descriptor, int offset, int count foreach (int iterations in rc4 ? [0] : new[] { 0, 50000 }) foreach (bool aesExpand in rc4 ? [false] : new[] { false, true }) { - var codec = new OfficeStandardEncryption(rc4, hashName, baseHash, iterations, truncateLen, finalLen, aesExpand, encodingKey); + var codec = new OfficeStandardEncryption(rc4, hashName, baseHash, iterations, truncateLen, finalLen, aesExpand, databaseKey); if (codec.VerifyPassword(encVerifier, encVerifierHash, verifierHashSize)) return codec; } @@ -196,44 +246,32 @@ private static void Require(ReadOnlySpan descriptor, int offset, int count } } - /// Generates a fresh Office-Standard EncryptionInfo for a new password: returns the - /// descriptor blob (to place at page-0 0x29B, with its 2-byte length at 0x299) and a codec that - /// encrypts the data pages. selects AES-256 (the 0-iteration variant Access accepts on - /// a created file) vs RC4-40; is a fresh random 0x3E key. - internal static (byte[] Descriptor, OfficeStandardEncryption Codec) Create(string password, bool aes, int databaseKey) + /// Generates a fresh Office-Standard AES-256 EncryptionInfo for a new password — the 0-iteration + /// variant Access accepts on a created file: returns the descriptor blob (to place at page-0 0x29B, with + /// its 2-byte length at 0x299) and a codec that encrypts the data pages. is + /// a fresh random 0x3E key. RC4 is . + internal static (byte[] Descriptor, OfficeStandardEncryption Codec) Create(string password, int databaseKey) { - uint algId = aes ? 0x6610u : AlgIdRc4; - int keyBits = aes ? 256 : 40; - byte[] salt = RandomBytes(16); - // CA5350: the Office-Standard scheme hashes the password with SHA-1 by definition — a file created - // with anything else is not this scheme, and Access would not open it. -#pragma warning disable CA5350 - byte[] baseHash = SHA1.HashData(Concat(salt, Encoding.Unicode.GetBytes(password))); + const int keyBits = 256; + byte[] salt = RandomNumberGenerator.GetBytes(VerifierSaltSize); + // The Office-Standard scheme hashes the password with SHA-1 by definition — a file created with anything + // else is not this scheme, and Access would not open it. + HashAlgorithmName hash = HashAlgorithmName.SHA1; + byte[] baseHash = CryptographicOperations.HashData(hash, [.. salt, .. Encoding.Unicode.GetBytes(password)]); int keyLen = keyBits / 8; - int finalLen = !aes && keyBits == 40 ? 16 : keyLen; - // AES-256 (32 bytes) from SHA-1 (20 bytes) needs the 0x36/0x5C expansion; RC4 never expands. - var codec = new OfficeStandardEncryption(!aes, HashAlgorithmName.SHA1, baseHash, 0, keyLen, finalLen, aesExpand: aes, BitConverter.GetBytes(databaseKey)); + // AES-256 (32 bytes) from SHA-1 (20 bytes) needs the 0x36/0x5C expansion. + var codec = new OfficeStandardEncryption(rc4: false, hash, baseHash, 0, keyLen, keyLen, aesExpand: true, databaseKey); byte[] verKey = codec.ComputeKey(VerifierBlock); - byte[] verifier = RandomBytes(16); - byte[] verifierHash = SHA1.HashData(verifier); -#pragma warning restore CA5350 - byte[] encVerifier, encVerifierHash; - if (aes) - { - encVerifier = AesEcbEncrypt(verKey, verifier); - encVerifierHash = AesEcbEncrypt(verKey, Fix(verifierHash, 32)); // pad the 20-byte hash to a cipher block - } - else - { - byte[] stream = Concat(verifier, verifierHash); // one continuous RC4 stream - Rc4(verKey, stream); - encVerifier = stream[..16]; - encVerifierHash = stream[16..]; - } - - var (provType, csp) = Provider(aes, keyBits, HashAlgorithmName.SHA1); - return (BuildDescriptor(algId, 0x8004, aes ? 0x0Cu : 0x04u, keyBits, provType, csp, salt, encVerifier, encVerifierHash, 20), codec); + byte[] verifier = RandomNumberGenerator.GetBytes(VerifierSize); + byte[] verifierHash = CryptographicOperations.HashData(hash, verifier); + byte[] encVerifier = AesEcb(verKey, verifier, decrypt: false); + byte[] encVerifierHash = AesEcb(verKey, Fix(verifierHash, 32), decrypt: false); // pad the 20-byte hash to a cipher block + + var (provType, csp) = Provider(aes: true, keyBits, hash); + return (BuildDescriptor(AlgIdAes256, HashAlgId(hash), + EncryptionInfoFlags.CryptoApi | EncryptionInfoFlags.DocProps, keyBits, provType, csp, salt, encVerifier, + encVerifierHash, verifierHash.Length), codec); } /// Generates a fresh Office-Standard RC4 EncryptionInfo with a caller-chosen key length @@ -241,20 +279,22 @@ internal static (byte[] Descriptor, OfficeStandardEncryption Codec) Create(strin /// cipher a stock/add-in Access reads back; AES-Standard is exposed only via the AES path. internal static (byte[] Descriptor, OfficeStandardEncryption Codec) CreateRc4(string password, int keyBits, HashAlgorithmName hash, int databaseKey) { - byte[] salt = RandomBytes(16); - byte[] baseHash = Hash(hash, Concat(salt, Encoding.Unicode.GetBytes(password))); + byte[] salt = RandomNumberGenerator.GetBytes(VerifierSaltSize); + byte[] baseHash = CryptographicOperations.HashData(hash, [.. salt, .. Encoding.Unicode.GetBytes(password)]); int keyLen = keyBits / 8; int finalLen = keyBits == 40 ? 16 : keyLen; // 40-bit is the one length the base provider zero-pads to 128 - var codec = new OfficeStandardEncryption(rc4: true, hash, baseHash, 0, keyLen, finalLen, aesExpand: false, BitConverter.GetBytes(databaseKey)); + var codec = new OfficeStandardEncryption(rc4: true, hash, baseHash, 0, keyLen, finalLen, aesExpand: false, databaseKey); byte[] verKey = codec.ComputeKey(VerifierBlock); - byte[] verifier = RandomBytes(16); - byte[] verifierHash = Hash(hash, verifier); - byte[] stream = Concat(verifier, verifierHash); // one continuous RC4 stream - Rc4(verKey, stream); + byte[] verifier = RandomNumberGenerator.GetBytes(VerifierSize); + byte[] verifierHash = CryptographicOperations.HashData(hash, verifier); + byte[] stream = [.. verifier, .. verifierHash]; // one continuous RC4 stream + Rc4Cipher.Apply(verKey, stream); var (provType, csp) = Provider(aes: false, keyBits, hash); - return (BuildDescriptor(AlgIdRc4, HashAlgId(hash), 0x04u, keyBits, provType, csp, salt, stream[..16], stream[16..], verifierHash.Length), codec); + return (BuildDescriptor(AlgIdRc4, HashAlgId(hash), EncryptionInfoFlags.CryptoApi, keyBits, + provType, csp, salt, stream[..VerifierSize], stream[VerifierSize..], + verifierHash.Length), codec); } // Picks the (ProviderType, CSP name) pair the way the CryptoAPI does: the Base provider handles RC4 ≤56-bit with @@ -264,26 +304,27 @@ private static (uint ProviderType, string Csp) Provider(bool aes, int keyBits, H bool sha2 = hash.Name is "SHA256" or "SHA384" or "SHA512"; bool enhanced = aes || keyBits > 56 || sha2; return enhanced - ? (0x18u, "Microsoft Enhanced RSA and AES Cryptographic Provider") - : (0x01u, "Microsoft Base Cryptographic Provider v1.0"); + ? (ProviderTypeEnhancedAes, "Microsoft Enhanced RSA and AES Cryptographic Provider") + : (ProviderTypeBase, "Microsoft Base Cryptographic Provider v1.0"); } // Builds the binary EncryptionInfo (version 4.2 + EncryptionHeader incl. ProviderType + CSP name + // EncryptionVerifier) byte-for-byte as Access writes it (verified against real files). - private static byte[] BuildDescriptor(uint algId, uint algIdHash, uint flags, int keyBits, uint providerType, string csp, - byte[] salt, byte[] encVerifier, byte[] encVerifierHash, int verifierHashSize) + private static byte[] BuildDescriptor(uint algId, uint algIdHash, EncryptionInfoFlags flags, int keyBits, uint providerType, + string csp, byte[] salt, byte[] encVerifier, byte[] encVerifierHash, int verifierHashSize) { - byte[] cspBytes = Concat(Encoding.Unicode.GetBytes(csp), new byte[2]); // null-terminated UTF-16LE + byte[] cspBytes = [.. Encoding.Unicode.GetBytes(csp), 0, 0]; // null-terminated UTF-16LE var b = new List(); void U16(ushort x) => b.AddRange(BitConverter.GetBytes(x)); void U32(uint x) => b.AddRange(BitConverter.GetBytes(x)); - int headerSize = 8 * 4 + cspBytes.Length; // Flags,SizeExtra,AlgID,AlgIDHash,KeySize,ProviderType,Reserved1,Reserved2 + CSP - U16(4); U16(2); // EncryptionVersionInfo 4.2 - U32(flags); // EncryptionInfo flags + // Written in field order, using the same layout as the reader. + int headerSize = HeaderFixedSize + cspBytes.Length; + U16(StandardCreatedMajorVersion); U16(StandardMinorVersion); + U32((uint)flags); U32((uint)headerSize); - U32(flags); U32(0); U32(algId); U32(algIdHash); U32((uint)keyBits); U32(providerType); U32(0); U32(0); + U32((uint)flags); U32(0); U32(algId); U32(algIdHash); U32((uint)keyBits); U32(providerType); U32(0); U32(0); b.AddRange(cspBytes); // EncryptionVerifier U32((uint)salt.Length); b.AddRange(salt); @@ -292,17 +333,6 @@ private static byte[] BuildDescriptor(uint algId, uint algIdHash, uint flags, in return b.ToArray(); } - private static byte[] AesEcbEncrypt(byte[] key, byte[] data) - { - using var aes = Aes.Create(); - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; - aes.Key = key; - return aes.EncryptEcb(data, PaddingMode.None); - } - - private static byte[] RandomBytes(int n) { byte[] b = new byte[n]; RandomNumberGenerator.Fill(b); return b; } - private bool VerifyPassword(byte[] encVerifier, byte[] encVerifierHash, int hashSize) { byte[] key = ComputeKey(VerifierBlock); @@ -310,17 +340,17 @@ private bool VerifyPassword(byte[] encVerifier, byte[] encVerifierHash, int hash if (_rc4) { // one continuous RC4 stream over verifier(16) ‖ verifierHash - byte[] stream = Concat(encVerifier, encVerifierHash); - Rc4(key, stream); - verifier = stream[..16]; - storedHash = stream[16..]; + byte[] stream = [.. encVerifier, .. encVerifierHash]; + Rc4Cipher.Apply(key, stream); + verifier = stream[..VerifierSize]; + storedHash = stream[VerifierSize..]; } else { - verifier = AesEcb(key, encVerifier); - storedHash = AesEcb(key, encVerifierHash); + verifier = AesEcb(key, encVerifier, decrypt: true); + storedHash = AesEcb(key, encVerifierHash, decrypt: true); } - byte[] computed = Hash(_hashName, verifier); + byte[] computed = CryptographicOperations.HashData(_hashName, verifier); return computed.Length == hashSize && storedHash.Length >= hashSize && CryptographicOperations.FixedTimeEquals(computed, storedHash.AsSpan(0, hashSize)); } @@ -335,16 +365,16 @@ private void Transform(int pageNumber, Span page, bool decrypt) if (pageNumber == 0) return; - // block = LE32(pageNumber) XOR encodingKey - Span block = stackalloc byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(block, pageNumber); - for (int i = 0; i < 4; i++) block[i] ^= _encodingKey[i]; - - byte[] key = ComputeKey(BinaryPrimitives.ReadInt32LittleEndian(block)); + byte[] key = ComputeKey(pageNumber ^ _databaseKey); if (_rc4) - Rc4(key, page); // symmetric + { + Rc4Cipher.Apply(key, page); // symmetric + } else - AesEcbInPlace(key, page, decrypt); + { + int len = page.Length - page.Length % 16; + AesEcb(key, page[..len], decrypt).CopyTo(page); + } } private byte[] ComputeKey(int block) @@ -356,17 +386,17 @@ private byte[] ComputeKey(int block) for (int i = 0; i < _iterations; i++) { BinaryPrimitives.WriteInt32LittleEndian(it, i); - iterHash = Hash(_hashName, Concat(it.ToArray(), iterHash)); + iterHash = CryptographicOperations.HashData(_hashName, [.. it, .. iterHash]); } } - byte[] blk = new byte[4]; + byte[] blk = new byte[sizeof(int)]; BinaryPrimitives.WriteInt32LittleEndian(blk, block); - byte[] hf = Hash(_hashName, Concat(iterHash, blk)); + byte[] hf = CryptographicOperations.HashData(_hashName, [.. iterHash, .. blk]); // RC4 keys are always the truncated hash. AES either uses the truncated hash or the CryptDeriveKey // 0x36/0x5C expansion — the choice does not follow a clean keyLen-vs-hashLen rule (AES-128 from MD5 (16==16) // expands, yet AES-256 from SHA-256 (32==32) truncates), so both are tried and the verifier selects. - byte[] derived = !_rc4 && _aesExpand ? Concat(GenX(hf, 0x36), GenX(hf, 0x5C)) : hf; + byte[] derived = !_rc4 && _aesExpand ? [.. GenX(hf, 0x36), .. GenX(hf, 0x5C)] : hf; byte[] key = Fix(derived, _truncateLen); return _finalLen != _truncateLen ? Fix(key, _finalLen) : key; } @@ -378,53 +408,46 @@ private byte[] GenX(byte[] hf, byte pad) byte[] buf = new byte[DeriveKeyPadSize]; Array.Fill(buf, pad); for (int i = 0; i < hf.Length; i++) buf[i] ^= hf[i]; - return Hash(_hashName, buf); + return CryptographicOperations.HashData(_hashName, buf); } - private static int LocateBinaryEncryptionInfo(ReadOnlySpan page0) + /// Finds the binary EncryptionInfo in , returning where it starts and the two + /// fields it was recognised by — its header's size and cipher AlgID — or null when there is none. + private static (int Offset, int HeaderSize, uint AlgId)? LocateBinaryEncryptionInfo(ReadOnlySpan descriptor) { // 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 = 0; i + 32 < page0.Length; i++) + // (fCryptoAPI set), uint32 headerSize. Validate against a known cipher AlgID to avoid false hits. + const int algIdAt = HeaderOffset + HeaderAlgIdOffset; + for (int i = 0; i + algIdAt + sizeof(uint) <= descriptor.Length; i++) { - ushort major = BinaryPrimitives.ReadUInt16LittleEndian(page0.Slice(i, 2)); - ushort minor = BinaryPrimitives.ReadUInt16LittleEndian(page0.Slice(i + 2, 2)); - uint flags = BinaryPrimitives.ReadUInt32LittleEndian(page0.Slice(i + 4, 2 + 2)); - int headerSize = BinaryPrimitives.ReadInt32LittleEndian(page0.Slice(i + 8, 4)); - if (major is < 2 or > 4 || minor != 2 || (flags & 0x04) == 0 || headerSize is <= 0 or > 512) + ReadOnlySpan info = descriptor[i..]; + ushort major = BinaryPrimitives.ReadUInt16LittleEndian(info[MajorVersionOffset..]); + ushort minor = BinaryPrimitives.ReadUInt16LittleEndian(info[MinorVersionOffset..]); + var flags = (EncryptionInfoFlags)BinaryPrimitives.ReadUInt32LittleEndian(info[FlagsOffset..]); + int headerSize = BinaryPrimitives.ReadInt32LittleEndian(info[HeaderSizeOffset..]); + if (major is < StandardMinMajorVersion or > StandardMaxMajorVersion + || minor != StandardMinorVersion || !flags.HasFlag(EncryptionInfoFlags.CryptoApi) + || headerSize is <= 0 or > MaxHeaderSize) continue; // Recognise the descriptor by a CALG_* cipher id (0x66xx block ciphers, 0x68xx stream). This covers the // ciphers we support (RC4, AES-128/192/256) *and* ones we don't (e.g. 3DES 0x6603) so those surface as a // clean "unsupported" error in TryCreate instead of being mistaken for an unencrypted file. - uint algId = BinaryPrimitives.ReadUInt32LittleEndian(page0.Slice(i + 12 + 8, 4)); - if ((algId & 0xFF00) is 0x6600 or 0x6800) - return i; + uint algId = BinaryPrimitives.ReadUInt32LittleEndian(info[algIdAt..]); + if ((algId & AlgIdClassMask) + is AlgIdBlockCipherClass or AlgIdStreamCipherClass) + return (i, headerSize, algId); } - return -1; + return null; } - private static byte[] AesEcb(byte[] key, byte[] data) + /// AES-ECB with no padding — the verifier fields and every data page. + private static byte[] AesEcb(byte[] key, ReadOnlySpan data, bool decrypt) { using var aes = Aes.Create(); - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; aes.Key = key; - return aes.DecryptEcb(data, PaddingMode.None); + return decrypt ? aes.DecryptEcb(data, PaddingMode.None) : aes.EncryptEcb(data, PaddingMode.None); } - private static void AesEcbInPlace(byte[] key, Span page, bool decrypt) - { - using var aes = Aes.Create(); - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; - aes.Key = key; - int len = page.Length - page.Length % 16; - byte[] result = decrypt ? aes.DecryptEcb(page[..len], PaddingMode.None) : aes.EncryptEcb(page[..len], PaddingMode.None); - result.CopyTo(page); - } - - private static void Rc4(ReadOnlySpan key, Span data) => Rc4Cipher.Apply(key, data); - private static byte[] Fix(byte[] b, int len) { if (b.Length == len) return b; @@ -432,12 +455,4 @@ private static byte[] Fix(byte[] b, int len) Array.Copy(b, r, Math.Min(b.Length, len)); return r; } - - private static byte[] Concat(byte[] a, byte[] b) - { - byte[] r = new byte[a.Length + b.Length]; - a.CopyTo(r, 0); - b.CopyTo(r, a.Length); - return r; - } } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Crypto/SidKeystream.cs b/src/LibRed/LibRed.Core/Crypto/SidKeystream.cs index eeea445b..0d8df0c7 100644 --- a/src/LibRed/LibRed.Core/Crypto/SidKeystream.cs +++ b/src/LibRed/LibRed.Core/Crypto/SidKeystream.cs @@ -1,5 +1,6 @@ using System.Buffers.Binary; using LibRed.Formats; +using LibRed.Pages; namespace LibRed.Crypto; @@ -11,33 +12,31 @@ namespace LibRed.Crypto; /// internal static class SidKeystream { - private const int FoldRegion = 0x42; // 80 bytes from here: the 40-byte password field and 40 after it - private const int PasswordFieldSize = 40; - private const int CreationDateOffset = JetFormatBase.CreationDateOffset; + // The default workgroup's account SIDs, as a stock System.mdw's MSysAccounts holds them (the file Access opens + // first to authenticate). The Admins group alone has a 102-byte SID, which Access adds on first open, so it is + // not emitted. On disk each is XOR'd with the file's own keystream (see MaskAccount). + public static readonly byte[] AdminAccount = [0x03, 0x01]; // admin user — owner of what it creates, read grantee on the system tables + public static readonly byte[] UsersAccount = [0x02, 0x01]; // Users group — full grantee + public static readonly byte[] EngineAccount = [0x02, 0x03]; // Engine — owner of the system tables and the DAO containers + public static readonly byte[] CreatorAccount = [0x02, 0x04]; // Creator — the inheritable container grant's placeholder for an object's owner /// The first keystream bytes for a page 0 as stored on disk. Each SID is /// XOR'd from the stream's first byte, whatever its length. - public static byte[] For(ReadOnlySpan page0, int length) + public static byte[] For(ReadOnlySpan page0, int length, JetFormatBase format) { - // 1. Remove the fixed header mask. - int start = JetFormatBase.PageZeroHeaderMaskStart; - ReadOnlySpan headerMask = JetFormatBase.PageZeroHeaderMask; - Span header = stackalloc byte[start + headerMask.Length]; - page0[..header.Length].CopyTo(header); - for (int i = 0; i < headerMask.Length; i++) header[start + i] ^= headerMask[i]; - - // 2-3. The 80-byte region, its first 40 bytes (the password field) unmasked with the creation date's - // integer part, as the field is stored. - Span region = stackalloc byte[2 * PasswordFieldSize]; - header.Slice(FoldRegion, region.Length).CopyTo(region); - Span dateMask = stackalloc byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(dateMask, - (int)BinaryPrimitives.ReadDoubleLittleEndian(header.Slice(CreationDateOffset, 8))); - for (int i = 0; i < PasswordFieldSize; i++) region[i] ^= dateMask[i % 4]; + // 1-3. The region folded is the password field and as many bytes again after it, out from under the + // fixed header mask; the password field is then unmasked with the creation date's integer part, as it is + // stored. + int passwordSize = format.PasswordSize; + Span region = stackalloc byte[2 * passwordSize]; + DatabaseDefinitionPage.ReadMasked(page0, format.PasswordOffset, region, format); + Span creationDate = stackalloc byte[sizeof(double)]; + DatabaseDefinitionPage.ReadMasked(page0, format.CreationDateOffset, creationDate, format); + DatabaseDefinitionPage.XorPasswordDateMask(region[..passwordSize], creationDate); // 4-5. Seeded with the creation date's low four raw bytes, folding every even byte of the region. - uint key = BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(CreationDateOffset, 4)); - for (int i = 0; i < PasswordFieldSize; i++) key ^= (uint)region[2 * i] << (i % 24); + uint key = BinaryPrimitives.ReadUInt32LittleEndian(creationDate); + for (int i = 0; i < passwordSize; i++) key ^= (uint)region[2 * i] << (i % 24); // 6. RC4 under the key's four little-endian bytes. Span rc4Key = stackalloc byte[4]; @@ -49,11 +48,11 @@ public static byte[] For(ReadOnlySpan page0, int length) /// A workgroup account's SID — as the workgroup file's MSysAccounts holds it — the way this /// file stores it: the SID XOR the keystream, from its first byte. - public static byte[] MaskAccount(ReadOnlySpan page0, ReadOnlySpan account) + public static byte[] MaskAccount(ReadOnlySpan page0, ReadOnlySpan account, JetFormatBase format) { - byte[] stream = For(page0, account.Length); + byte[] stream = For(page0, account.Length, format); byte[] masked = account.ToArray(); for (int i = 0; i < masked.Length; i++) masked[i] ^= stream[i]; return masked; } -} +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/CatalogFormat.cs b/src/LibRed/LibRed.Core/Formats/CatalogFormat.cs deleted file mode 100644 index f3465c67..00000000 --- a/src/LibRed/LibRed.Core/Formats/CatalogFormat.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace LibRed.Formats; - -/// Shared MSysObjects catalog constants used when writing new object rows. -internal static class CatalogFormat -{ - /// The MSysObjects.ParentId of a top-level user object — the database's object container, - /// a constant 0x0F000001. Used for both table and query/view objects (verified vs Northwind). - public const int ObjectContainerParentId = 0x0F000001; - - /// The MSysObjects.ParentId of a relationship object — the Relationships container, - /// 0x0F000003 (verified vs ACE). A relationship may share its name with a table or a query but not with - /// another relationship (verified vs ACE). - public const int RelationshipContainerParentId = 0x0F000003; - - /// MSysObjects.Type value for a table object. - public const short ObjectTypeTable = 1; - - /// MSysObjects.Flags bit of a table that owns a complex column — set on exactly those tables, - /// and on no table without one (measured across Access-written files; system-catalog §11). - public const int ObjectFlagOwnsComplexColumns = 0x00040000; - - /// MSysObjects.Type value for a relationship object. - public const short ObjectTypeRelationship = 8; -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/ColumnFlags.cs b/src/LibRed/LibRed.Core/Formats/ColumnFlags.cs new file mode 100644 index 00000000..510ca0d0 --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/ColumnFlags.cs @@ -0,0 +1,52 @@ +namespace LibRed.Formats; + +/// +/// The column flag byte (). The user-column bits are modelled (read +/// into ColumnDef, written from it); the two catalog bits travel as a created column's raw +/// SystemFlags, and 0x08 is set on no column seen. +/// +[Flags] +internal enum ColumnFlags : byte +{ + None = 0, + + /// The column is fixed-length. + FixedLength = 0x01, + + /// The column is updatable (set on essentially every column). + Updatable = 0x02, + + /// The column is an AutoNumber. + AutoNumber = 0x04, + + /// A column of the engine's own catalog — every column of MSysObjects, MSysACEs, MSysQueries, + /// MSysRelationships and MSysComplexColumns, and no other. + SystemCatalog = 0x10, + + /// A catalog column holding a security identifier (MSysObjects.Owner, MSysACEs.SID), always with + /// . + SecurityId = 0x20, + + /// An AutoNumber column that generates GUIDs (Replication ID) rather than Longs. + GuidAutoNumber = 0x40, + + /// A hyperlink (a Memo column presented as a hyperlink). + Hyperlink = 0x80, +} + +/// The extended column flag byte (). +[Flags] +internal enum ColumnExtendedFlags : byte +{ + None = 0, + + /// The column can store compressed Unicode text (§7). + CompressedUnicode = 0x01, + + /// An attachment value column (FileData, FileFlags, FileName, FileTimeStamp, FileType, FileURL), in + /// the attachment template and every attachment flat table. + AttachmentValue = 0x10, + + /// A calculated (computed) column (ACE 14+): the 0xC0 pair, either bit set. + Calculated = 0xC0, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/EncryptionInfoFlags.cs b/src/LibRed/LibRed.Core/Formats/EncryptionInfoFlags.cs new file mode 100644 index 00000000..9a0f5826 --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/EncryptionInfoFlags.cs @@ -0,0 +1,17 @@ +namespace LibRed.Formats; + +/// The flags word of an EncryptionInfo and of its EncryptionHeader +/// (, ), as +/// MS-OFFCRYPTO §2.3.1 names its bits. +[Flags] +internal enum EncryptionInfoFlags : uint +{ + None = 0, + CryptoApi = 0x04, + DocProps = 0x08, + External = 0x10, + Aes = 0x20, + + /// The value an Agile EncryptionInfo carries in this word, which MS-OFFCRYPTO reserves. + Agile = 0x40, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/ForeignKeyType.cs b/src/LibRed/LibRed.Core/Formats/ForeignKeyType.cs new file mode 100644 index 00000000..a1e204a8 --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/ForeignKeyType.cs @@ -0,0 +1,19 @@ +namespace LibRed.Formats; + +/// A relationship's direction as an index-info block records it +/// (). +public enum ForeignKeyType : byte +{ + /// Not a relationship's index. + None = 0x00, + + /// This table is the parent, the referenced end — the half Access names .r… and keeps out of + /// its schema views. + Incoming = 0x01, + + /// This table is the child, the end holding the foreign key, with an index. + Outgoing = 0x02, + + /// The child end of a relationship declared FOREIGN KEY NO INDEX. + OutgoingNoIndex = 0x03, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/IndexFlags.cs b/src/LibRed/LibRed.Core/Formats/IndexAttributes.cs similarity index 55% rename from src/LibRed/LibRed.Core/Formats/IndexFlags.cs rename to src/LibRed/LibRed.Core/Formats/IndexAttributes.cs index c74d8bd2..7a1f827c 100644 --- a/src/LibRed/LibRed.Core/Formats/IndexFlags.cs +++ b/src/LibRed/LibRed.Core/Formats/IndexAttributes.cs @@ -1,28 +1,29 @@ namespace LibRed.Formats; /// -/// The flag bits at offset of an index-data block. Shared by the -/// read side (TableDefinitionPage) and the write sides (TdefBuilder, TableCreator) so the -/// bit values can't drift. Verified against ACE: plain 0x0080, IGNORE NULL 0x0082, DISALLOW NULL 0x0088, -/// primary key 0x0089. +/// The flag bits at of an index-data block. Verified against +/// ACE: plain 0x0080, IGNORE NULL 0x0082, DISALLOW NULL 0x0088, primary key 0x0089. /// -internal static class IndexFlags +[Flags] +public enum IndexAttributes : ushort { + None = 0, + /// The index is unique (WITH PRIMARY / a unique index or constraint). - public const ushort Unique = 0x0001; + Unique = 0x0001, /// WITH IGNORE NULL: rows with a null in any indexed column are excluded from the index. - public const ushort IgnoreNulls = 0x0002; + IgnoreNulls = 0x0002, /// WITH DISALLOW NULL (Required): the index rejects a null key. Set for primary keys and for /// any required index. - public const ushort Required = 0x0008; + Required = 0x0008, /// Always set on Access 2000+ index-data blocks. - public const ushort AlwaysSet = 0x0080; + AlwaysSet = 0x0080, /// The index is over a complex (multi-value / attachment) column. Set on every such index Access /// writes — always as 0x0289, with unique, required and always-set — and on no other (measured across /// Access-written files). - public const ushort ComplexColumn = 0x0200; + ComplexColumn = 0x0200, } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/IndexBlockFormat.cs b/src/LibRed/LibRed.Core/Formats/IndexBlockFormat.cs deleted file mode 100644 index 7e4dabd2..00000000 --- a/src/LibRed/LibRed.Core/Formats/IndexBlockFormat.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace LibRed.Formats; - -/// -/// Layout of the two per-index TDEF sub-structures on the Jet 4 / ACE family: the 52-byte index-data -/// block (column slots, root page, flags) and the 28-byte index-info block (name/data-block linkage -/// and relationship action bytes). Shared by the read side (TableDefinitionPage) and the write sides -/// (TdefBuilder, TableCreator) so the offsets and markers can't drift. -/// -internal static class IndexBlockFormat -{ - // Index-data block (52 bytes): a 0x783 marker, up to 10 column slots, root page, flags. - public const int DataBlockSize = 52; - public const int MaxColumns = 10; - public const int ColumnSlotSize = 3; // 2-byte column id + 1-byte flags - public const int ColumnsOffset = 0x04; - public const int UsageMapRowOffset = 0x22; // 1-byte row + 3-byte page for the index's pages - public const int RootPageOffset = 0x26; - public const int FlagsOffset = 0x2E; - public const short ColumnUnused = -1; // 0xFFFF in an unused column slot - public const byte ColumnAscending = 0x01; // slot flags byte: 0x01 = ascending, 0x00 = descending - public const uint DataMarker = 0x783; - - // Index-info block (28 bytes, one per logical index): links a name to a data block. - public const int InfoBlockSize = 28; - public const int InfoMarkerOffset = 0x00; // 0x0659 record marker - public const int InfoNumberOffset = 0x04; - public const int InfoDataNumberOffset = 0x08; - public const int InfoFkTypeOffset = 0x0C; // 0=none, 1=incoming, 2=outgoing relationship - public const int InfoFkNumberOffset = 0x0D; // 0xFFFFFFFF = no foreign key - public const int InfoFkTablePageOffset = 0x11; // the other table's TDEF page (0 = none) - public const int InfoUpdateActionOffset = 0x15; - public const int InfoDeleteActionOffset = 0x16; - public const int InfoTypeOffset = 0x17; - public const uint NoForeignKey = 0xFFFFFFFF; - - /// Update/delete action byte on a plain (non-relationship) index. - public const byte PlainAction = 0x04; - - // Update/delete action bytes on a relationship's blocks, verified against ACE-created relationships. These, - // not MSysRelationships.grbit, are what ACE acts on: with the two made to disagree, it cascades by the block. - public const byte NoCascadeAction = 0x00; - public const byte CascadeAction = 0x01; - public const byte SetNullAction = 0x02; // delete only: ON DELETE SET NULL - - // Index-info block type byte (at InfoTypeOffset). - public const byte TypeSecondary = 0x00; - public const byte TypePrimary = 0x01; - public const byte TypeForeign = 0x02; -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/IndexColumnOrder.cs b/src/LibRed/LibRed.Core/Formats/IndexColumnOrder.cs new file mode 100644 index 00000000..a312962c --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/IndexColumnOrder.cs @@ -0,0 +1,8 @@ +namespace LibRed.Formats; + +/// An index-data block column slot's order byte (). +public enum IndexColumnOrder : byte +{ + Descending = 0x00, + Ascending = 0x01, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/IndexInfoType.cs b/src/LibRed/LibRed.Core/Formats/IndexInfoType.cs new file mode 100644 index 00000000..2c0316fa --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/IndexInfoType.cs @@ -0,0 +1,9 @@ +namespace LibRed.Formats; + +/// An index-info block's type byte (). +public enum IndexInfoType : byte +{ + Secondary = 0x00, + Primary = 0x01, + Foreign = 0x02, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/IndexKeyFlags.cs b/src/LibRed/LibRed.Core/Formats/IndexKeyFlags.cs deleted file mode 100644 index ad61542a..00000000 --- a/src/LibRed/LibRed.Core/Formats/IndexKeyFlags.cs +++ /dev/null @@ -1,49 +0,0 @@ -using LibRed.Catalog; - -namespace LibRed.Formats; - -/// -/// The per-column prefix bytes of an order-preserving index key. Each non-boolean column is prefixed by a -/// start byte (present value) or null byte, with distinct values for ascending vs descending columns so that -/// a lexicographic byte compare matches the index's logical order. Shared by the write side -/// (IndexKeyEncoder) and the read side (IndexKeyDecoder) so the two can't drift. -/// -internal static class IndexKeyFlags -{ - /// Ascending column, present value. - public const byte AscStart = 0x7F; - - /// Ascending column, null value. - public const byte AscNull = 0x00; - - /// Descending column, present value. - public const byte DescStart = 0x80; - - /// Descending column, null value. - public const byte DescNull = 0xFF; - - /// - /// The key width of a fixed-width column type, or -1 where the key is not fixed-width — TEXT, Binary, - /// GUID and DATETIME2 all encode to a variable, and for the first three lossy, form - /// (page-03-04 §10.4). Here for the same reason as the flags above: the encoder and the decoder each had - /// their own copy of this table and they drifted, the decoder never learning the widths the encoder - /// writes for and . - /// - public static int FixedKeySize(JetDataType type) => type switch - { - JetDataType.Byte => 1, - JetDataType.Int16 => 2, - JetDataType.Int32 => 4, - // A complex (multi-value / attachment) column's key is its Int32 complex id, encoded exactly as an - // Int32 — verified against ACE over 43 entries across 9 such indexes in two files, covering - // attachment, Text and Long element types, with no difference in any byte. - JetDataType.Complex => 4, - JetDataType.Single => 4, - JetDataType.Double or JetDataType.DateTime => 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, - }; -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/Jet3Format.cs b/src/LibRed/LibRed.Core/Formats/Jet3Format.cs index e8f30b9c..95f00f5f 100644 --- a/src/LibRed/LibRed.Core/Formats/Jet3Format.cs +++ b/src/LibRed/LibRed.Core/Formats/Jet3Format.cs @@ -1,9 +1,12 @@ namespace LibRed.Formats; -/// Access 97 (Jet 3.x) — 2 KB pages, MDB. -internal sealed class Jet3Format : JetFormatBase +/// Access 97 (Jet 3.x) — 2 KB pages, MDB. Abstract until its offsets are set: nothing reads or writes +/// Jet 3 yet, and refuses its version byte. +internal abstract class Jet3Format : JetFormatBase { - public Jet3Format() => PageSize = 2048; + public override int PageSize => 2048; public override JetVersion Version => JetVersion.Version3; + + public override bool IsAccdb => false; } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/Jet4Format.cs b/src/LibRed/LibRed.Core/Formats/Jet4Format.cs index 708f9a65..6ec8e3dd 100644 --- a/src/LibRed/LibRed.Core/Formats/Jet4Format.cs +++ b/src/LibRed/LibRed.Core/Formats/Jet4Format.cs @@ -1,9 +1,207 @@ namespace LibRed.Formats; -/// Access 2000–2003 (Jet 4.x) — 4 KB pages, MDB. +/// Access 2000–2003 (Jet 4.x) — 4 KB pages, MDB. Every ACE format derives from it: the layout is +/// the same (verified against real MDB and ACCDB files), and each ACE version overrides only what it changes. internal class Jet4Format : JetFormatBase { - public Jet4Format() => PageSize = 4096; - + // --- File --- + public override int PageSize => 4096; public override JetVersion Version => JetVersion.Version4; + public override bool IsAccdb => false; + + // --- Page 0 --- + public override int MinorVersionOffset => 0x15; + public override int PageZeroHeaderMaskStart => 0x18; + public override int PageZeroHeaderMaskLength => 128; + public override ReadOnlySpan PageZeroHeaderMaskKey => [0xC7, 0xDA, 0x39, 0x6B]; + public override int FreePagesMapPointerOffset => 0x18; + public override int ReleasedPagesMapPointerOffset => 0x1C; + public override int CatalogRootPointerOffset => 0x20; + public override int AcesRootPointerOffset => 0x24; + public override int QueriesRootPointerOffset => 0x28; + public override int RelationshipsRootPointerOffset => 0x2C; + public override int AccountsRootPointerOffset => 0x30; + public override int GroupsRootPointerOffset => 0x34; + public override int CodePageOffset => 0x3C; + public override int DatabaseKeyOffset => 0x3E; + public override int PasswordOffset => 0x42; + public override int PasswordSize => 40; // 20 UTF-16LE chars + public override int EngineBuildOffset => 0x6A; + public override int CollationSortOrderOffset => 0x6E; + public override int CollationSortIdOffset => 0x70; + public override int CollationVersionOffset => 0x71; + public override int CreationDateOffset => 0x72; + public override int PageZeroConstantOffset => 0x98; + public override int PageZeroConstant => 0x00000654; + public override int EncryptionInfoLengthOffset => 0x299; + public override int EncryptionInfoOffset => 0x29B; + public override int CommitByteTableUsers => 256; + public override int CommitByteSlotSize => 2; + public override int CommitByteTableOffset => PageSize - CommitByteTableUsers * CommitByteSlotSize; // 0xE00 + + // --- Data page layout --- + public override int DataFreeSpaceOffset => 0x02; + public override int DataOwnerOffset => 0x04; + public override int DataChainStampOffset => 0x08; + public override int DataRowCountOffset => 0x0C; + public override int DataRowDirectoryOffset => 0x0E; + public override int DataRowDirectoryEntrySize => 2; + public override int DataRowOffsetMask => 0x1FFF; + + // ACE itself writes at most 255 rows, one below what a byte can name, and LibRed matches ACE rather than the + // pointer's maximum so its pages are shapes Access also produces. Only narrow rows reach it: a 4 KB page fits + // this many once they are under about 14 bytes each. ACE's cap is not about space — a page it had filled to 255 + // still held 2,297 of its 4,096 bytes free — and on reaching it ACE drops the page from the table's free-pages + // map exactly as if it were full. Exceeding it loses rows silently: every row past slot 255 is unaddressable by + // any index, its entry aliasing a different row on another page, and ACE cannot see those rows at all — it + // reads the full 16-bit row count but caps at 256 slots per page. Measured: LibRed wrote 900 rows as + // 400/400/100 and read all 900 back, while ACE counted 612, exactly 256 + 256 + 100. + public override int MaxRowsPerPage => 255; + + public override int RowColumnCountSize => 2; + public override int RowVariableOffsetSize => 2; + public override int RowVariableCountSize => 2; + + // 4060 bytes, where ACE raises "Record is too large." Measured across three table shapes whose row overhead + // differs by 23 bytes (9, 12 and 20 text columns), and the total lands on 4060 every time, so it is a flat + // cap rather than something derived from the row's layout (RecordSizeAccessTests). It is 20 bytes below + // what the page could hold — 4096 less the 14-byte header and a 2-byte slot leaves 4080 — and that reserve + // is not explained. Enforcing it is not optional politeness: a record between 4061 and 4080 fits the page + // and LibRed used to write it happily, but ACE then cannot read the row, failing with an unrelated "another + // user are attempting to change the same data" error. + public override int MaxRecordSize => 4060; + + // --- Table definition (TDEF) page header (verified against a real ACCDB) --- + public override int TdefFreeSpaceOffset => 0x02; + public override int TdefNextPageOffset => 0x04; + public override int TdefLengthOffset => 0x08; + public override int TdefRecordMarkerOffset => 0x0C; + public override int TdefRowCountOffset => 0x10; + public override int TdefLastAutoNumberOffset => 0x14; + public override int TdefAutoNumberIncrementOffset => 0x18; + public override int TdefComplexAutoNumberOffset => 0x1C; + public override int TdefTableTypeOffset => 0x28; + public override int TdefMaxColumnsOffset => 0x29; + public override int TdefVariableColumnsOffset => 0x2B; + public override int TdefColumnCountOffset => 0x2D; + public override int TdefLogicalIndexCountOffset => 0x2F; + public override int TdefIndexCountOffset => 0x33; + public override int TdefOwnedPagesOffset => 0x37; + public override int TdefFreePagesOffset => 0x3B; + public override int TdefRealIndexBlockOffset => 0x3F; + public override int TdefContinuationHeaderSize => 8; + + // --- TDEF limits --- + public override int MaxColumnsPerTable => 255; + public override int MaxIndexesPerTable => 32; + public override int MaxNameBytes => Catalog.JetName.MaxLength * 2; // 64 UTF-16 code units (verified Access limit) + public override int TdefNameLengthSize => 2; + + // --- TDEF real-index block --- + public override int RealIndexEntrySize => 12; // [+0] total entries, [+4] unique entries, [+8] reserved + public override int RealIndexRowCountOffset => 0; + public override int RealIndexUniqueCountOffset => 4; + + // --- TDEF column descriptor --- + public override int ColumnDescriptorSize => 25; + public override int ColumnTypeOffset => 0x00; + public override int ColumnRecordMarkerOffset => 0x01; + public override int ColumnNumberOffset => 0x05; + public override int ColumnVariableIndexOffset => 0x07; + public override int ColumnSecondaryNumberOffset => 0x09; + public override int ColumnPrecisionOffset => 0x0B; + public override int ColumnScaleOffset => 0x0C; + public override int ColumnLocaleOffset => 0x0B; + public override int ColumnCollationSortIdOffset => 0x0D; + public override int ColumnCollationVersionOffset => 0x0E; + public override int ColumnFlagsOffset => 0x0F; + public override int ColumnExtendedFlagsOffset => 0x10; + public override int ColumnFixedOffsetOffset => 0x15; + public override int ColumnLengthOffset => 0x17; + + // --- TDEF index-data block --- + public override int IndexDataBlockSize => 52; + public override int IndexDataColumnsOffset => 0x04; + public override int IndexDataMaxColumns => 10; + public override int IndexDataColumnSlotSize => 3; + public override int IndexDataColumnOrderOffset => 2; + public override int IndexDataUsageMapOffset => 0x22; + public override int IndexDataRootPageOffset => 0x26; + public override int IndexDataFlagsOffset => 0x2E; + + // --- TDEF index-info block --- + public override int IndexInfoBlockSize => 28; + public override int IndexInfoMarkerOffset => 0x00; + public override int IndexInfoNumberOffset => 0x04; + public override int IndexInfoDataNumberOffset => 0x08; + public override int IndexInfoFkTypeOffset => 0x0C; + public override int IndexInfoFkNumberOffset => 0x0D; + public override int IndexInfoFkTablePageOffset => 0x11; + public override int IndexInfoUpdateActionOffset => 0x15; + public override int IndexInfoDeleteActionOffset => 0x16; + public override int IndexInfoTypeOffset => 0x17; + + // --- TDEF long-value map list --- + public override int TdefLongValueMapEntrySize => 10; + public override int TdefLongValueMapOwnedOffset => 2; + public override int TdefLongValueMapFreeOffset => 6; + + // --- Long values --- + public override int LongValueDescriptorSize => 12; + + // ACE accepted and fully read back 0x3FFFFFFF binary bytes, then rejected 0x40000000. The length carries + // into the descriptor's byte 3; its storage bits must remain separate. + public override int LongValueLengthMask => 0x3FFFFFFF; + public override int LongValueDescriptorPointerOffset => 4; + public override int LongValueDescriptorChainStampOffset => 8; + public override int LongValueMaxInline => 64; + + // Measured (LongTextStorageAccessTests): 3816 bytes stays single-page, 3818 chains, for a plain and a WITH + // COMPRESSION column alike. Distinct from LongValueMaxRowSize — conflating the two made LibRed keep 3817–4076 + // byte values on one page where ACE chains them. What fixes the boundary at 3816 is not established. + public override int LongValueMaxSinglePage => 3816; + + // MAX_LONG_VALUE_ROW_SIZE (Jackcess): 4 bytes short of the page's usable space. Verified against ACE's own + // chained OLE values (Northwind Employee photos: 4076, 4076, 2606-byte chunk rows). + public override int LongValueMaxRowSize => 4076; + + // --- Usage maps --- + public override int UsageMapStartPageOffset => 1; + public override int UsageMapInlineHeaderSize => 5; + + // 512 pages. A movable window is exactly this, aligned to a 512-page boundary — verified against ACE + // free-pages maps: the sole set bit at page 852/1227/1852/2852 sat in a 64-byte record whose startPage was + // 512/1024/1536/2560, i.e. floor(page / 512) * 512. + public override int UsageMapInlineBitmapSize => 64; + public override int UsageMapInlineRecordSize => UsageMapInlineHeaderSize + UsageMapInlineBitmapSize; + + // Verified against owned-map record lengths on a 255-column ACE table whose data pages start at 353: + // 8,000 rows → 1053, 12,000 → 1553, 30,000 → 3801, i.e. 5 + roundUp(ceil((353 + rows) / 8), 4) exactly. + // (A 32-byte chunk would give 1056 / 1568 / 3808.) Overshooting spends the usage-map page's remaining room + // and converts the map to reference type earlier than Access would. + public override int UsageMapInlineGrowthSize => 4; + + // Measured against ACE (GlobalMapGrowthTests). + public override int UsageMapHolderReserve => 4; + + public override int UsageMapReferencePointersOffset => 1; + + // Not arbitrary: each bitmap page covers (pageSize - 4) * 8 = 32,736 pages, so 17 slots span ~2.28 GB — + // just past Jet's 2 GB file ceiling. Verified against an ACE-built 134 MB table. + public override int UsageMapReferenceSlots => 17; + public override int UsageMapReferenceRecordSize => UsageMapReferencePointersOffset + UsageMapReferenceSlots * sizeof(int); + public override int UsageMapBitmapPageHeaderSize => 4; // type + flags + 2 unused + public override int UsageMapPagesPerBitmapPage => (PageSize - UsageMapBitmapPageHeaderSize) * 8; + + // --- Index page layout --- + public override int IndexFreeSpaceOffset => 0x02; + public override int IndexOwnerOffset => 0x04; + public override int IndexPrevPageOffset => 0x0C; + public override int IndexNextPageOffset => 0x10; + public override int IndexChildTailOffset => 0x14; + public override int IndexCompressedByteCountOffset => 0x18; + public override int IndexLevelOffset => 0x1A; + public override int IndexEntryMaskOffset => 0x1B; + public override int IndexEntryDataOffset => 0x1E0; + public override int IndexEntryTrailerSize => 4; } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs index 3249ebf3..1d38e519 100644 --- a/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs +++ b/src/LibRed/LibRed.Core/Formats/JetFormatBase.cs @@ -1,36 +1,21 @@ namespace LibRed.Formats; /// -/// Version-specific layout description for a Jet/ACE database. Holds every byte -/// offset, size and limit that differs between format versions, so the page -/// parsers can read named constants instead of hard-coded magic numbers. +/// Version-specific layout description for a Jet/ACE database. Names every byte offset, size and limit of the +/// on-disk format; each format family sets its own values — for Jet 4 and, by +/// inheritance, every ACE version, and for Jet 3. /// /// -/// The authoritative references for these values are the mdbtools source -/// (include/mdbtools.h, src/libmdb/) and Jackcess -/// (com.healthmarketscience.jackcess.impl.JetFormat). +/// Two kinds of value live here rather than in a format: what has to be read before a format is known — the +/// format identifier and version byte page 0 is recognised by, and the engine-version string an unknown ACE byte +/// is checked against — and the marker values written inside the structures (, +/// , , , +/// , ). The authoritative references for the layouts are the mdbtools source (include/mdbtools.h, +/// src/libmdb/) and Jackcess (com.healthmarketscience.jackcess.impl.JetFormat). /// public abstract class JetFormatBase { - /// Offset of the one-byte format version marker within page 0. - public const int VersionOffset = 0x14; - - /// Offset of the one-byte minor version that follows the version byte. - public const int MinorVersionOffset = 0x15; - - /// The minor byte ACE writes when it CREATES a database of : 0x01 - /// for the 2010 format (0x03), 0x00 for every other. A version raise writes 0x00 - /// whatever the target, so a 2007 file raised to 0x03 does not carry the 0x01 a created one - /// does. - public static byte CreatedMinorVersion(byte version) => (byte)(version == 0x03 ? 0x01 : 0x00); - - /// Offset of the cleartext ASCII engine-version string ("4.0", NUL-terminated) — past the masked - /// header window, so readable directly. Present on both Jet 4 (.mdb) and ACE (.accdb), which - /// are both the Jet-4.0 engine. Used to confirm an unknown version byte is still a 4.0-family database - /// before falling back to the latest known ACE layout. - public const int EngineVersionOffset = 0x9C; - public const int EngineVersionLength = 4; - private const string Jet40EngineVersion = "4.0"; + // --- Format detection (page 0, read before the format is known) --- /// Offset of the ASCII format identifier string within page 0. public const int FormatIdentifierOffset = 0x04; @@ -49,128 +34,254 @@ public abstract class JetFormatBase /// ). public const string JetSystemIdentifier = "Jet System DB"; - // --- Page 0 obfuscated header (0x18..0x98) --- - // The header is XOR-obfuscated with a fixed 128-byte mask (below): the RC4 keystream of the key - // C7 DA 39 6B, the same for every file. Field offsets and the mask - // are corroborated by mdbtools and Jackcess AND verified against real files here: the mask - // reproduces the code page (0x3C), collation LCID (0x6E) and creation date (0x72) bytes we - // recovered independently by known-plaintext, and it decodes every fixture's header to sensible - // values (see docs/format/page-00-database.md §2.1). + /// Offset of the one-byte format version marker within page 0. + public const int VersionOffset = 0x14; + + /// Offset of the cleartext ASCII engine-version string ("4.0", NUL-terminated) — past the masked + /// header window, so readable directly. Present on both Jet 4 (.mdb) and ACE (.accdb), which + /// are both the Jet-4.0 engine. Used to confirm an unknown version byte is still a 4.0-family database + /// before falling back to the latest known ACE layout. + public const int EngineVersionOffset = 0x9C; + public const int EngineVersionLength = 4; + public const string Jet40EngineVersion = "4.0"; + + // --- File --- + + /// Page size in bytes. + public abstract int PageSize { get; } + + /// The logical version this format describes. + public abstract JetVersion Version { get; } + + /// True for the ACCDB (ACE 12+) family, which uses different encryption and layout details. + public abstract bool IsAccdb { get; } + + // --- Page 0 --- + + /// Offset of the one-byte minor version that follows the version byte. + public abstract int MinorVersionOffset { get; } + + // The header is XOR-obfuscated with a fixed mask: the RC4 keystream of a fixed key, the same for every file. + // Field offsets and the mask are corroborated by mdbtools and Jackcess AND verified against real files here: + // the mask reproduces the code page, collation LCID and creation date bytes we recovered independently by + // known-plaintext, and it decodes every fixture's header to sensible values (see + // docs/format/page-00-database.md §2.1). /// Start offset of the obfuscated page-0 header region (also the mask's first byte). - public const int PageZeroHeaderMaskStart = 0x18; + public abstract int PageZeroHeaderMaskStart { get; } + + /// Length of the obfuscated page-0 header region, and so of the mask. + public abstract int PageZeroHeaderMaskLength { get; } + + /// The fixed RC4 key whose keystream is the page-0 header mask. + public abstract ReadOnlySpan PageZeroHeaderMaskKey { get; } + + /// The XOR mask applied to the page-0 header from : the first + /// bytes of the RC4 keystream under . + public ReadOnlySpan PageZeroHeaderMask + { + get + { + if (_pageZeroHeaderMask is null) + { + var mask = new byte[PageZeroHeaderMaskLength]; + Crypto.Rc4Cipher.Apply(PageZeroHeaderMaskKey, mask); // the keystream, XOR'd over zeros + _pageZeroHeaderMask = mask; + } + return _pageZeroHeaderMask; + } + } + + private byte[]? _pageZeroHeaderMask; /// Offset of the 4-byte [row:1][page:3] pointer to the global free-pages usage map — the /// map every allocation takes a page from. Page 1 row 0 in every file ACE writes, but ACE follows the /// pointer, row included, so a reader must too (docs/format/page-05-usage-maps.md §9.1). - public const int FreePagesMapPointerOffset = 0x18; + public abstract int FreePagesMapPointerOffset { get; } /// Offset of the 4-byte [row:1][page:3] pointer to the global released-pages usage map: /// pages released but not yet reusable, which ACE never allocates. Page 1 row 1 in every file ACE writes. - public const int ReleasedPagesMapPointerOffset = 0x1C; + public abstract int ReleasedPagesMapPointerOffset { get; } /// Offset of the 4-byte page number of the MSysObjects TDEF — the catalog root, the /// bootstrap pointer that lets the engine find the system catalog before it can read any table. It is - /// the first of four system-table pointers (MSysObjects/MSysACEs/MSysQueries/ - /// MSysRelationships at 0x20/0x24/0x28/0x2C, values 2/3/4/5 in every - /// file); the others are reachable via the catalog itself. Verified: each value equals the object's - /// MSysObjects.Id and the page it names is a TDEF. - public const int CatalogRootPointerOffset = 0x20; + /// the first of six system-table pointers (MSysObjects/MSysACEs/MSysQueries/ + /// MSysRelationships, then MSysAccounts/MSysGroups in a workgroup file); the others + /// are reachable via the catalog itself. Verified: each value equals the object's MSysObjects.Id + /// and the page it names is a TDEF. + public abstract int CatalogRootPointerOffset { get; } + + /// Offset of the 4-byte page number of the MSysACEs TDEF. + public abstract int AcesRootPointerOffset { get; } + + /// Offset of the 4-byte page number of the MSysQueries TDEF. + public abstract int QueriesRootPointerOffset { get; } + + /// Offset of the 4-byte page number of the MSysRelationships TDEF. + public abstract int RelationshipsRootPointerOffset { get; } + + /// Offset of the 4-byte page number of the MSysAccounts TDEF in a workgroup file; zero in an + /// ordinary database, which has no such table. + public abstract int AccountsRootPointerOffset { get; } + + /// Offset of the 4-byte page number of the MSysGroups TDEF in a workgroup file; zero in an + /// ordinary database, which has no such table. + public abstract int GroupsRootPointerOffset { get; } /// Offset of the 2-byte ANSI code page (LE): 0x04E4 = 1252, 0x04E2 = 1250. - public const int CodePageOffset = 0x3C; + public abstract int CodePageOffset { get; } /// Offset of the 4-byte database (encryption) key; 0 when the database has no password. - public const int DatabaseKeyOffset = 0x3E; + public abstract int DatabaseKeyOffset { get; } - /// Offset of the database password (Jet 4: 40 bytes; additionally masked by a - /// creation-date-derived value, so an empty password does not read as zeroes). - public const int PasswordOffset = 0x42; + /// Offset of the database password field, additionally masked by a creation-date-derived value, + /// so an empty password does not read as zeroes. + public abstract int PasswordOffset { get; } + + /// Size of the database password field, UTF-16LE. + public abstract int PasswordSize { get; } + + /// Offset of the 4-byte build number of the engine that created the file — stamped once and + /// preserved across copy and compact, so it is not a constant (docs/format/page-00-database.md). + public abstract int EngineBuildOffset { get; } /// 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; + /// descriptor's block: LANGID (2 bytes LE, 0x0409 = 1033 en-US), + /// then , then . + public abstract int CollationSortOrderOffset { get; } /// 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; + public abstract int CollationSortIdOffset { get; } /// Offset of the 1-byte collation sort-order version within the sort-order field (0/1). - public const int CollationVersionOffset = 0x71; + public abstract int CollationVersionOffset { get; } /// Offset of the 8-byte database creation timestamp: an OLE automation date /// (IEEE double, days from the 1899-12-30 epoch). - public const int CreationDateOffset = 0x72; + public abstract int CreationDateOffset { get; } + + /// Offset of a 4-byte cleartext constant just past the masked window; undecoded. + public abstract int PageZeroConstantOffset { get; } + + /// The value every file carries at . + public abstract int PageZeroConstant { get; } + + /// Offset of the 2-byte EncryptionInfo blob length (LE) — Access's "is this file + /// encrypted?" signal, 0 when unencrypted. Only meaningful for an ACCDB. + public abstract int EncryptionInfoLengthOffset { get; } + + /// Offset of the EncryptionInfo descriptor, bytes + /// long: a binary Office "Standard" header or an XML Agile descriptor. + public abstract int EncryptionInfoOffset { get; } + + /// Number of slots in the user commit-byte table: the exclusive-mode slot, then one per shared-mode + /// user (docs/format/page-00-database.md §2.2). + public abstract int CommitByteTableUsers { get; } + + /// Size of one commit-byte table slot. + public abstract int CommitByteSlotSize { get; } + + /// Offset of the user commit-byte table, which runs to the end of page 0: + /// slots of bytes, the first the + /// exclusive-mode commit state. Nothing else may be written over it — an EncryptionInfo descriptor has + /// to end before it. + public abstract int CommitByteTableOffset { get; } + + // --- Data page layout --- + + /// Offset of the 2-byte free-space count on a data page. + public abstract int DataFreeSpaceOffset { get; } + + /// Offset of the 4-byte owning-table TDEF page (or the "LVAL" marker on long-value pages). + public abstract int DataOwnerOffset { get; } + + /// + /// Offset of the 4-byte chain stamp on a data page. Zero on every page but the first of a + /// multi-page long-value chain, where it must equal the pointing descriptor's own stamp or ACE refuses to + /// materialise the value — see docs/format/long-values.md. Jet 3 has the row count where Jet 4 has + /// this (which is why Jet 4's row count sits four bytes later), so a Jet 3 file has nowhere to put one. + /// + public abstract int DataChainStampOffset { get; } + + /// Offset of the 2-byte row count on a data page. + public abstract int DataRowCountOffset { get; } + + /// Offset of the row-offset slot directory, bytes per row. + public abstract int DataRowDirectoryOffset { get; } + + /// Size of one entry in the row-offset slot directory. + public abstract int DataRowDirectoryEntrySize { get; } + + /// The row slot directory entry's offset bits; the bits above are its . + public abstract int DataRowOffsetMask { get; } /// - /// The fixed 128-byte XOR mask applied to the page-0 header from - /// (Jet 4 / ACE; Jet 3 uses 126 bytes). This is Jackcess's BASE_HEADER_MASK, verified here to - /// reproduce the header bytes LibRed recovered from first principles and to decode real fixtures. + /// The most rows a data page may hold. Not a space limit — an index entry addresses a row as + /// page << 8 | row (page-03-04-index-btree.md §10.2), and a long-value descriptor its row in one + /// byte too, so the slot number has to fit one byte. /// - public static ReadOnlySpan PageZeroHeaderMask => - [ - 0xB5, 0x6F, 0x03, 0x62, 0x61, 0x08, 0xC2, 0x55, 0xEB, 0xA9, 0x67, 0x72, 0x43, 0x3F, 0x00, 0x9C, - 0x7A, 0x9F, 0x90, 0xFF, 0x80, 0x9A, 0x31, 0xC5, 0x79, 0xBA, 0xED, 0x30, 0xBC, 0xDF, 0xCC, 0x9D, - 0x63, 0xD9, 0xE4, 0xC3, 0x7B, 0x42, 0xFB, 0x8A, 0xBC, 0x4E, 0x86, 0xFB, 0xEC, 0x37, 0x5D, 0x44, - 0x9C, 0xFA, 0xC6, 0x5E, 0x28, 0xE6, 0x13, 0xB6, 0x8A, 0x60, 0x54, 0x94, 0x7B, 0x36, 0xF5, 0x72, - 0xDF, 0xB1, 0x77, 0xF4, 0x13, 0x43, 0xCF, 0xAF, 0xB1, 0x33, 0x34, 0x61, 0x79, 0x5B, 0x92, 0xB5, - 0x7C, 0x2A, 0x05, 0xF1, 0x7C, 0x99, 0x01, 0x1B, 0x98, 0xFD, 0x12, 0x4F, 0x4A, 0x94, 0x6C, 0x3E, - 0x60, 0x26, 0x5F, 0x95, 0xF8, 0xD0, 0x89, 0x24, 0x85, 0x67, 0xC6, 0x1F, 0x27, 0x44, 0xD2, 0xEE, - 0xCF, 0x65, 0xED, 0xFF, 0x07, 0xC7, 0x46, 0xA1, 0x78, 0x16, 0x0C, 0xED, 0xE9, 0x2D, 0x62, 0xD4, - ]; - - // --- Table definition (TDEF) page layout --- - // Defaults below are for Jet 4 / ACE (verified against a real ACCDB). Jet 3 differs - // (18-byte column entries, 1-byte ASCII name lengths) and will override these. + public abstract int MaxRowsPerPage { get; } + + /// Size of the column-count field at the start of a row record. The count (the highest column id ever + /// handed out, plus one) also sets the width of the row's null bitmap: one bit per column id. + public abstract int RowColumnCountSize { get; } + + /// Size of one entry of a row's variable-offset table — numVar + 1 entries, end-first, the last + /// the variable-data start. + public abstract int RowVariableOffsetSize { get; } + + /// Size of a row's variable-column count (numVar), which follows its offset table. + public abstract int RowVariableCountSize { get; } + + /// The largest record the engine will store, excluding anything that lives on LVAL pages. + public abstract int MaxRecordSize { get; } + + // --- Table definition (TDEF) page header --- /// Offset of the 2-byte free-space count: the bytes still free in this page. (Before it, at 0x00, /// the 2-byte page type — .) - public virtual int TdefFreeSpaceOffset => 0x02; + public abstract int TdefFreeSpaceOffset { get; } /// Offset of the 4-byte pointer to the next TDEF page (0 if the definition fits one page). - public virtual int TdefNextPageOffset => 0x04; + public abstract int TdefNextPageOffset { get; } /// Offset of the 4-byte total TDEF definition length. - public virtual int TdefLengthOffset => 0x08; - - /// Offset of the 4-byte TDEF record marker (0x00000659); see . - public virtual int TdefRecordMarkerOffset => 0x0C; + public abstract int TdefLengthOffset { get; } - /// Offset of the 2-byte maximum-column-count high-water (the next column id to assign). - public virtual int TdefMaxColumnsOffset => 0x29; + /// Offset of the 4-byte TDEF record marker; see . + public abstract int TdefRecordMarkerOffset { get; } /// The 0x00000659 record marker written at the TDEF header (), /// each column descriptor (+0x01) and each index-info block (+0x00). Access validates it; the reader ignores it. public const uint TdefRecordMarker = 0x00000659; - /// Size of the 8-byte continuation header that prefixes each TDEF continuation page's payload - /// (also the free-space reserve the first page leaves for it). - public const int TdefContinuationHeaderSize = 8; - /// Offset of the 4-byte row count. - public virtual int TdefRowCountOffset => 0x10; + public abstract int TdefRowCountOffset { get; } /// Offset of the 4-byte highest-AutoNumber-assigned value (the last id used; next = +increment). - public virtual int TdefLastAutoNumberOffset => 0x14; + public abstract int TdefLastAutoNumberOffset { get; } /// Offset of the 4-byte AutoNumber increment (default 1; a custom COUNTER sets it). - public virtual int TdefAutoNumberIncrementOffset => 0x18; + public abstract int TdefAutoNumberIncrementOffset { get; } /// Offset of the 4-byte complex-type AutoNumber high-water (mdbtools ct_autonum) — the /// next id for a complex (multi-value/attachment) column. 0 for every table without such a column. - public virtual int TdefComplexAutoNumberOffset => 0x1C; + public abstract int TdefComplexAutoNumberOffset { get; } /// Offset of the 1-byte table type (0x4E 'N' user, 0x53 'S' system). - public virtual int TdefTableTypeOffset => 0x28; + public abstract int TdefTableTypeOffset { get; } + + /// Offset of the 2-byte maximum-column-count high-water (the next column id to assign). + public abstract int TdefMaxColumnsOffset { get; } /// Offset of the 2-byte variable-length column count. - public virtual int TdefVariableColumnsOffset => 0x2B; + public abstract int TdefVariableColumnsOffset { get; } /// Offset of the 2-byte total column count. - public virtual int TdefColumnCountOffset => 0x2D; + public abstract int TdefColumnCountOffset { get; } /// /// Offset of the 4-byte logical index count — the §3.6 index-info blocks, one per named index. @@ -182,11 +293,11 @@ public abstract class JetFormatBase /// /// Named for the meaning rather than the offset's history, because the history is a trap: this was /// previously TdefRealIndexCountOffset, which inverts the reference vocabulary. mdbtools calls - /// 0x2F num_idx ("number of logical indexes") and 0x33 num_real_idx; - /// Jackcess calls them OFFSET_NUM_INDEX_SLOTS and OFFSET_NUM_INDEXES. "Real" belongs to - /// 0x33, not here. + /// it num_idx ("number of logical indexes") and + /// num_real_idx; Jackcess calls them OFFSET_NUM_INDEX_SLOTS and OFFSET_NUM_INDEXES. + /// "Real" belongs to the other one, not here. /// - public virtual int TdefLogicalIndexCountOffset => 0x2F; + public abstract int TdefLogicalIndexCountOffset { get; } /// /// Offset of the 4-byte real index count — the §3.5 index-data blocks, one per B-tree actually @@ -194,124 +305,317 @@ public abstract class JetFormatBase /// and the index-data blocks that follow the column names. /// mdbtools calls it num_real_idx; Jackcess calls it OFFSET_NUM_INDEXES. /// - public virtual int TdefIndexCountOffset => 0x33; + public abstract int TdefIndexCountOffset { get; } + + /// Offset in a TDEF of the owned-pages usage-map pointer: 1 byte row, then a 3-byte page. + public abstract int TdefOwnedPagesOffset { get; } + + /// Offset in a TDEF of the free-pages usage-map pointer (same 1-byte row + 3-byte page shape): + /// the subset of the table's owned data pages that still have room for a row. Once earlier pages fill, + /// Access leaves only the page it is currently appending to marked here. + public abstract int TdefFreePagesOffset { get; } /// Offset where the real-index block begins; column descriptors follow it. - public virtual int TdefRealIndexBlockOffset => 0x3F; + public abstract int TdefRealIndexBlockOffset { get; } + + /// Size of the header that prefixes each TDEF continuation page's payload (also the free-space + /// reserve the first page leaves for it). + public abstract int TdefContinuationHeaderSize { get; } + + // --- TDEF limits --- + + /// The most columns a table can have, and one past the highest column id. The count and id fields are + /// wider, so more could be written, but Access would refuse to open the table. + public abstract int MaxColumnsPerTable { get; } + + /// + /// The most indexes a table can have — and the cap applies to BOTH TDEF counts, the index-data blocks + /// () and the logical index-info blocks + /// (). Microsoft states it against the logical one: "Number of + /// indexes in a table: 32, including indexes created internally to maintain table relationships, + /// single-field and composite indexes." + /// + /// + /// The logical count is the one that binds, because a data block must be named by a logical block, so the + /// data count never exceeds it. A table many others reference gains a logical block per incoming + /// relationship and no data block, so it overruns on the logical count while the data count still looks + /// healthy. Building EF Core's ComplexNavigationsSharedType model, Level1 reached 46 logical + /// against 31 data, and the resulting file was unreadable by Access ("Unrecognized database format", the + /// table missing entirely). Measured in IndexCountLimitAccessTests; see + /// docs/format/page-02d-constraints.md. + /// + public abstract int MaxIndexesPerTable { get; } + + /// The longest name a TDEF stores — a column's or an index's — in bytes as stored. + public abstract int MaxNameBytes { get; } + + /// Size of the byte-length prefix on each TDEF name entry (column and index names alike); the text + /// follows it. + public abstract int TdefNameLengthSize { get; } + + // --- TDEF real-index block: one entry per real index, at TdefRealIndexBlockOffset --- /// Size in bytes of each real-index entry in the block before the column descriptors. - public virtual int RealIndexEntrySize => 12; + public abstract int RealIndexEntrySize { get; } + + /// Offset in a real-index entry of its 4-byte total entry count (the row count). + public abstract int RealIndexRowCountOffset { get; } + + /// Offset in a real-index entry of its 4-byte unique entry count — cumulative, never decremented by + /// Access. + public abstract int RealIndexUniqueCountOffset { get; } + + // --- TDEF column descriptor: one per column, after the real-index block (offsets within a descriptor) --- /// Size in bytes of one column descriptor. - public virtual int ColumnDescriptorSize => 25; - - // --- Column descriptor layout (offsets within a single descriptor) --- - public virtual int ColumnTypeOffset => 0x00; - public virtual int ColumnNumberOffset => 0x05; - public virtual int ColumnVariableIndexOffset => 0x07; // position among variable columns (0 for fixed) - // A second copy of the column id. Every creator writes it on a user table — ACE's SQL DDL, DAO's object - // model and DAO-executed SQL alike — while the engine's own bootstrap tables (MSysObjects and friends, - // and the f_ complex-column tables) leave it zero. It stops tracking 0x05 after an ALTER COLUMN - // type change, which burns a new id there and leaves this at the old one (§3.8). - public virtual int ColumnSecondaryNumberOffset => 0x09; - public virtual int ColumnPrecisionOffset => 0x0B; // Decimal/Numeric columns only - public virtual int ColumnScaleOffset => 0x0C; // Decimal/Numeric columns only - // 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 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; - public virtual int ColumnFixedOffsetOffset => 0x15; - public virtual int ColumnLengthOffset => 0x17; - - // Column flag byte (0x0F). Every documented bit is modelled (read into ColumnDef, written from it); the - // undocumented bits (0x08/0x10/0x20, zero in every file observed) are the only ones carried through raw. - /// Column flag: the column is fixed-length. - public const byte ColumnFlagFixedLength = 0x01; - /// Column flag: the column is updatable (set on essentially every column). - public const byte ColumnFlagUpdatable = 0x02; - /// Column flag: the column is an AutoNumber. - public const byte ColumnFlagAutoNumber = 0x04; - /// Column flag: an AutoNumber column that generates GUIDs (Replication ID) rather than Longs. - public const byte ColumnFlagGuidAutoNumber = 0x40; - /// Column flag: a hyperlink (a Memo column presented as a hyperlink). - public const byte ColumnFlagHyperlink = 0x80; - - // Extended flag byte (0x10). - /// Extended flag: the column can store compressed Unicode text (§7). - public const byte ColumnExtFlagCompressedUnicode = 0x01; - /// Extended flag: a calculated (computed) column (ACE 14+); the 0xC0 pair. - public const byte ColumnExtFlagCalculated = 0xC0; - // Note: nullability is NOT in the column flag byte (bit 0x02 is set on every column). A NOT NULL column - // is marked by a boolean `Required` property in the LvProp blob instead — see PropertyBlob / §11. - - // --- Data page layout (Jet 4 / ACE) --- + public abstract int ColumnDescriptorSize { get; } - /// Offset of the 2-byte free-space count on a data page. - public virtual int DataFreeSpaceOffset => 0x02; + public abstract int ColumnTypeOffset { get; } - /// Offset of the 4-byte owning-table TDEF page (or the "LVAL" marker on long-value pages). - public virtual int DataOwnerOffset => 0x04; + /// Offset of the 2-byte low half of in a column descriptor. + /// Access needs it; the reader ignores it. + public abstract int ColumnRecordMarkerOffset { get; } + + public abstract int ColumnNumberOffset { get; } + + /// Position among variable columns (0 for fixed). + public abstract int ColumnVariableIndexOffset { get; } + + /// A second copy of the column id. Every creator writes it on a user table — ACE's SQL DDL, DAO's + /// object model and DAO-executed SQL alike — while the engine's own bootstrap tables (MSysObjects and + /// friends, and the f_<GUID> complex-column tables) leave it zero. It stops tracking + /// after an ALTER COLUMN type change, which burns a new id there and leaves + /// this at the old one (§3.8). + public abstract int ColumnSecondaryNumberOffset { get; } + + /// Decimal/Numeric columns only. + public abstract int ColumnPrecisionOffset { get; } + + /// Decimal/Numeric columns only. + public abstract int ColumnScaleOffset { get; } /// - /// Offset of the 4-byte chain stamp on a data page. Zero on every page but the first of a - /// multi-page long-value chain, where it must equal the pointing descriptor's own stamp or ACE refuses to - /// materialise the value — see docs/format/long-values.md. Jet 3 has the row count at this offset - /// instead (which is why Jet 4's row count sits four bytes later), so a Jet 3 file has nowhere to put one; - /// Jet3Format does not override the data-page offsets today, and this is one of the things it will - /// have to when Jet 3 writing is built out. + /// Non-numeric columns use the precision/scale bytes and the two after them 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: the LANGID, little-endian (0x0409 = General/en-US), at this offset; then + /// ; then . /// - public virtual int DataChainStampOffset => 0x08; + public abstract int ColumnLocaleOffset { get; } - /// Offset of the 2-byte row count on a data page. - public virtual int DataRowCountOffset => 0x0C; + /// The 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). + public abstract int ColumnCollationSortIdOffset { get; } + + /// The sort-order version (0 = the legacy compacted table, 1 = the Access 2010 NLS order). + public abstract int ColumnCollationVersionOffset { get; } + + /// The column flag byte — . Nullability is NOT in it (updatable is set + /// on every column): a NOT NULL column is marked by a boolean Required property in the LvProp blob + /// instead — see PropertyBlob / §11. + public abstract int ColumnFlagsOffset { get; } + + /// The extended column flag byte — . + public abstract int ColumnExtendedFlagsOffset { get; } + + public abstract int ColumnFixedOffsetOffset { get; } + public abstract int ColumnLengthOffset { get; } + + // --- TDEF index-data block (§3.5): one per real index, after the column names --- + + /// Size of one index-data block. + public abstract int IndexDataBlockSize { get; } + + /// The 4-byte marker that opens an index-data block. + public const uint IndexDataMarker = 0x783; + + /// Offset of the block's column slots — a fixed array of , with no + /// count field, so an index spans at most that many columns. + public abstract int IndexDataColumnsOffset { get; } + + /// Number of column slots in an index-data block: the most columns an index can span. + public abstract int IndexDataMaxColumns { get; } + + /// Size of one column slot: the 2-byte column id, then its byte. + public abstract int IndexDataColumnSlotSize { get; } + + /// Offset in a column slot of its byte. + public abstract int IndexDataColumnOrderOffset { get; } + + /// The column id in an unused slot (0xFFFF). + public const short IndexDataColumnUnused = -1; + + /// Offset of the usage-map pointer for the index's own pages. + public abstract int IndexDataUsageMapOffset { get; } + + /// Offset of the 4-byte B-tree root page. + public abstract int IndexDataRootPageOffset { get; } + + /// Offset of the 2-byte . + public abstract int IndexDataFlagsOffset { get; } + + // --- TDEF index-info block (§3.6): one per logical index, linking a name to a data block --- + + /// Size of one index-info block. + public abstract int IndexInfoBlockSize { get; } + + /// Offset of the block's . + public abstract int IndexInfoMarkerOffset { get; } + + /// Offset of the 4-byte logical index number (index_num). + public abstract int IndexInfoNumberOffset { get; } + + /// Offset of the 4-byte number of the index-data block it uses (index_num2). + public abstract int IndexInfoDataNumberOffset { get; } + + /// Offset of the byte. + public abstract int IndexInfoFkTypeOffset { get; } + + /// Offset of the 4-byte index_num of the relationship's other end; + /// when there is none. + public abstract int IndexInfoFkNumberOffset { get; } + + /// The value of an index that is not a relationship's. + public const uint IndexInfoNoForeignKey = 0xFFFFFFFF; + + /// Offset of the 4-byte TDEF page of the relationship's other table; 0 when there is none. + public abstract int IndexInfoFkTablePageOffset { get; } - /// Offset of the row-offset slot directory (2 bytes per row). - public virtual int DataRowDirectoryOffset => 0x0E; + /// Offset of the update byte. + public abstract int IndexInfoUpdateActionOffset { get; } - /// Size of the column-count field at the start of a row record (2 bytes in Jet 4 / ACE, 1 in Jet 3). - public virtual int RowColumnCountSize => 2; + /// Offset of the delete byte. + public abstract int IndexInfoDeleteActionOffset { get; } + + /// Offset of the byte. + public abstract int IndexInfoTypeOffset { get; } + + // --- TDEF long-value map list: ends the definition, after the index names (§3.3.2) --- + + /// Size of one entry in the long-value column usage-map list that ends the definition, after the + /// index names: the 2-byte column id, then the owned- and free-pages map pointers (§3.3.2). + public abstract int TdefLongValueMapEntrySize { get; } + + /// Offset in a long-value map entry of its owned-pages map pointer. + public abstract int TdefLongValueMapOwnedOffset { get; } + + /// Offset in a long-value map entry of its free-pages map pointer. + public abstract int TdefLongValueMapFreeOffset { get; } + + /// The column id that ends the long-value map list — written even when the list is empty, and + /// counted in the definition length. + public const ushort TdefLongValueMapTerminator = 0xFFFF; + + // --- Long values (Memo / OLE / long binary): an in-row descriptor, and LVAL pages --- + + /// The owner field of a long-value page (): ASCII "LVAL". + public const uint LongValuePageMarker = 0x4C41564C; + + /// Bytes in an in-row long-value descriptor. Every consumer must have all of them before reading + /// any field — the descriptor arrives as a row's variable chunk, so its width is whatever the offset table + /// declared, not something the column guarantees. An inline value's payload follows it. + public abstract int LongValueDescriptorSize { get; } + + /// The bits of the descriptor's first 4 bytes holding the value's length; the bits above are its + /// . + public abstract int LongValueLengthMask { get; } + + /// Offset in the descriptor of the record pointer to a single-page value's row, or to a chained + /// value's first chunk. + public abstract int LongValueDescriptorPointerOffset { get; } /// - /// The largest record ACE will store — 4060 bytes, excluding anything that lives on LVAL pages, - /// where ACE raises "Record is too large." Measured across three table shapes whose row overhead differs - /// by 23 bytes (9, 12 and 20 text columns), and the total lands on 4060 every time, so it is a flat cap - /// rather than something derived from the row's layout (RecordSizeAccessTests). + /// Offset in the descriptor of the 4-byte chain stamp, which must equal the + /// stamp of the first page of the chain it points at. Non-zero only on the + /// chained form; ACE leaves it zero on inline and single-page values and checks it on neither. The value + /// itself is arbitrary — ACE writes GetTickCount() and so does LibRed — because what it proves is that + /// the descriptor and the chain came from the same write, not when. Patching either copy alone makes ACE + /// refuse to materialise the value. /// - /// - /// It is 20 bytes below what the page could hold — 4096 less the 14-byte header and a 2-byte slot leaves - /// 4080 — and that reserve is not explained. Enforcing it is not optional politeness: a record between - /// 4061 and 4080 fits the page and LibRed used to write it happily, but ACE then cannot read the - /// row, failing with an unrelated "another user are attempting to change the same data" error. - /// - public virtual int MaxRecordSize => 4060; + public abstract int LongValueDescriptorChainStampOffset { get; } - /// Page number of the system catalog table MSysObjects (its TDEF page). - public virtual int CatalogPage => 2; + /// The largest payload kept inline in the row instead of being written to a long-value page. + public abstract int LongValueMaxInline { get; } - /// Offset in a TDEF of the owned-pages usage-map pointer: 1 byte row, then a 3-byte page. - public virtual int TdefOwnedPagesOffset => 0x37; + /// The largest value kept on a single LVAL page; anything longer is chained. The decision is + /// made on the uncompressed length, as are the inline and chained ones; compression is applied to + /// whatever form results, and a chained value is never compressed. + public abstract int LongValueMaxSinglePage { get; } - /// Offset in a TDEF of the free-pages usage-map pointer (same 1-byte row + 3-byte page shape): - /// the subset of the table's owned data pages that still have room for a row. Once earlier pages fill, - /// Access leaves only the page it is currently appending to marked here. - public virtual int TdefFreePagesOffset => 0x3B; + /// The largest LVAL page row: a single-page value up to this fits one row, and a chained value's + /// chunk row is this size — a record pointer to the next chunk, then data. + public abstract int LongValueMaxRowSize { get; } - /// Page size in bytes (2048 for Jet 3, 4096 for Jet 4 and all ACE versions). - public int PageSize { get; protected set; } = 4096; + // --- Usage maps (§9): a record on a usage-map data page, inline or reference (UsageMapType) --- - /// The logical version this format describes. - public abstract JetVersion Version { get; } + /// Offset in an inline record of its 4-byte start page — the page its bitmap's bit 0 stands for. + public abstract int UsageMapStartPageOffset { get; } - /// True for the ACCDB (ACE 12+) family, which uses different encryption and layout details. - public virtual bool IsAccdb => false; + /// Size of an inline record's header (the type byte and the start page); the bitmap follows it. + public abstract int UsageMapInlineHeaderSize { get; } + + /// Bitmap size of a full-width inline record: what a fresh map is written with, and the fixed + /// window a free-pages map slides. + public abstract int UsageMapInlineBitmapSize { get; } + + /// Size of a full-width inline record: its header and . + public abstract int UsageMapInlineRecordSize { get; } + + /// The step an inline bitmap grows in. + public abstract int UsageMapInlineGrowthSize { get; } + + /// Bytes the global maps' holder page keeps free: an inline global map that would leave less + /// converts to reference form instead. + public abstract int UsageMapHolderReserve { get; } + + /// Offset in a reference record of its 4-byte bitmap-page pointers. + public abstract int UsageMapReferencePointersOffset { get; } + + /// Number of bitmap-page pointers in a reference record. + public abstract int UsageMapReferenceSlots { get; } + + /// Size of a reference record: the type byte and pointers. + public abstract int UsageMapReferenceRecordSize { get; } + + /// Bytes preceding the bitmap on a dedicated usage-bitmap page (type 0x0105). + public abstract int UsageMapBitmapPageHeaderSize { get; } + + /// Number of pages one dedicated bitmap page covers; reference pointer k covers the range + /// starting at k times this. + public abstract int UsageMapPagesPerBitmapPage { get; } + + // --- Index page layout: a node (0x0103) or a leaf (0x0104), §10 --- + + /// Offset of the 2-byte free-space count on an index page. + public abstract int IndexFreeSpaceOffset { get; } + + /// Offset of the 4-byte owning-table TDEF page on an index page. + public abstract int IndexOwnerOffset { get; } + + /// Offset of a leaf's 4-byte link to the previous (lower-key) leaf. + public abstract int IndexPrevPageOffset { get; } + + /// Offset of a leaf's 4-byte link to the next (higher-key) leaf — Access walks this for COUNT/scan. + public abstract int IndexNextPageOffset { get; } + + /// Offset of a node's 4-byte child-tail page. + public abstract int IndexChildTailOffset { get; } + + /// Offset of the 2-byte count of leading bytes every later entry shares with the first. + public abstract int IndexCompressedByteCountOffset { get; } + + /// Offset of the level byte: 0 on a leaf, its height above the leaves on a node. + public abstract int IndexLevelOffset { get; } + + /// Offset of the entry-position bitmask, whose set bits give the end of each entry within the + /// entry-data region; it runs up to . + public abstract int IndexEntryMaskOffset { get; } + + /// Offset of the entry-data region. + public abstract int IndexEntryDataOffset { get; } + + /// Size of the big-endian trailer ending every index entry: a leaf entry's row (page, then row) or a + /// node entry's child page. + public abstract int IndexEntryTrailerSize { get; } /// /// Sniffs the format version byte from page 0 of and @@ -343,7 +647,7 @@ public static JetFormatBase Detect(Stream stream) throw new NotSupportedException( $"Not a Jet/ACE database: expected \"{JetIdentifier}\", \"{AceIdentifier}\" or \"{JetSystemIdentifier}\" at offset 0x{FormatIdentifierOffset:X2}, found \"{identifier}\"."); - byte version = header[VersionOffset]; + byte version = ReadVersionByte(header); bool identifierMatchesVersion = identifier == AceIdentifier ? version >= 0x02 : version <= 0x01; @@ -377,6 +681,17 @@ public static string ReadFormatIdentifier(ReadOnlySpan header) => // (a workgroup file) is padded with spaces to the field width. System.Text.Encoding.ASCII.GetString(header.Slice(FormatIdentifierOffset, FormatIdentifierLength)).TrimEnd('\0', ' '); + /// Reads the version byte at from a page-0 header. + public static byte ReadVersionByte(ReadOnlySpan header) => header[VersionOffset]; + + /// Writes page 0's version byte and the minor byte beside it — the inverse of + /// , for a created file and for a version raise alike. + internal void WriteVersion(Span page0, byte version, byte minor) + { + page0[VersionOffset] = version; + page0[MinorVersionOffset] = minor; + } + /// Maps the raw version byte at to a format instance. public static JetFormatBase FromVersionByte(byte versionByte) => versionByte switch { diff --git a/src/LibRed/LibRed.Core/Formats/LongValueFormat.cs b/src/LibRed/LibRed.Core/Formats/LongValueFormat.cs deleted file mode 100644 index 65588a0c..00000000 --- a/src/LibRed/LibRed.Core/Formats/LongValueFormat.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace LibRed.Formats; - -/// -/// Constants of the long-value (Memo / OLE / long binary) storage form: the flags in a 12-byte in-row -/// long-value descriptor, and the "LVAL" page owner marker. Shared by the read side -/// (LongValueReader, DataPage) and the write side (LongValueWriter) so they can't drift. -/// -internal static class LongValueFormat -{ - // ACE accepted and fully read back 0x3FFFFFFF binary bytes, then rejected 0x40000000. - // Length carries into byte 3; its 0x40/0x80 storage flags must remain separate. - public const int LengthMask = 0x3FFFFFFF; - public const byte FlagMask = 0xC0; - - internal static void ValidateLength(int length) - { - if ((uint)length > LengthMask) - throw new ArgumentOutOfRangeException(nameof(length), "The long-value length would overwrite its storage flags."); - } - - /// - /// The largest value ACE keeps on a single LVAL page; anything longer is chained. Measured - /// (LongTextStorageAccessTests): 3816 bytes stays single-page, 3818 chains, for a plain and a - /// WITH COMPRESSION column alike. Distinct from the 4076-byte chunk row a chained value uses — - /// conflating the two made LibRed keep 3817–4076 byte values on one page where ACE chains them. What - /// fixes the boundary at 3816 rather than the 4076 a row can hold is not established. - /// - /// The decision is made on the uncompressed length, as are the inline and chained ones; - /// compression is applied to whatever form results, and a chained value is never compressed. - public const int MaxSinglePageValue = 3816; - - /// The largest payload kept inline in the row (flag ) instead of being - /// written to a long-value page. - public const int MaxInlineValue = 64; - - /// Bytes in an in-row long-value descriptor. Every consumer must have all of them before - /// reading any field — the descriptor arrives as a row's variable chunk, so its width is whatever the - /// offset table declared, not something the column guarantees. - public const int DescriptorSize = 12; - - /// - /// Offset within the descriptor of the 4-byte chain stamp, which must equal the header stamp - /// () of the first page of the chain it points at. - /// Non-zero only on the chained form; ACE leaves it zero on inline and single-page values and checks it - /// on neither. The value itself is arbitrary — ACE writes GetTickCount() and so does LibRed — - /// because what it proves is that the descriptor and the chain came from the same write, not when. - /// Patching either copy alone makes ACE refuse to materialise the value. - /// - public const int ChainStampOffset = 8; - - /// Descriptor flag: the payload follows the descriptor inline (no LVAL page). - public const byte FlagInline = 0x80; - - /// Descriptor flag: the payload is a single LVAL page row. - public const byte FlagSinglePage = 0x40; - - /// Descriptor flag value for a payload chained across multiple LVAL pages. - public const byte FlagChained = 0x00; - - /// The 4-byte owner marker on a long-value page: ASCII "LVAL" (little-endian 4C 56 41 4C). - public const uint LvalMarker = 0x4C41564C; -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/ObjectAttributes.cs b/src/LibRed/LibRed.Core/Formats/ObjectAttributes.cs new file mode 100644 index 00000000..1b185c31 --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/ObjectAttributes.cs @@ -0,0 +1,34 @@ +namespace LibRed.Formats; + +/// +/// The MSysObjects.Flags bits that decide how an object is classified — the way Access classifies it, not +/// by name (measured on ACE 16; docs/format/system-catalog.md). +/// +[Flags] +public enum ObjectAttributes : uint +{ + None = 0, + + /// The system attribute, beside on the engine's own objects. + SystemAttribute = 0x00000002, + + /// A hidden object — Access's application tables, the nav-pane group, MSysResources, and + /// EFCore.Jet's #Dual helper. Without it is an ACCESS TABLE in the schema + /// rowsets, and Access keeps it out of its user-table list. + Hidden = 0x00000008, + + /// A complex column's flat storage table (MSysComplexType_*): not listed in the schema + /// rowsets at all. + ComplexStorage = 0x00030000, + + /// A table that owns a complex column — set on exactly those tables, and on no table without one + /// (measured across Access-written files; system-catalog §11). + OwnsComplexColumns = 0x00040000, + + /// Set on every stored query, whose kind fills the low byte (), and on DAO's + /// SingleRecord pseudo-object. + QueryDef = 0x10000000, + + /// A system object: a SYSTEM TABLE in the schema rowsets. + System = 0x80000000, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/ObjectType.cs b/src/LibRed/LibRed.Core/Formats/ObjectType.cs new file mode 100644 index 00000000..d906a416 --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/ObjectType.cs @@ -0,0 +1,49 @@ +namespace LibRed.Formats; + +/// +/// MSysObjects.Type: what kind of object a catalog row is, and so which container it sits in +/// (docs/format/system-catalog.md, Object kinds). +/// +public enum ObjectType : short +{ + /// A form, in the Forms container. + Form = -32768, + + /// A macro, in the Scripts container. + Macro = -32766, + + /// A report, in the Reports container. + Report = -32764, + + /// A module, in the Modules container. + Module = -32761, + + /// A row per user (Admin), in the SysRel container. + User = -32758, + + /// A database document — SummaryInfo, UserDefined, AccessLayout — in the + /// Databases container. + DatabaseDocument = -32757, + + /// A local table, in the Tables container. Its Id is its TDEF page. + Table = 1, + + /// The database object, MSysDb, in the Databases container. + Database = 2, + + /// A container — Tables, Databases, Relationships and the rest — under the root, + /// which has no row of its own. + Container = 3, + + /// A stored query, in the Tables container — so it cannot share a table's name. + Query = 5, + + /// A linked table, in the Tables container. + LinkedTable = 6, + + /// A relationship, in the Relationships container. + Relationship = 8, + + /// DAO's SingleRecord pseudo-object, in the Relationships container. + SingleRecord = 9, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/QueryAttribute.cs b/src/LibRed/LibRed.Core/Formats/QueryAttribute.cs new file mode 100644 index 00000000..d78886b6 --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/QueryAttribute.cs @@ -0,0 +1,49 @@ +using LibRed.Catalog; + +namespace LibRed.Formats; + +/// An MSysQueries row's Attribute: which part of the stored query the row holds (spec §11). +internal enum QueryAttribute : byte +{ + /// The start record (Flag = for a SELECT). + Type = 0x00, + + /// The query's kind: Flag is a . + Operation = 0x01, + + /// A declared parameter: Name1 its name, Flag its Jet type code. + Parameter = 0x02, + + /// The in Flag, cumulative across rows. + Option = 0x03, + + /// The connection string to an external table (pass-through). + Connect = 0x04, + + /// A named or derived table, a UNION segment, or a subquery. + Table = 0x05, + + /// An output column: Expression is its text. + Column = 0x06, + + /// A join: Expression the condition, Flag a . + Join = 0x07, + + /// The predicate, in Expression. + Where = 0x08, + + /// A GROUP BY column, in Expression. + GroupBy = 0x09, + + /// An aggregate query's HAVING predicate, in Expression. + Having = 0x0A, + + /// A sort column, in Expression. + OrderBy = 0x0B, + + /// Complex data types (multi-value / attachment / version history). + Complex = 0x0C, + + /// The terminating row. + End = 0xFF, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/QueryDefType.cs b/src/LibRed/LibRed.Core/Formats/QueryDefType.cs new file mode 100644 index 00000000..108df500 --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/QueryDefType.cs @@ -0,0 +1,18 @@ +namespace LibRed.Formats; + +/// +/// DAO's QueryDef.Type, which a stored query's MSysObjects.Flags carries beside +/// (verified vs ACE for crosstab, delete, update, append, make-table and +/// data-definition) — not the the MSysQueries action row carries, which numbers the +/// kinds differently. +/// +internal enum QueryDefType +{ + Select = 0, + Crosstab = 16, + Delete = 32, + Update = 48, + Append = 64, + MakeTable = 80, + DataDefinition = 96, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/QueryOperation.cs b/src/LibRed/LibRed.Core/Formats/QueryOperation.cs new file mode 100644 index 00000000..65c895ff --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/QueryOperation.cs @@ -0,0 +1,38 @@ +namespace LibRed.Formats; + +/// +/// The row's Flag — the query KIND. SELECT is one of them: the row's +/// presence does not make a query an action query, and Access writes it on plain SELECTs too (221 of them across +/// the sample corpus). Measured against DAO's QueryDef.Type on every stored query in the corpus; +/// and have no sample there and come from the published MSysQueries +/// tables. +/// +internal enum QueryOperation : short +{ + /// DAO dbQSelect. + Select = 1, + + /// DAO dbQMakeTable: SELECT … INTO, the target in Name1. + MakeTable = 2, + + /// DAO dbQAppend: the target table in Name1. + Append = 3, + + /// DAO dbQUpdate. + Update = 4, + + /// DAO dbQDelete. + Delete = 5, + + /// DAO dbQCrosstab: TRANSFORM. + Crosstab = 6, + + /// DAO dbQDDL: the whole SQL in Expression. + Ddl = 7, + + /// DAO dbQSQLPassThrough. + PassThrough = 8, + + /// DAO dbQSetOperation: a UNION. + Union = 9, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/QueryOptions.cs b/src/LibRed/LibRed.Core/Formats/QueryOptions.cs new file mode 100644 index 00000000..447883c7 --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/QueryOptions.cs @@ -0,0 +1,28 @@ +namespace LibRed.Formats; + +/// +/// The row's Flag bits. Cumulative, so test them as bits: 18 = +/// DISTINCT TOP, 24 = DISTINCTROW TOP, 48 = TOP PERCENT, 56 = DISTINCTROW TOP PERCENT. Flag 9 +/// (OutputAllFields | DistinctRow) is what Access writes for the auto-generated form/report record-source queries. +/// +[Flags] +internal enum QueryOptions : short +{ + None = 0, + + /// Also UNION ALL on a UNION query. + OutputAllFields = 0x01, + + Distinct = 0x02, + + /// WITH OWNERACCESS OPTION. + OwnerAccess = 0x04, + + DistinctRow = 0x08, + + /// TOP n: the count is in the row's Name1. + Top = 0x10, + + /// Only ever with : TOP n PERCENT. + Percent = 0x20, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/RelationshipAction.cs b/src/LibRed/LibRed.Core/Formats/RelationshipAction.cs new file mode 100644 index 00000000..62b42fa5 --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/RelationshipAction.cs @@ -0,0 +1,19 @@ +namespace LibRed.Formats; + +/// +/// An index-info block's update and delete action bytes (, +/// ), verified against ACE-created relationships. These, +/// not MSysRelationships.grbit, are what ACE acts on: with the two made to disagree, it cascades by the +/// block. Written on both ends of a relationship. +/// +public enum RelationshipAction : byte +{ + NoCascade = 0x00, + Cascade = 0x01, + + /// Delete only: ON DELETE SET NULL. + SetNull = 0x02, + + /// Both actions of a plain index, which is not a relationship's. + NotRelationship = 0x04, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/RelationshipFlags.cs b/src/LibRed/LibRed.Core/Formats/RelationshipFlags.cs index 57bdb196..834b0dc1 100644 --- a/src/LibRed/LibRed.Core/Formats/RelationshipFlags.cs +++ b/src/LibRed/LibRed.Core/Formats/RelationshipFlags.cs @@ -2,24 +2,27 @@ namespace LibRed.Formats; /// /// The MSysRelationships.grbit flag values, shared by the read side (JetCatalog) and the -/// write side (TableCreator) so the two can't drift. Values verified against Access. +/// write side (SchemaEditor) so the two can't drift. Values verified against Access. /// -internal static class RelationshipFlags +[Flags] +internal enum RelationshipFlags { + None = 0, + /// A one-to-one relationship (DAO's dbRelationUnique). Set by whoever creates the relationship /// — Access's dialog when both sides are unique — and never by ACE's SQL; on an enforced relationship it makes /// the child's backing index unique. - public const int OneToOne = 0x00000001; + OneToOne = 0x00000001, /// Referential integrity is NOT enforced. - public const int DontEnforce = 0x00000002; + DontEnforce = 0x00000002, /// Updates to the parent key cascade to the child (ON UPDATE CASCADE). - public const int UpdateCascade = 0x00000100; + UpdateCascade = 0x00000100, /// Deletes of the parent row cascade to the child (ON DELETE CASCADE). - public const int DeleteCascade = 0x00001000; + DeleteCascade = 0x00001000, /// Deleting the parent sets the child's FK columns to NULL (Jet ON DELETE SET NULL). - public const int DeleteSetNull = 0x00002000; + DeleteSetNull = 0x00002000, } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/RowPointer.cs b/src/LibRed/LibRed.Core/Formats/RowPointer.cs deleted file mode 100644 index a8bc3bbf..00000000 --- a/src/LibRed/LibRed.Core/Formats/RowPointer.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace LibRed.Formats; - -/// -/// Encoding of a 16-bit row-pointer directory entry on a data page: the low 13 bits are the row's byte -/// offset within the page, the top bits are status flags. Shared by the read side (DataPage) and the -/// write side (RowInserter) so the mask and flag bits can't drift. Fixed across the Jet 4 / ACE family. -/// -internal static class RowPointer -{ - /// Low 13 bits: the row's byte offset within the page. - public const int OffsetMask = 0x1FFF; - - /// Top bit: the row is deleted (a tombstone). - public const int DeletedFlag = 0x8000; - - /// Second-from-top bit: the pointer targets an overflow (long-value / relocated) row. - public const int OverflowFlag = 0x4000; - - /// - /// Rows a data page may hold. Not a space limit — an index entry addresses a row as - /// page << 8 | row (page-03-04-index-btree.md §10.2), so the slot number has to fit one byte - /// and slots 0..255 are all that can ever be named. ACE itself writes at most 255, one below that, - /// and LibRed matches ACE rather than the pointer's maximum so its pages are shapes Access also produces. - /// - /// - /// - /// Only narrow rows reach it: a 4 KB page fits this many once they are under about 14 bytes each, which in - /// practice means an all-fixed-column table of a few small columns. ACE's own cap is not about space — - /// a page it had filled to 255 still held 2,297 of its 4,096 bytes free — and on reaching it ACE drops the - /// page from the table's free-pages map exactly as if it were full. - /// - /// - /// Exceeding it loses rows silently, and to more than just indexed reads. Every row past slot 255 is - /// unaddressable by any index, its entry aliasing a different row on another page. Worse, ACE - /// cannot see those rows at all: it reads the full 16-bit row count but then caps at 256 slots per - /// page and ignores the remainder. Measured — LibRed wrote 900 rows as 400/400/100 and read all 900 back, - /// while ACE counted 612, exactly 256 + 256 + 100. A LibRed-only scan walks slots directly and sees them, - /// which is what made this look harmless the first time. Hence the placer refuses the page rather than - /// trusting free space alone. - /// - /// - public const int MaxRowsPerPage = 255; -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/RowSlotFlags.cs b/src/LibRed/LibRed.Core/Formats/RowSlotFlags.cs new file mode 100644 index 00000000..4c8eaf4f --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/RowSlotFlags.cs @@ -0,0 +1,17 @@ +namespace LibRed.Formats; + +/// The status bits of a data page's row slot directory entry, above its +/// offset. +[Flags] +internal enum RowSlotFlags +{ + None = 0, + + /// The slot holds a forward pointer, not an inline row: a live one begins with the record pointer to + /// where its row was relocated (the new home is Deleted with this flag clear). With , a + /// zero-length tombstone. + Overflow = 0x4000, + + /// The row is deleted (a tombstone). + Deleted = 0x8000, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs b/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs deleted file mode 100644 index 00dca489..00000000 --- a/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs +++ /dev/null @@ -1,84 +0,0 @@ -using LibRed.Catalog; - -namespace LibRed.Formats; - -/// -/// The MSysQueries row vocabulary that encodes a stored query (view / action query): the per-row -/// attribute codes, the operation (query-type) codes, the option flag bits, and the -/// MSysObjects.Type value for a query object. Shared by the read side (JetCatalog) and the write -/// side (ViewCreator) so the two can't drift. See spec §11. -/// -internal static class StoredQueryFormat -{ - /// MSysObjects.Type value for a stored query / view object. - public const short ObjectTypeQuery = 5; - - // MSysQueries per-row attribute codes. - public const byte AttrType = 0x00; // start record (Flag = 1 for SELECT) - public const byte AttrOperation = 0x01; // query type: Flag = one of the Operation* values below - public const byte AttrParameter = 0x02; // Name1 = param name, Flag = Jet type code - public const byte AttrOption = 0x03; // Flag = the Flag* option bits below (cumulative) - public const byte AttrConnect = 0x04; // connection string to an external table (pass-through) - public const byte AttrTable = 0x05; // named/derived table, UNION segment, or subquery - public const byte AttrColumn = 0x06; // Expression = column text - public const byte AttrJoin = 0x07; // Expression = condition, Flag = kind - public const byte AttrWhere = 0x08; // Expression = predicate text - public const byte AttrGroupBy = 0x09; // Expression = a GROUP BY column - public const byte AttrHaving = 0x0A; // Expression = an aggregate query's HAVING predicate - public const byte AttrOrderBy = 0x0B; // Expression = a sort column - public const byte AttrComplex = 0x0C; // complex data types (MVF / attachment / version history) - public const byte AttrEnd = 0xFF; // terminating row - - /// AttrType Flag value for a SELECT query. - public const short QueryTypeSelect = 1; - - // AttrOperation Flag values -- the query KIND. Note SELECT is one of them: the row's presence does not - // make a query an action query, and Access writes it on plain SELECTs too (221 of them across the sample - // corpus). Measured against DAO's QueryDef.Type on every stored query in the corpus; 7 and 8 have no - // sample there and come from the published MSysQueries tables. - public const short OperationSelect = 1; // DAO dbQSelect (0) - public const short ActionMakeTable = 2; // DAO dbQMakeTable (80) -- SELECT … INTO; target in Name1 - public const short ActionAppend = 3; // DAO dbQAppend (64) -- target table in Name1 - public const short ActionUpdate = 4; // DAO dbQUpdate (48) - public const short ActionDelete = 5; // DAO dbQDelete (32) - public const short ActionCrosstab = 6; // DAO dbQCrosstab (16) -- TRANSFORM - public const short ActionDdl = 7; // DAO dbQDDL (96) -- whole SQL in Expression - public const short ActionPassThrough = 8; // DAO dbQSQLPassThrough (112) - public const short ActionUnion = 9; // DAO dbQSetOperation (128) - - // AttrOption Flag bits. Cumulative, so test them as bits: 18 = DISTINCT TOP, 24 = DISTINCTROW TOP, - // 48 = TOP PERCENT, 56 = DISTINCTROW TOP PERCENT. Flag 9 (OutputAllFields|DistinctRow) is what Access - // writes for the auto-generated form/report record-source queries. - public const short FlagOutputAllFields = 1; // also UNION ALL on a UNION query - public const short FlagDistinct = 2; - public const short FlagOwnerAccess = 4; // WITH OWNERACCESS OPTION - public const short FlagDistinctRow = 8; - public const short FlagTop = 0x10; // the count is in Name1 - public const short FlagPercent = 0x20; // only ever with FlagTop: TOP n PERCENT - - /// AttrColumn Flag bit marking an appended literal value. - public const short AppendValueFlag = unchecked((short)0x8000); - - /// - /// The LvExtra value for a declared parameter's facets, or null for a type that carries none. - /// Measured against ACE: a Text(50) parameter stores the length, 50; a Decimal(18,4) packs - /// both into one value, (scale << 16) | precision = 262162; and a sized Binary(10) - /// stores nothing at all, as every type without a declared size does. Access renders the query's - /// PARAMETERS clause from this, so a parameter with no value here reads back as Text(255). - /// - public static int? PackParameterFacets(JetDataType type, int? size, int? scale) => type switch - { - JetDataType.Text => size, - JetDataType.FixedPoint when size is { } precision => (scale ?? 0) << 16 | precision & 0xFFFF, - _ => null, - }; - - /// A declared parameter's facets back out of its LvExtra — the inverse of - /// . A text parameter reports its length as the size; a decimal reports - /// the precision and scale packed into the one value. - public static (int? Size, int? Precision, int? Scale) UnpackParameterFacets(JetDataType type, int? lvExtra) => - lvExtra is not { } packed ? (null, null, null) - : type == JetDataType.FixedPoint ? (null, packed & 0xFFFF, packed >> 16) - : type == JetDataType.Text ? (packed, null, null) - : (null, null, null); -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Formats/UsageMapType.cs b/src/LibRed/LibRed.Core/Formats/UsageMapType.cs new file mode 100644 index 00000000..a2b33256 --- /dev/null +++ b/src/LibRed/LibRed.Core/Formats/UsageMapType.cs @@ -0,0 +1,11 @@ +namespace LibRed.Formats; + +/// A usage-map record's first byte: which of the two forms it takes (§9). +internal enum UsageMapType : byte +{ + /// A start page, then a bitmap whose bit i stands for page start + i. + Inline = 0x00, + + /// Pointers to dedicated bitmap pages (type 0x0105), each covering a fixed range of pages. + Reference = 0x01, +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/IO/PageBuffer.cs b/src/LibRed/LibRed.Core/IO/PageBuffer.cs index 38eac8c2..b5ba8405 100644 --- a/src/LibRed/LibRed.Core/IO/PageBuffer.cs +++ b/src/LibRed/LibRed.Core/IO/PageBuffer.cs @@ -1,5 +1,6 @@ using System.Buffers.Binary; using System.Text; +using LibRed.Storage; namespace LibRed.IO; @@ -29,8 +30,25 @@ public readonly struct PageBuffer(ReadOnlyMemory data, int pageNumber) public long ReadInt64(int offset) => BinaryPrimitives.ReadInt64LittleEndian(Span.Slice(offset, 8)); - /// Reads a 3-byte little-endian page pointer (used by index/usage structures). - public int ReadInt24(int offset) => Span[offset] | (Span[offset + 1] << 8) | (Span[offset + 2] << 16); + /// Reads a record pointer: a 1-byte row, then the 3-byte little-endian page holding it. Every usage + /// map is addressed this way — the TDEF's own, each index's and each long-value column's — and so is a long + /// value's first chunk and each chunk's next. + public (int Row, int Page) ReadRecordPointer(int offset) => ReadRecordPointer(Span, offset); + + /// over bytes that are not a whole page: , + /// stored little-endian. + public static (int Row, int Page) ReadRecordPointer(ReadOnlySpan buffer, int offset) + { + RowId id = RowId.FromPacked(BinaryPrimitives.ReadInt32LittleEndian(buffer.Slice(offset, RecordPointerSize))); + return (id.Row, id.Page); + } + + /// Size of a record pointer: the row byte and the 3-byte page. + public const int RecordPointerSize = 4; + + /// Writes a record pointer — the inverse of . + public static void WriteRecordPointer(Span buffer, int offset, int row, int page) => + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(offset, RecordPointerSize), new RowId(page, row).Packed); public ReadOnlySpan Slice(int offset, int length) => Span.Slice(offset, length); diff --git a/src/LibRed/LibRed.Core/IO/PageChannel.cs b/src/LibRed/LibRed.Core/IO/PageChannel.cs index b2f2e68e..7f4c9eba 100644 --- a/src/LibRed/LibRed.Core/IO/PageChannel.cs +++ b/src/LibRed/LibRed.Core/IO/PageChannel.cs @@ -1,6 +1,7 @@ using LibRed.Crypto; using LibRed.Formats; -using System.Buffers.Binary; +using LibRed.Pages; +using LibRed.Storage; namespace LibRed.IO; @@ -87,8 +88,12 @@ private PageChannel(FileStream stream, JetFormatBase format, bool readOnly, stri _locks = locks ?? MonitorLockManager.AcquireKey(_identity); _cache = PageCache.AcquireKey(_identity); _cache.InitFileLength(stream.Length); + Allocator = new PageAllocator(this); } + /// The allocator owning this channel's page allocation and release paths. + internal PageAllocator Allocator { get; } + /// Whether a transaction is currently open on this channel. public bool InTransaction => _active is not null; @@ -200,7 +205,7 @@ public static PageChannel Open(string path, bool readOnly = true, string? passwo var page0 = new byte[format.PageSize]; stream.Seek(0, SeekOrigin.Begin); stream.ReadExactly(page0); - int databaseKey = DecodeDatabaseKey(page0); + int databaseKey = DatabaseDefinitionPage.ReadDatabaseKey(page0, format); // A nonzero database key means the pages are encrypted. ACE (.accdb) uses Office Agile encryption // (with a password); the pre-ACE Jet 3/4 formats (.mdb and the .mdw workgroup file) use the legacy // RC4 scheme keyed by the database key alone (no password). @@ -208,8 +213,8 @@ public static PageChannel Open(string path, bool readOnly = true, string? passwo // descriptor) — detect by descriptor, trying Agile first then Standard. The pre-ACE Jet 3/4 formats // (.mdb and the .mdw workgroup file) use the legacy RC4 scheme keyed by the database key alone. IPageCodec? codec = format.IsAccdb - ? (IPageCodec?)AgileEncryption.TryCreate(page0, databaseKey, password) - ?? OfficeStandardEncryption.TryCreate(page0, databaseKey, password) + ? (IPageCodec?)AgileEncryption.TryCreate(page0, databaseKey, password, format) + ?? OfficeStandardEncryption.TryCreate(page0, databaseKey, password, format) : JetLegacyEncryption.TryCreate(databaseKey); // A nonzero database key means the file is encrypted; if no codec recognised the descriptor the scheme @@ -233,17 +238,6 @@ public static PageChannel Open(string path, bool readOnly = true, string? passwo /// The database file this channel reads and writes. internal string Path => _path; - /// Decodes the 4-byte database (encryption) key at page-0 0x3E through the fixed header mask. - private static int DecodeDatabaseKey(ReadOnlySpan page0) - { - ReadOnlySpan mask = JetFormatBase.PageZeroHeaderMask; - int start = JetFormatBase.PageZeroHeaderMaskStart; - Span key = stackalloc byte[4]; - for (int i = 0; i < 4; i++) - key[i] = (byte)(page0[JetFormatBase.DatabaseKeyOffset + i] ^ mask[JetFormatBase.DatabaseKeyOffset - start + i]); - return BinaryPrimitives.ReadInt32LittleEndian(key); - } - /// Rejects a page number that cannot exist in this file. Page numbers come out of the file /// itself, so a bad one is corruption — without this the read seeks past the end and throws /// EndOfStreamException, which callers handling a damaged file do not catch. Called only on the @@ -271,7 +265,7 @@ public PageBuffer ReadPage(int pageNumber) /// the page is read from disk and cached, then returned. The returned bytes are live cache state: valid /// only until the next write to or eviction of this page. /// - public PageBuffer ReadPageShared(int pageNumber) + internal PageBuffer ReadPageShared(int pageNumber) { // Read-your-own-writes: a page this transaction has written lives only in the overlay until commit. if (_active is not null && _overlay.TryGetValue(pageNumber, out byte[]? buffered)) @@ -344,7 +338,7 @@ public void ReadPage(int pageNumber, Span destination) /// physical end (the map pre-accounts for growth, and allocation defers the physical write), and /// writing the page is what materialises it — the same growth Access performs on such a write. /// - public void WritePage(int pageNumber, ReadOnlySpan source) => WritePage(pageNumber, source, parsed: null); + internal void WritePage(int pageNumber, ReadOnlySpan source) => WritePage(pageNumber, source, parsed: null); /// /// Writes a page as does, and keeps @@ -352,7 +346,7 @@ public void ReadPage(int pageNumber, Span destination) /// written. The caller vouches that it is exactly what parsing gives. Attached in the /// same publication as the write, so no other channel's write of the page can come between the two. /// - public void WritePage(int pageNumber, ReadOnlySpan source, object? parsed) + internal void WritePage(int pageNumber, ReadOnlySpan source, object? parsed) { if (_readOnly) throw new InvalidOperationException("This channel was opened read-only."); @@ -441,21 +435,6 @@ private void WriteThroughUnderPublishLock(int pageNumber, byte[] source) finally { _locks?.ExitExclusive(pageNumber); } } - /// - /// Allocates a fresh page by growing the file by one page, returning its number. Jet also - /// recycles freed pages via usage maps; appending at the end is always valid since the page - /// count is simply the file length divided by the page size. - /// - public int AllocatePage() - { - if (_readOnly) - throw new InvalidOperationException("This channel was opened read-only."); - - int pageNumber = PageCount; - WritePage(pageNumber, new byte[PageSize]); - return pageNumber; - } - /// /// Begins a page-level transaction. Subsequent calls snapshot the /// original bytes of each page they touch, so can undo them. @@ -610,19 +589,18 @@ private void ClearTransactionState() internal bool RaiseFormatVersion(byte version) { byte[] page0 = ReadPage(0).Span.ToArray(); - if (page0[JetFormatBase.VersionOffset] >= version) return false; + if (JetFormatBase.ReadVersionByte(page0) >= version) return false; // A Jet MDB carries the "Standard Jet DB" identifier, which JetFormatBase.Detect pairs with version // 0x00/0x01 only. Raising one to an ACE version writes a file nothing can reopen — not LibRed, not - // Access. DatabaseCreator refuses to create that same pair; refuse to upgrade into it too. + // Access. JetDatabase refuses to create that same pair; refuse to upgrade into it too. if (!Format.IsAccdb) throw new NotSupportedException( $"Cannot raise this database to version 0x{version:X2}: it is a Jet MDB " + $"(\"{JetFormatBase.JetIdentifier}\"), and only an ACCDB can take that version. " + "The statement needs a data type this format cannot store."); - page0[JetFormatBase.VersionOffset] = version; - page0[JetFormatBase.MinorVersionOffset] = 0x00; + Format.WriteVersion(page0, version, minor: 0x00); WritePage(0, page0); Format = JetFormatBase.FromVersionByte(version); return true; @@ -635,7 +613,7 @@ internal bool RaiseFormatVersion(byte version) /// page 0 is cached by then. internal void ResyncFormatVersion() { - byte onDisk = ReadPage(0).Span[JetFormatBase.VersionOffset]; + byte onDisk = JetFormatBase.ReadVersionByte(ReadPage(0).Span); if ((byte)Format.Version == onDisk) return; try { @@ -739,7 +717,7 @@ private static bool SamePage(byte[]? left, byte[]? 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. - public bool TryGetParsedPage(int pageNumber, out object? parsed) + internal bool TryGetParsedPage(int pageNumber, out object? parsed) { // A page buffered in this transaction's overlay has uncommitted bytes; the shared parsed cache reflects // the committed image, so it is served from the transaction's own parses instead. @@ -751,7 +729,7 @@ public bool TryGetParsedPage(int pageNumber, out object? parsed) /// Caches a higher-layer parse of a (resident) page so repeated reads — e.g. a B-tree descent that /// re-visits the same root/internal pages — can skip re-decoding it. The caller must not mutate the object /// afterwards, as it is shared with other readers of the same file. - public void SetParsedPage(int pageNumber, object parsed) + internal void SetParsedPage(int pageNumber, object parsed) { // A transaction-local parse goes with the transaction's bytes, never onto the shared (committed) entry. if (_active is not null && _overlay.ContainsKey(pageNumber)) diff --git a/src/LibRed/LibRed.Core/JetDatabase.cs b/src/LibRed/LibRed.Core/JetDatabase.cs index 5bf98a10..88b33d25 100644 --- a/src/LibRed/LibRed.Core/JetDatabase.cs +++ b/src/LibRed/LibRed.Core/JetDatabase.cs @@ -1,4 +1,5 @@ using LibRed.Catalog; +using LibRed.Crypto; using LibRed.Formats; using LibRed.IO; using LibRed.Pages; @@ -7,7 +8,7 @@ namespace LibRed; /// -/// The public entry point to the Core layer: opens a Jet/ACE database file and +/// The public entry point to the Core layer: creates and opens Jet/ACE database files and /// exposes its catalog and tables. This is what the SQL engine and ADO provider /// build on; consumers wanting raw storage access start here. /// @@ -31,12 +32,11 @@ private JetDatabase(PageChannel channel) // a file corrupt when one names a page past the end, and fails its first allocation when one names // anything but a usage map. A read-only open never allocates, so it reads such a file as ACE would. if (!channel.IsReadOnly) - new PageAllocator(channel).ValidateGlobalMaps(); + channel.Allocator.ValidateGlobalMaps(); - // Find MSysObjects via the page-0 bootstrap pointer (0x20); fall back to the format default - // if it reads as 0 (never observed — every file points at page 2). - int catalogPage = DefinitionPage.CatalogRootPage > 0 ? DefinitionPage.CatalogRootPage : channel.Format.CatalogPage; - Catalog = new JetCatalog(channel, catalogPage); + // Find MSysObjects via the page-0 bootstrap pointer: its page is also its own MSysObjects Id, but that row + // can only be read once the table has been found. + Catalog = new JetCatalog(channel); } /// The decoded database definition page (page 0). @@ -66,9 +66,9 @@ private JetDatabase(PageChannel channel) public Collation Collation => DefinitionPage.Collation; /// Reads and decodes the table definition (TDEF) page at . - public TableDefinitionPage ReadTableDefinition(int pageNumber) + public TableDefinition ReadTableDefinition(int pageNumber) { - var tdef = new TableDefinitionPage(); + var tdef = new TableDefinition(); tdef.Read(_channel, pageNumber); return tdef; } @@ -81,6 +81,98 @@ public DataPage ReadDataPage(int pageNumber) return page; } + /// + /// Creates a new, empty database at from scratch — no DAO/ADOX. Hand-builds the + /// bootstrap for the given — Jet 4 (0x01, the Access 2000 / + /// 2002-2003 .mdb) or any ACCDB version — (page 0, the page-1 free map, and the + /// MSysObjects/MSysACEs TDEFs with their + /// usage maps + self-registering catalog rows), then the file is a normal LibRed database: further tables + /// are added through the ordinary writers. The file opens in the Access GUI: the 0xE00 user + /// commit-byte table is seeded here, and Access adds the system tables it wants (MSysAccessStorage, the + /// navigation-pane objects) itself — hand-creating those was tried and made things worse. + /// + /// Where the new file is written. + /// The format version byte to create it at. + /// + /// 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 — 417 configurations + /// (405 at version 0, and the twelve orders that have a version-1 table), the + /// two General orders, every locale in JetLocaleTailoring and the CJK orders, 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 Create(string path, byte version = 0x02, Collation? collation = null) + { + Collation sortOrder = collation ?? Collation.GeneralLegacy; + + // ACE 15 (0x04) can be read but never created: ACE REFUSES a file carrying the byte, an empty one + // included, and restamping 0x14 to 0x03 opens the identical bytes + // (Ace_refuses_the_0x04_version_byte_and_nothing_else_about_the_file). FromVersionByte still maps 0x04 + // onto the 0x03 layout, so reading one stays supported; only writing it is refused. + if (version == (byte)JetVersion.Version15_2013) + throw new NotSupportedException( + $"Cannot create a database at {nameof(JetVersion.Version15_2013)} (version byte 0x04): it is not a " + + $"valid format for a new database. Use {nameof(JetVersion.Version14_2010)}, the format Access 2013 " + + "databases use. Existing 0x04 files can still be opened for reading."); + + JetFormatBase format = JetFormatBase.FromVersionByte(version); + + // Page 0 carries the ANSI code page of the order's language, as ACE writes it — not a fixed 1252. + int codePage = JetCodePages.For(sortOrder) ?? throw new NotSupportedException( + $"Cannot create a database in {sortOrder}: the code page ACE writes for it has not been measured."); + + // Jet 4 (0x01, the Access 2000 / 2002-2003 `.mdb`) and the ACCDB versions can both be created; the + // identifier follows the version byte in DatabaseDefinitionPage.Build, so the pair is always consistent and the + // file reopens. Jet 3 is rejected by FromVersionByte already — DAO cannot create one either + // ("Could not find installable ISAM"), so there is nothing to compare against. + // + // A Jet 4 file needs no other difference: DAO's own dbVersion40 database contains exactly the same + // four core system tables this method builds. Access adds MSysAccessStorage, the navigation-pane + // tables and the MSysDb properties (AccessVersion, Build, ProjVer, …) when it first opens the file, + // for a Jet 4 file just as for an ACCDB — measured by diffing a DAO-created Access 2000 database + // before and after Access opened it. + // + // It adds AccessVersion **09.50** and MSysAccessStorage even to a Jet 4 file. The legacy + // MSysAccessObjects store (08.50, and the only place the unmodelled 0x11 type appears) comes from + // Access *creating* the database itself in the Access 2000 generation — a new file from its own New + // dialog. So the generation is chosen at creation by Access, and a file created by anything else gets + // the modern one when Access first opens it. Nothing a creator can or should reproduce: DAO does not, + // and neither does this. + // A page's number is where it lands as the file is generated, in ACE's order: page 0 (built last, once + // the pages it points at have numbers), the global usage maps, the four core TDEFs, then their usage maps. + // Page 0's bootstrap pointers record where they went, and that is how Access finds the catalog. + var (pages, globalMapPage, objPage, acesPage, queriesPage, relPage) = + JetCatalog.BuildBootstrap(format, sortOrder); + + // Created now, as Access would. The SIDs are worked out from the finished page 0 — its creation date, + // collation and empty password field all feed the keystream — so nothing in it may change after this. + // A database has no MSysAccounts or MSysGroups — those live in a workgroup file — so their pointers are zero. + byte[] page0 = pages[0] = DatabaseDefinitionPage.Build(version, format.IsAccdb, codePage, sortOrder, + DateTime.Now.ToOADate(), globalMapPage, objPage, acesPage, queriesPage, relPage, + accountsPage: 0, groupsPage: 0); + // The account SIDs as this file stores them: page 0 determines the keystream (page-00 §2.3). + byte[] sidUsers = SidKeystream.MaskAccount(page0, SidKeystream.UsersAccount, format); + byte[] sidAdmin = SidKeystream.MaskAccount(page0, SidKeystream.AdminAccount, format); + byte[] sidEngine = SidKeystream.MaskAccount(page0, SidKeystream.EngineAccount, format); + byte[] sidCreator = SidKeystream.MaskAccount(page0, SidKeystream.CreatorAccount, format); + + // Creation is an explicit create-new operation. FileMode.Create would silently truncate an existing + // database before any of the format bootstrap work could validate or fail. + using (var fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + foreach (byte[]? p in pages) fs.Write(p!, 0, format.PageSize); + + // Reopen through the normal stack and self-register the four core tables, so the catalog then finds + // them and every other table can go through SchemaEditor. + using var db = JetDatabase.Open(path, readOnly: false); + JetCatalog.InitializeBootstrap(db, sidUsers, sidAdmin, sidEngine, sidCreator); + } + /// Opens a database file (read-only by default). For a password-encrypted ACCDB, supply /// ; writable opens encrypt modified pages again before publishing them. /// exclusive takes the file for this database alone, as ACE's Mode=Share Exclusive @@ -380,13 +472,14 @@ public void CreateTable( IReadOnlyList<(string Column, string DefaultSql)>? columnDefaults = null, IReadOnlyList<(string Name, string Expression)>? checkConstraints = null, string? primaryKeyName = null, - int primaryKeyDeclaredAfterColumns = 0) + int primaryKeyDeclaredAfterColumns = 0, + TableType tableType = TableType.User) { ValidateCalculated(columns, [.. columns.Select(c => c.Name)]); EnsureFormatForCalculated(columns); - new Storage.TableCreator(_channel, Catalog, Collation) + new Catalog.SchemaEditor(_channel, Catalog, Collation) .Create(name, columns, primaryKey, relationships, uniqueConstraints, columnDefaults, checkConstraints, - primaryKeyName, primaryKeyDeclaredAfterColumns); + primaryKeyName, primaryKeyDeclaredAfterColumns, tableType); Catalog.Invalidate(); } @@ -397,7 +490,7 @@ public void CreateTable( public void CreateIndex(string table, string index, IReadOnlyList<(string Column, bool Descending)> columns, bool isUnique = false, bool isPrimary = false, bool disallowNull = false, bool ignoreNulls = false) { - new Storage.TableCreator(_channel, Catalog, Collation).AddIndex(table, index, columns, isUnique, isPrimary, disallowNull, ignoreNulls); + new Catalog.SchemaEditor(_channel, Catalog, Collation).AddIndex(table, index, columns, isUnique, isPrimary, disallowNull, ignoreNulls); Catalog.Invalidate(); } @@ -405,7 +498,7 @@ public void CreateIndex(string table, string index, IReadOnlyList<(string Column /// FOREIGN KEY. Writes the child backing index, the parent's incoming block and MSysRelationships. public void AddForeignKey(string childTable, RelationshipSpec relationship) { - new Storage.TableCreator(_channel, Catalog, Collation).AddForeignKey(childTable, relationship); + new Catalog.SchemaEditor(_channel, Catalog, Collation).AddForeignKey(childTable, relationship); Catalog.Invalidate(); } @@ -413,7 +506,7 @@ public void AddForeignKey(string childTable, RelationshipSpec relationship) /// Merges into the table's LvProp CheckConstraints property; the engine enforces it on insert/update. public void AddCheckConstraint(string table, string name, string expression) { - new Storage.TableCreator(_channel, Catalog, Collation).AddCheckConstraint(table, name, expression); + new Catalog.SchemaEditor(_channel, Catalog, Collation).AddCheckConstraint(table, name, expression); Catalog.Invalidate(); } @@ -421,7 +514,7 @@ public void AddCheckConstraint(string table, string name, string expression) /// LvProp CheckConstraints property. Returns false if no CHECK of that name exists. public bool DropCheckConstraint(string table, string name) { - bool dropped = new Storage.TableCreator(_channel, Catalog, Collation).DropCheckConstraint(table, name); + bool dropped = new Catalog.SchemaEditor(_channel, Catalog, Collation).DropCheckConstraint(table, name); if (dropped) Catalog.Invalidate(); return dropped; } @@ -430,7 +523,7 @@ public bool DropCheckConstraint(string table, string name) /// column's length included, is . public void AlterColumn(string table, string column, ColumnSpec newSpec) { - new Storage.TableCreator(_channel, Catalog, Collation).AlterColumn(table, column, newSpec); + new Catalog.SchemaEditor(_channel, Catalog, Collation).AlterColumn(table, column, newSpec); Catalog.Invalidate(); } @@ -439,7 +532,7 @@ public void AlterColumn(string table, string column, ColumnSpec newSpec) /// preserving the dead old slot + a rebuild of the indexes over the column. public void AlterColumnTypeInPlace(string table, string column, ColumnSpec newSpec) { - new Storage.TableCreator(_channel, Catalog, Collation).AlterColumnTypeInPlace(table, column, newSpec); + new Catalog.SchemaEditor(_channel, Catalog, Collation).AlterColumnTypeInPlace(table, column, newSpec); Catalog.Invalidate(); } @@ -447,7 +540,7 @@ public void AlterColumnTypeInPlace(string table, string column, ColumnSpec newSp /// DefaultValue in the LvProp blob; the engine applies it on an omit-insert. public void SetColumnDefault(string table, string column, string defaultSql) { - new Storage.TableCreator(_channel, Catalog, Collation).SetColumnDefault(table, column, defaultSql); + new Catalog.SchemaEditor(_channel, Catalog, Collation).SetColumnDefault(table, column, defaultSql); Catalog.Invalidate(); } @@ -456,7 +549,7 @@ public void SetColumnDefault(string table, string column, string defaultSql) /// (ACE-verified). A no-op if the column had no default. public void DropColumnDefault(string table, string column) { - new Storage.TableCreator(_channel, Catalog, Collation).DropColumnDefault(table, column); + new Catalog.SchemaEditor(_channel, Catalog, Collation).DropColumnDefault(table, column); Catalog.Invalidate(); } @@ -465,21 +558,21 @@ public void DropColumnDefault(string table, string column) /// the engine enforces it on insert and ACE reads it byte-faithfully (verified). public void SetColumnRequired(string table, string column, bool required) { - new Storage.TableCreator(_channel, Catalog, Collation).SetColumnRequired(table, column, required); + new Catalog.SchemaEditor(_channel, Catalog, Collation).SetColumnRequired(table, column, required); Catalog.Invalidate(); } /// Drops a named FOREIGN KEY constraint from a table — ALTER TABLE … DROP CONSTRAINT. Returns /// false if no such relationship exists (e.g. the name is a primary-key/unique index, not yet handled). public bool DropConstraint(string childTable, string name) => - new Storage.TableCreator(_channel, Catalog, Collation).DropConstraint(childTable, name); + new Catalog.SchemaEditor(_channel, Catalog, Collation).DropConstraint(childTable, name); /// Renames a table — ALTER TABLE … RENAME TO. Updates MSysObjects.Name and repoints the by-name /// table references in MSysRelationships; indexes and stored queries are left alone, matching ACE. Returns /// false if the table doesn't exist; throws if the new name is taken. public bool RenameTable(string oldName, string newName) { - bool renamed = new Storage.TableCreator(_channel, Catalog, Collation).RenameTable(oldName, newName); + bool renamed = new Catalog.SchemaEditor(_channel, Catalog, Collation).RenameTable(oldName, newName); if (renamed) Catalog.Invalidate(); // the cached TableDefs still carry the old name return renamed; } @@ -489,7 +582,7 @@ public bool RenameTable(string oldName, string newName) /// DEFAULT. Returns false if the column doesn't exist; throws if the new name is taken on that table. public bool RenameColumn(string table, string oldName, string newName) { - bool renamed = new Storage.TableCreator(_channel, Catalog, Collation).RenameColumn(table, oldName, newName); + bool renamed = new Catalog.SchemaEditor(_channel, Catalog, Collation).RenameColumn(table, oldName, newName); if (renamed) Catalog.Invalidate(); return renamed; } @@ -498,7 +591,7 @@ public bool RenameColumn(string table, string oldName, string newName) /// untouched), except that a memo/OLE column's long-value usage maps are retired with it, as ACE does. /// Returns false if the column doesn't exist; throws for an indexed/keyed column (drop the index first). public bool DropColumn(string table, string column) => - new Storage.TableCreator(_channel, Catalog, Collation).DropColumn(table, column); + new Catalog.SchemaEditor(_channel, Catalog, Collation).DropColumn(table, column); /// Adds a column — ALTER TABLE … ADD COLUMN. Appends the descriptor/name and bumps the counts; /// existing rows read it as NULL. A memo/OLE column gets its long-value usage maps at the same time. @@ -512,7 +605,7 @@ public bool AddColumn(string table, Catalog.ColumnSpec column, string? defaultVa ValidateCalculated([column], visible); EnsureFormatForCalculated([column]); } - return new Storage.TableCreator(_channel, Catalog, Collation).AddColumn(table, column, defaultValue); + return new Catalog.SchemaEditor(_channel, Catalog, Collation).AddColumn(table, column, defaultValue); } /// Raises the file to ACE 14 when a calculated column is being created — the floor its own @@ -543,23 +636,23 @@ private static void ValidateCalculated( /// Drops an index — DROP INDEX … ON table. Removes its TDEF blocks and frees its B-tree root. /// Returns false if the index doesn't exist; throws if it backs a relationship. public bool DropIndex(string table, string index) => - new Storage.TableCreator(_channel, Catalog, Collation).DropIndex(table, index); + new Catalog.SchemaEditor(_channel, Catalog, Collation).DropIndex(table, index); /// Drops a table — DROP TABLE. Removes its MSysObjects + MSysACEs rows and frees its pages. /// Returns false if the table doesn't exist; throws if it is in a relationship. public bool DropTable(string table) => - new Storage.TableCreator(_channel, Catalog, Collation).DropTable(table); + new Catalog.SchemaEditor(_channel, Catalog, Collation).DropTable(table); /// Drops a view or stored procedure — DROP VIEW / DROP PROCEDURE (interchangeable, both target a /// type-5 query object). Removes its MSysObjects + MSysQueries + MSysACEs rows. Returns false if absent. public bool DropQueryObject(string name) => - new Storage.TableCreator(_channel, Catalog, Collation).DropQueryObject(name); + new Catalog.SchemaEditor(_channel, Catalog, Collation).DropQueryObject(name); /// Creates a view (a stored SELECT query) — the CREATE VIEW statement. Written the way Access /// does: an MSysObjects type-5 row plus the query decomposed into MSysQueries rows. public void CreateView(string name, ViewSpec spec) { - new Storage.ViewCreator(_channel, Catalog).Create(name, spec); + Catalog.CreateView(name, spec); Catalog.Invalidate(); } @@ -567,15 +660,15 @@ public void CreateView(string name, ViewSpec spec) /// query, as CREATE PROCEDURE persists them. Written byte-faithfully the way Access does. public void CreateActionQuery(string name, ActionQuerySpec spec) { - new Storage.ViewCreator(_channel, Catalog).CreateAction(name, spec); + Catalog.CreateActionQuery(name, spec); Catalog.Invalidate(); } /// Every MSysNameMap row — Access's Name AutoCorrect map, one row per object it tracks — with /// its map decoded; empty when the file has no such table. The engine never maintains these: a row reflects /// the names as Access last saw them (docs/format/system-catalog.md §11). - public IReadOnlyList ReadNameMaps() => - new Storage.TableCreator(_channel, Catalog, Collation).ReadNameMapRows(); + public IReadOnlyList ReadNameMaps() => + new Catalog.SchemaEditor(_channel, Catalog, Collation).ReadNameMapRows(); /// Replaces the map of the MSysNameMap row for the object whose GUID property is /// — and the row's name when is given. Returns false @@ -583,34 +676,34 @@ public IReadOnlyList ReadNameMaps() => public bool UpdateNameMap(Guid objectGuid, NameMap map, string? name = null) { ArgumentNullException.ThrowIfNull(map); - return new Storage.TableCreator(_channel, Catalog, Collation).WriteNameMapRow(objectGuid, map, name); + return new Catalog.SchemaEditor(_channel, Catalog, Collation).WriteNameMapRow(objectGuid, map, name); } /// The NameMap property — the object's own copy of its Name AutoCorrect map — of the object - /// with this name and MSysObjects.Type (1 table, 6 linked table, -32768 form, -32764 report); null - /// when it has none. Throws when there is no such object. - public NameMap? ReadNameMapProperty(string objectName, short objectType) => - new Storage.TableCreator(_channel, Catalog, Collation).ReadNameMapProperty(objectName, objectType); + /// with this name and type — a table, a linked table, a form or a report; null when it has none. Throws when + /// there is no such object. + public NameMap? ReadNameMapProperty(string objectName, Formats.ObjectType objectType) => + new Catalog.SchemaEditor(_channel, Catalog, Collation).ReadNameMapProperty(objectName, objectType); /// Sets the object's NameMap property, or removes it when is null, /// leaving every other property as it was. Throws when there is no such object. - public void UpdateNameMapProperty(string objectName, short objectType, NameMap? map) + public void UpdateNameMapProperty(string objectName, Formats.ObjectType objectType, NameMap? map) { - new Storage.TableCreator(_channel, Catalog, Collation).WriteNameMapProperty(objectName, objectType, map); + new Catalog.SchemaEditor(_channel, Catalog, Collation).WriteNameMapProperty(objectName, objectType, map); Catalog.Invalidate(); } /// Opens a table directly from its TDEF page, bypassing the catalog — used during database /// creation to seed the system tables before they self-register in MSysObjects. - public Storage.Table OpenTableAt(int tdefPage, string name, bool isSystem = true) => - new(_channel, Catalog.ReadTableDefinitionAt(tdefPage, name, isSystem)); + internal Storage.Table OpenTableAt(int tdefPage, string name, bool isSystem = true) => + new(_channel, Catalog.ReadTableDefinition(tdefPage, name, isSystem), Catalog); /// Opens a table by name for row access. public Table OpenTable(string name) { - TableDef def = Catalog.FindTable(name) + TableDefinition def = Catalog.FindTable(name) ?? throw new ArgumentException($"Table '{name}' was not found.", nameof(name)); - return new Table(_channel, def); + return new Table(_channel, def, Catalog); } /// Closes the database. A writable handle first returns the pages it released to the global free @@ -625,7 +718,7 @@ public void Dispose() if (!_channel.IsReadOnly) { _channel.RollbackTransaction(); - new PageAllocator(_channel).ReturnReleasedPages(); + _channel.Allocator.ReturnReleasedPages(); } } catch (InvalidDataException) diff --git a/src/LibRed/LibRed.Core/LibRed.Core.csproj b/src/LibRed/LibRed.Core/LibRed.Core.csproj index f216d823..b44f46b3 100644 --- a/src/LibRed/LibRed.Core/LibRed.Core.csproj +++ b/src/LibRed/LibRed.Core/LibRed.Core.csproj @@ -20,6 +20,9 @@ encoder internals — in particular the switch that makes a generator read the bare table. --> + + diff --git a/src/LibRed/LibRed.Core/Pages/DataPage.cs b/src/LibRed/LibRed.Core/Pages/DataPage.cs index e3b3d7c3..ae75667a 100644 --- a/src/LibRed/LibRed.Core/Pages/DataPage.cs +++ b/src/LibRed/LibRed.Core/Pages/DataPage.cs @@ -1,15 +1,10 @@ using LibRed.Formats; using LibRed.IO; +using LibRed.Storage; +using System.Buffers.Binary; namespace LibRed.Pages; -/// One entry in a data page's row slot directory. -/// Byte offset of the row record within the page. -/// Length of the row record in bytes. -/// The row is marked deleted. -/// The slot points at an overflow/lookup record rather than inline data. -public readonly record struct RowSlot(int Offset, int Length, bool IsDeleted, bool HasOverflow); - /// /// A data page holding the rows of a single table. Rows are addressed by a slot /// directory near the front of the page and packed from the page end backward. @@ -17,12 +12,22 @@ namespace LibRed.Pages; /// public sealed class DataPage : Page { + /// One entry in a data page's row slot directory. + /// Byte offset of the row record within the page. + /// Length of the row record in bytes. + /// The row is marked deleted. + /// The slot points at an overflow/lookup record rather than inline data. + public readonly record struct RowSlot(int Offset, int Length, bool IsDeleted, bool HasOverflow); + - private readonly List _rows = []; + private readonly List _rows = []; private PageBuffer _buffer; public override PageType Type => PageType.DataPage; + /// Marks this data page released, preserving its row directory and data. + internal static void MarkReleased(Span page) => PageHeader.WriteType(page, PageType.ReleasedDataPage); + /// The TDEF page of the table that owns this data page (0 for long-value pages). public int OwningTablePage { get; private set; } @@ -32,39 +37,188 @@ public sealed class DataPage : Page public int FreeSpace { get; private set; } public int RowCount { get; private set; } - public IReadOnlyList Rows => _rows; + public IReadOnlyList Rows => _rows; - public override void Read(PageBuffer buffer, JetFormatBase format) + internal override void Read(PageBuffer buffer, JetFormatBase format) { ValidateHeader(buffer, format, out int rowCount, out int directoryEnd); _buffer = buffer; PageNumber = buffer.PageNumber; - uint owner = buffer.ReadUInt32(format.DataOwnerOffset); - IsLongValuePage = owner == LongValueFormat.LvalMarker; + uint owner = ReadOwner(buffer.Span, format); + IsLongValuePage = owner == JetFormatBase.LongValuePageMarker; OwningTablePage = IsLongValuePage ? 0 : (int)owner; - FreeSpace = buffer.ReadUInt16(format.DataFreeSpaceOffset); + FreeSpace = ReadFreeSpace(buffer.Span, format); RowCount = rowCount; _rows.Clear(); int prevEnd = buffer.Length; for (int i = 0; i < RowCount; i++) { - int raw = buffer.ReadUInt16(format.DataRowDirectoryOffset + i * 2); - int offset = raw & RowPointer.OffsetMask; - bool deleted = (raw & RowPointer.DeletedFlag) != 0; - bool overflow = (raw & RowPointer.OverflowFlag) != 0; + (int offset, RowSlotFlags flags) = ReadSlot(buffer.Span, format, i); // Rows are packed from the page end backward, so a slot runs from its own // offset up to where the previous slot's row began. ValidateSlot(buffer, i, offset, prevEnd, directoryEnd); - int length = prevEnd - offset; - _rows.Add(new RowSlot(offset, length, deleted, overflow)); + _rows.Add(Slot(offset, prevEnd - offset, flags)); prevEnd = offset; } } + private static DataPage.RowSlot Slot(int offset, int length, RowSlotFlags flags) => + new(offset, length, flags.HasFlag(RowSlotFlags.Deleted), flags.HasFlag(RowSlotFlags.Overflow)); + + /// A slot's , the inverse of how reads them. + internal static RowSlotFlags Flags(DataPage.RowSlot slot) => + (slot.IsDeleted ? RowSlotFlags.Deleted : RowSlotFlags.None) + | (slot.HasOverflow ? RowSlotFlags.Overflow : RowSlotFlags.None); + + /// The page's owner field: the TDEF page of the table it belongs to, + /// on a long-value page, and 0 on a usage-map page, which belongs to no table. + internal static uint ReadOwner(ReadOnlySpan page, JetFormatBase format) => + BinaryPrimitives.ReadUInt32LittleEndian(page.Slice(format.DataOwnerOffset, sizeof(uint))); + + /// A fresh data page belonging to (as reads it), with no + /// rows: a zero count, and the whole page free. Every data page this engine creates starts here. + internal static byte[] NewPage(JetFormatBase format, uint owner) + { + var page = new byte[format.PageSize]; + PageHeader.WriteType(page, PageType.DataPage); + BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.DataOwnerOffset, sizeof(uint)), owner); + LayRows(page, format, [], []); + return page; + } + + /// The page's row count — the number of entries in its slot directory. + internal static int ReadRowCount(ReadOnlySpan page, JetFormatBase format) => + BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(format.DataRowCountOffset, sizeof(ushort))); + + /// The page's declared free space: the bytes between the end of the slot directory and the lowest row. + internal static int ReadFreeSpace(ReadOnlySpan page, JetFormatBase format) => + BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(format.DataFreeSpaceOffset, sizeof(ushort))); + + /// Where a slot directory of entries ends — the lowest offset a row may take. + internal static int DirectoryEnd(JetFormatBase format, int rowCount) => + format.DataRowDirectoryOffset + rowCount * format.DataRowDirectoryEntrySize; + + private static void WriteHeader(Span page, JetFormatBase format, int rowCount, int freeSpace) + { + BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(format.DataRowCountOffset, sizeof(ushort)), (ushort)rowCount); + BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(format.DataFreeSpaceOffset, sizeof(ushort)), (ushort)freeSpace); + } + + /// The slot directory entry of : the row's offset, and the + /// in the bits above it. Unvalidated — the caller bounds the row and the offset. + internal static (int Offset, RowSlotFlags Flags) ReadSlot(ReadOnlySpan page, JetFormatBase format, int row) + { + int raw = BinaryPrimitives.ReadUInt16LittleEndian( + page.Slice(format.DataRowDirectoryOffset + row * format.DataRowDirectoryEntrySize, format.DataRowDirectoryEntrySize)); + return (raw & format.DataRowOffsetMask, (RowSlotFlags)(raw & ~format.DataRowOffsetMask)); + } + + /// Writes the slot directory entry of — the inverse of . + internal static void WriteSlot(Span page, JetFormatBase format, int row, int offset, RowSlotFlags flags) => + BinaryPrimitives.WriteUInt16LittleEndian( + page.Slice(format.DataRowDirectoryOffset + row * format.DataRowDirectoryEntrySize, format.DataRowDirectoryEntrySize), + (ushort)((int)flags | (offset & format.DataRowOffsetMask))); + + /// The offset of the lowest row on the page — where the next row packed from the end goes below — + /// or the page size when there are none. + internal static int LowestRowOffset(ReadOnlySpan page, JetFormatBase format) + { + int lowest = format.PageSize; + for (int i = ReadRowCount(page, format) - 1; i >= 0; i--) + lowest = Math.Min(lowest, ReadSlot(page, format, i).Offset); + return lowest; + } + + /// + /// Lays on from its end backward in slot order — row 0 + /// nearest the end — each slot carrying its own , and sets the row count and free space + /// to match. A zero-length record is a tombstone on the previous row's start. Every write that rearranges a + /// page's rows comes through here; what precedes the directory and what lies between it and the rows is left as + /// it stands. Returns false, writing nothing, when the records and their directory do not fit the page. + /// + internal static bool LayRows(Span page, JetFormatBase format, IReadOnlyList records, + IReadOnlyList flags) + { + int directoryEnd = DirectoryEnd(format, records.Count); + if (format.PageSize - records.Sum(r => r.Length) < directoryEnd) return false; + + int offset = format.PageSize; + for (int i = 0; i < records.Count; i++) + { + offset -= records[i].Length; + records[i].CopyTo(page[offset..]); + WriteSlot(page, format, i, offset, flags[i]); + } + WriteHeader(page, format, records.Count, offset - directoryEnd); + return true; + } + + /// + /// Appends below the page's lowest row as a new slot carrying , + /// leaving every existing row where it is, and updates the row count and free space — the declared free space + /// less the record and its slot, so bytes the page already counted are not recounted. is + /// the new slot — the old row count. Returns false, writing nothing, when the record and its slot do not fit + /// between the directory and the lowest row. + /// + internal static bool TryAppendRow(Span page, JetFormatBase format, ReadOnlySpan record, + RowSlotFlags flags, out int row) + { + row = ReadRowCount(page, format); + int offset = LowestRowOffset(page, format) - record.Length; + if (offset < DirectoryEnd(format, row + 1)) return false; + + record.CopyTo(page[offset..]); + WriteSlot(page, format, row, offset, flags); + WriteHeader(page, format, row + 1, + ReadFreeSpace(page, format) - record.Length - format.DataRowDirectoryEntrySize); + return true; + } + + /// + /// Takes a row's bytes off its page the way ACE does: the rows stored below it slide up to close the + /// gap, their slot offsets follow, and the emptied slot becomes a zero-length tombstone whose offset is + /// the row's FORMER END, flagged deleted + overflow. The freed bytes go back to the page's free-space + /// count, so a delete-heavy table stops growing where ACE's would not. + /// + /// Slot indices never move, which is what keeps index entries and row ids valid — only offsets + /// change. Pointing the tombstone at the former end rather than at the page end is what keeps the + /// directory non-increasing, which relies on to derive each row's length from the + /// previous slot. Verified against ACE for a first, middle and last row: deleting the first of three + /// 19-byte rows gives D000 0FED 0FDA, the middle 0FED CFED 0FDA, the last + /// 0FED 0FDA CFDA, with free space rising by 19 in each case. + /// + /// + internal static void ReclaimRow(Span page, JetFormatBase format, int row) + { + int rowCount = ReadRowCount(page, format); + + // Slot offsets are non-increasing with slot index, so row i occupies [offset(i), offset(i-1)) and + // every row stored below this one is simply a LATER slot. Working by slot index rather than by + // comparing offsets is what keeps zero-length tombstones correct: one sitting at exactly this row's + // offset has to move up with the rows after it, and an offset comparison leaves it behind — where it + // then absorbs this row's length and starves the next live row down to zero. + int start = ReadSlot(page, format, row).Offset; + int end = row == 0 ? format.PageSize : ReadSlot(page, format, row - 1).Offset; + int length = end - start; + + int lowest = rowCount > 0 ? ReadSlot(page, format, rowCount - 1).Offset : format.PageSize; + if (length > 0 && start > lowest) + page[lowest..start].CopyTo(page[(lowest + length)..]); + + for (int i = row + 1; i < rowCount; i++) + { + (int offset, RowSlotFlags flags) = ReadSlot(page, format, i); + WriteSlot(page, format, i, offset + length, flags); + } + + WriteSlot(page, format, row, end, RowSlotFlags.Deleted | RowSlotFlags.Overflow); + WriteHeader(page, format, rowCount, ReadFreeSpace(page, format) + length); + } + /// Returns the raw bytes of the row at in the slot directory. /// The index is outside the slot directory. Callers pass a row /// number read out of the file (a usage-map pointer, a long-value descriptor), so out of range means @@ -75,7 +229,7 @@ public ReadOnlySpan GetRow(int index) throw new InvalidDataException( $"Row {index} is outside this page's slot directory ({_rows.Count} rows)."); - RowSlot slot = _rows[index]; + DataPage.RowSlot slot = _rows[index]; return _buffer.Slice(slot.Offset, slot.Length); } @@ -84,7 +238,7 @@ public ReadOnlySpan GetRow(int index) /// where parsing every slot on the page just to take one row was the dominant cost. Returns false when /// is past the page's row count. public static bool TryReadRow(PageBuffer buffer, JetFormatBase format, int index, - out RowSlot slot, out ReadOnlySpan bytes) + out DataPage.RowSlot slot, out ReadOnlySpan bytes) { ValidateHeader(buffer, format, out int rowCount, out int directoryEnd); if (index < 0 || index >= rowCount) @@ -94,14 +248,12 @@ public static bool TryReadRow(PageBuffer buffer, JetFormatBase format, int index return false; } - int dir = format.DataRowDirectoryOffset; - int raw = buffer.ReadUInt16(dir + index * 2); - int offset = raw & RowPointer.OffsetMask; + (int offset, RowSlotFlags flags) = ReadSlot(buffer.Span, format, index); // Rows pack from the page end backward, so this slot runs up to where the previous slot began // (or the page end for slot 0) — read just those two directory entries instead of walking all of them. - int prevEnd = index == 0 ? buffer.Length : buffer.ReadUInt16(dir + (index - 1) * 2) & RowPointer.OffsetMask; + int prevEnd = index == 0 ? buffer.Length : ReadSlot(buffer.Span, format, index - 1).Offset; ValidateSlot(buffer, index, offset, prevEnd, directoryEnd); - slot = new RowSlot(offset, prevEnd - offset, (raw & RowPointer.DeletedFlag) != 0, (raw & RowPointer.OverflowFlag) != 0); + slot = Slot(offset, prevEnd - offset, flags); bytes = buffer.Slice(offset, prevEnd - offset); return true; } @@ -115,26 +267,25 @@ private static void ValidateHeader(PageBuffer buffer, JetFormatBase format, out throw new InvalidDataException( $"Page {buffer.PageNumber} is type 0x{(ushort)PageHeader.ReadType(buffer.Span):X4}, not a data page (0x0101)."); - rowCount = buffer.ReadUInt16(format.DataRowCountOffset); + rowCount = ReadRowCount(buffer.Span, format); // An index addresses a row by a one-byte slot number, so a page can hold at most 255 and ACE stops // there. Reading further is not tolerance, it is reading rows no index can name — including any this // engine wrote past the cap, which is exactly the bug a strict reader is here to surface. - if (rowCount > RowPointer.MaxRowsPerPage) + if (rowCount > format.MaxRowsPerPage) throw new InvalidDataException( $"Data page {buffer.PageNumber} declares {rowCount} rows; a page holds at most " - + $"{RowPointer.MaxRowsPerPage}, the most a one-byte slot number can address."); - long end = (long)format.DataRowDirectoryOffset + rowCount * 2L; - if (end > buffer.Length) + + $"{format.MaxRowsPerPage}, the most a one-byte slot number can address."); + directoryEnd = DirectoryEnd(format, rowCount); // the count is capped above, so this cannot overflow + if (directoryEnd > buffer.Length) throw new InvalidDataException( $"Data page {buffer.PageNumber} declares {rowCount} rows, placing its slot directory past the page."); - directoryEnd = (int)end; } private static void ValidateSlot(PageBuffer buffer, int index, int offset, int previousEnd, int directoryEnd) => ValidateSlot(buffer.PageNumber, buffer.Length, index, offset, previousEnd, directoryEnd); /// The slot-directory invariant, over raw values so the write paths can share it. Offsets are - /// masked out of 13 bits and so can reach 8191 on a 4096-byte page; rows are packed from the page end + /// masked out of and so can exceed the page; rows are packed from the page end /// backward, so they must also never increase. RowInserter's in-place repackers re-derived this /// arithmetic without the checks, which on a corrupt directory produced an out-of-range exception from a /// half-repacked page rather than a diagnosis. @@ -149,4 +300,63 @@ internal static void ValidateSlot( throw new InvalidDataException( $"Data page {pageNumber} row slot {index} has offset {offset}, above the previous row boundary {previousEnd}."); } + + /// A validated, page-backed relocated-row target. The bytes remain zero-copy for index seeks. + internal readonly record struct RelocatedRow(PageBuffer Buffer, DataPage.RowSlot Slot, int RowNumber) + { + public ReadOnlySpan Bytes => Buffer.Slice(Slot.Offset, Slot.Length); + } + + + + /// + /// 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 RelocatedRow ResolveRelocation(PageChannel channel, int owningTablePage, + DataPage.RowSlot sourceSlot, ReadOnlySpan sourceBytes) + { + if (sourceSlot.IsDeleted || !sourceSlot.HasOverflow) + throw new InvalidDataException("A relocation source must be a live overflow row slot."); + if (sourceBytes.Length < PageBuffer.RecordPointerSize) + throw new InvalidDataException( + $"A relocation source must begin with a {PageBuffer.RecordPointerSize}-byte pointer; found {sourceBytes.Length} bytes."); + + (int rowNumber, int pageNumber) = PageBuffer.ReadRecordPointer(sourceBytes, 0); + if (pageNumber <= 0 || pageNumber >= channel.PageCount) + throw new InvalidDataException( + $"Relocation pointer targets page {pageNumber}, outside the file's 1..{channel.PageCount - 1} range."); + + PageBuffer targetBuffer = channel.ReadPageShared(pageNumber); + uint owner = DataPage.ReadOwner(targetBuffer.Span, channel.Format); + if (owner != (uint)owningTablePage) + throw new InvalidDataException( + $"Relocation target page {pageNumber} belongs to TDEF {owner}, not TDEF {owningTablePage}."); + if (!DataPage.TryReadRow(targetBuffer, channel.Format, rowNumber, out DataPage.RowSlot targetSlot, out _)) + throw new InvalidDataException( + $"Relocation pointer targets missing row {rowNumber} on page {pageNumber}."); + if (!targetSlot.IsDeleted || targetSlot.HasOverflow || targetSlot.Length == 0) + throw new InvalidDataException( + $"Relocation target {pageNumber}:{rowNumber} is not a nonempty hidden inline row."); + + return new RelocatedRow(targetBuffer, targetSlot, rowNumber); + } + } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs index 6641e4cf..b71bd28a 100644 --- a/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs +++ b/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs @@ -1,5 +1,8 @@ using LibRed.Catalog; using LibRed.IO; +using LibRed.Formats; +using LibRed.Storage; +using System.Text; using System.Buffers.Binary; namespace LibRed.Pages; @@ -44,76 +47,247 @@ public sealed class DatabaseDefinitionPage : Page 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. + /// pointer at . 2 where ACE creates it, but any page the pointer names. public int CatalogRootPage { get; internal set; } + /// Page number of the MSysACEs TDEF, from the bootstrap pointer at + /// . + public int AcesRootPage { get; internal set; } + + /// Page number of the MSysQueries TDEF, from the bootstrap pointer at + /// . + public int QueriesRootPage { get; internal set; } + + /// Page number of the MSysRelationships TDEF, from the bootstrap pointer at + /// . + public int RelationshipsRootPage { get; internal set; } + + /// Page number of the MSysAccounts TDEF, from the bootstrap pointer at + /// ; zero unless this is a workgroup file. + public int AccountsRootPage { get; internal set; } + + /// Page number of the MSysGroups TDEF, from the bootstrap pointer at + /// ; zero unless this is a workgroup file. + public int GroupsRootPage { get; internal set; } + /// Where the global free-pages usage map lives, from the [row:1][page:3] pointer at - /// . Page 1 row 0 in every file ACE writes. + /// . Page 1 row 0 where ACE creates it. public (int Row, int Page) FreePagesMap { get; internal set; } /// Where the global released-pages usage map lives, from the [row:1][page:3] pointer at - /// . Page 1 row 1 in every file ACE writes. + /// . Page 1 row 1 where ACE creates it. public (int Row, int Page) ReleasedPagesMap { get; internal set; } public DateTime DatabaseCreationDate { get; internal set; } - public override void Read(PageBuffer buffer, Formats.JetFormatBase format) + internal override void Read(PageBuffer buffer, Formats.JetFormatBase format) { PageNumber = buffer.PageNumber; FormatIdentifier = Formats.JetFormatBase.ReadFormatIdentifier(buffer.Span); - JetVersion = buffer.ReadByte(Formats.JetFormatBase.VersionOffset); + JetVersion = Formats.JetFormatBase.ReadVersionByte(buffer.Span); // The header from 0x18 is XOR-obfuscated with a fixed RC4 keystream; de-obfuscate the // whole region once, then read the fields out of the clear copy. - Span clear = stackalloc byte[Formats.JetFormatBase.PageZeroHeaderMask.Length]; - Demask(buffer.Span, clear); - int b = Formats.JetFormatBase.PageZeroHeaderMaskStart; - - 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)); - FreePagesMap = ReadMapPointer(buffer.Span, Formats.JetFormatBase.FreePagesMapPointerOffset); - ReleasedPagesMap = ReadMapPointer(buffer.Span, Formats.JetFormatBase.ReleasedPagesMapPointerOffset); + Span clear = stackalloc byte[format.PageZeroHeaderMaskLength]; + int b = format.PageZeroHeaderMaskStart; + ReadMasked(buffer.Span, b, clear, format); + + CodePage = BinaryPrimitives.ReadUInt16LittleEndian(clear.Slice(format.CodePageOffset - b, 2)); + DatabaseKey = ReadDatabaseKey(buffer.Span, format); + DefaultCollationLcid = BinaryPrimitives.ReadUInt16LittleEndian(clear.Slice(format.CollationSortOrderOffset - b, 2)); + DefaultCollationSortId = clear[format.CollationSortIdOffset - b]; + DefaultCollationVersion = clear[format.CollationVersionOffset - b]; + CatalogRootPage = BinaryPrimitives.ReadInt32LittleEndian(clear.Slice(format.CatalogRootPointerOffset - b, 4)); + AcesRootPage = BinaryPrimitives.ReadInt32LittleEndian(clear.Slice(format.AcesRootPointerOffset - b, 4)); + QueriesRootPage = BinaryPrimitives.ReadInt32LittleEndian(clear.Slice(format.QueriesRootPointerOffset - b, 4)); + RelationshipsRootPage = BinaryPrimitives.ReadInt32LittleEndian(clear.Slice(format.RelationshipsRootPointerOffset - b, 4)); + AccountsRootPage = BinaryPrimitives.ReadInt32LittleEndian(clear.Slice(format.AccountsRootPointerOffset - b, 4)); + GroupsRootPage = BinaryPrimitives.ReadInt32LittleEndian(clear.Slice(format.GroupsRootPointerOffset - b, 4)); + FreePagesMap = ReadMapPointer(buffer.Span, format.FreePagesMapPointerOffset, format); + ReleasedPagesMap = ReadMapPointer(buffer.Span, format.ReleasedPagesMapPointerOffset, format); // An OLE Automation date, so it is decoded by the OA function rather than by hand: the two disagree // below the epoch, where OA keeps the time fraction positive (-1.25 is 1899-12-29 06:00, not // 1899-12-28 18:00). And the value comes straight off page 0, so a NaN, an infinity or anything past // DateTime.MaxValue is corruption in the very first thing an open does — reported as such, rather than // escaping as ArgumentOutOfRangeException from inside AddDays. - double days = BinaryPrimitives.ReadDoubleLittleEndian(clear.Slice(Formats.JetFormatBase.CreationDateOffset - b, 8)); - if (!double.IsFinite(days) || days <= MinOleAutomationDate || days >= MaxOleAutomationDate) - throw new InvalidDataException( - $"Page 0's creation date ({days}) is not a valid OLE Automation date."); - DatabaseCreationDate = DateTime.FromOADate(days); + double days = BinaryPrimitives.ReadDoubleLittleEndian(clear.Slice(format.CreationDateOffset - b, sizeof(double))); + DatabaseCreationDate = Storage.Types.JetTypeCodec.TryFromOaDate(days, out DateTime created) + ? created + : throw new InvalidDataException($"Page 0's creation date ({days}) is not a valid OLE Automation date."); + } + + /// Decodes one of page 0's global usage-map pointers: a record pointer under the header mask. + internal static (int Row, int Page) ReadMapPointer(ReadOnlySpan page, int offset, Formats.JetFormatBase format) + { + Span pointer = stackalloc byte[PageBuffer.RecordPointerSize]; + ReadMasked(page, offset, pointer, format); + return PageBuffer.ReadRecordPointer(pointer, 0); + } + + /// Reads 's length in bytes of the masked header from page offset + /// , removing the fixed header mask + /// (). + internal static void ReadMasked(ReadOnlySpan page0, int offset, Span clear, Formats.JetFormatBase format) + { + ReadOnlySpan mask = format.PageZeroHeaderMask[(offset - format.PageZeroHeaderMaskStart)..]; + for (int i = 0; i < clear.Length; i++) + clear[i] = (byte)(page0[offset + i] ^ mask[i]); + } + + /// Writes into the masked header at page offset + /// under the fixed header mask — the inverse of . + internal static void WriteMasked(Span page0, int offset, ReadOnlySpan clear, Formats.JetFormatBase format) + { + ReadOnlySpan mask = format.PageZeroHeaderMask[(offset - format.PageZeroHeaderMaskStart)..]; + for (int i = 0; i < clear.Length; i++) + page0[offset + i] = (byte)(clear[i] ^ mask[i]); + } + + /// The database (encryption) key at ; nonzero + /// when the pages are encrypted. + internal static int ReadDatabaseKey(ReadOnlySpan page0, Formats.JetFormatBase format) + { + Span key = stackalloc byte[sizeof(int)]; + ReadMasked(page0, format.DatabaseKeyOffset, key, format); + return BinaryPrimitives.ReadInt32LittleEndian(key); } - /// Decodes one of page 0's global usage-map pointers: a masked little-endian word whose low byte is - /// the record's row and whose upper three bytes are its page. - internal static (int Row, int Page) ReadMapPointer(ReadOnlySpan page, int offset) + /// The declared length of the EncryptionInfo descriptor at + /// , from the cleartext field at + /// ; 0 when there is none. Unbounded — it comes out of + /// the file, and each caller checks it against what it is about to touch. + internal static int ReadEncryptionInfoLength(ReadOnlySpan page0, Formats.JetFormatBase format) => + BinaryPrimitives.ReadUInt16LittleEndian(page0.Slice(format.EncryptionInfoLengthOffset, sizeof(ushort))); + + /// Writes the database key — the inverse of . + internal static void WriteDatabaseKey(Span page0, int key, Formats.JetFormatBase format) { - ReadOnlySpan mask = Formats.JetFormatBase.PageZeroHeaderMask; - int start = Formats.JetFormatBase.PageZeroHeaderMaskStart; - uint value = 0; - for (int i = 0; i < 4; i++) - value |= (uint)(page[offset + i] ^ mask[offset - start + i]) << (8 * i); - return ((int)(value & 0xFF), (int)(value >> 8)); + Span clear = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(clear, key); + WriteMasked(page0, format.DatabaseKeyOffset, clear, format); } - /// XOR-de-obfuscates the page-0 header region into , whose length - /// equals the mask length; clear[i] corresponds to page offset - /// + i. - internal static void Demask(ReadOnlySpan page, Span clear) + /// Writes the password field: zero-padded to the field's size, XOR'd with its + /// creation-date mask (, so the creation date must already be in place), under + /// the header mask. An .mdb's value is its Jet password, UTF-16LE — empty when it has none; an + /// .accdb's is the low byte of its database key, repeated across the field (zero when unencrypted). + internal static void WritePassword(Span page0, ReadOnlySpan value, Formats.JetFormatBase format) { - ReadOnlySpan mask = Formats.JetFormatBase.PageZeroHeaderMask; - int start = Formats.JetFormatBase.PageZeroHeaderMaskStart; - for (int i = 0; i < mask.Length; i++) - clear[i] = (byte)(page[start + i] ^ mask[i]); + Span creationDate = stackalloc byte[sizeof(double)]; + ReadMasked(page0, format.CreationDateOffset, creationDate, format); + + Span field = stackalloc byte[format.PasswordSize]; + field.Clear(); + value.CopyTo(field); + XorPasswordDateMask(field, creationDate); + WriteMasked(page0, format.PasswordOffset, field, format); + } + + /// XORs the password field's own mask over , in either direction: the + /// integer part of the creation date (, the clear 8-byte double), as a + /// 4-byte little-endian value, cycled across the field. It lies under the header mask as well. + internal static void XorPasswordDateMask(Span field, ReadOnlySpan creationDate) + { + Span dateMask = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(dateMask, (int)BinaryPrimitives.ReadDoubleLittleEndian(creationDate)); + for (int i = 0; i < field.Length; i++) + field[i] ^= dateMask[i % dateMask.Length]; + } + // The engine build LibRed stamps at page-0 EngineBuildOffset. A file keeps whatever its creator wrote, so this + // is the creator's choice, not part of the format: a Jet 4 .mdb gets Jet 4.0.9801, what the current msjet40.dll + // (x86 only) stamps on anything created through the Jet OLE DB 4.0 provider, and an ACCDB gets ACE 12.0.4518 + // (Office 2007 RTM), which every ACE version stamps. + private const int Jet4EngineBuild = 0x2649; + private const int AceEngineBuild = 0x11A6; + + /// The minor byte ACE writes when it CREATES a database of : 0x01 + /// for the 2010 format, 0x00 for every other. Like the build, the creator's choice rather than the + /// format's: a version raise writes 0x00 whatever the target, so a 2007 file raised to 2010 does not + /// carry the 0x01 a created one does. + private static byte CreatedMinorVersion(byte version) => + (byte)(version == (byte)Formats.JetVersion.Version14_2010 ? 0x01 : 0x00); + + /// + /// Synthesises page 0 (the database definition page) — the exact inverse of + /// . Byte-for-byte identical to a real empty file's + /// page 0 for the same parameters (verified against Access-created files). + /// + /// Format version byte (e.g. 0x02 = ACE 12 / Access 2007). + /// true for the ACCDB identifier, false for the MDB (Jet) identifier. + /// ANSI code page of the collation's language (1252 for en-US, 0 for a language + /// with none) — see . + /// The database's default collation — its LCID and sort-order version + /// (1033 / version 0 is General Legacy en-US). + /// Creation timestamp as an OLE-automation date (days since 1899-12-30), passed + /// as the raw double so the exact millisecond-precise bit pattern is preserved — the file's SIDs are masked + /// with a keystream folded from those bits and the rest of the header (page-00 §2.3). + /// The page holding the global usage maps: free pages in row 0, released pages in + /// row 1. + /// The MSysObjects TDEF page. + /// The MSysACEs TDEF page. + /// The MSysQueries TDEF page. + /// The MSysRelationships TDEF page. + /// The MSysAccounts TDEF page of a workgroup file; zero for a database, + /// which has no such table. + /// The MSysGroups TDEF page of a workgroup file; zero for a database. + public static byte[] Build( + byte version, bool isAccdb, int codePage, Collation collation, double creationDays, + int globalMapPage, int objectsPage, int acesPage, int queriesPage, int relationshipsPage, + int accountsPage, int groupsPage) + { + JetFormatBase format = JetFormatBase.FromVersionByte(version); + var page = new byte[format.PageSize]; + + // --- Pre-mask region (0x00..0x17, cleartext) --- + PageHeader.WriteType(page, PageType.DatabaseDefinition); + string id = isAccdb ? JetFormatBase.AceIdentifier : JetFormatBase.JetIdentifier; + Encoding.ASCII.GetBytes(id).CopyTo(page, JetFormatBase.FormatIdentifierOffset); // 0x04, 15 bytes; 0x13 stays NUL + format.WriteVersion(page, version, CreatedMinorVersion(version)); + + // --- Masked header (0x18..0x97): build the clear image, then XOR the fixed mask over it. --- + int b = format.PageZeroHeaderMaskStart; + Span clear = stackalloc byte[format.PageZeroHeaderMaskLength]; + + // [row][page] pointers to the global usage maps — free pages (row 0), released pages (row 1). + PageBuffer.WriteRecordPointer(clear, format.FreePagesMapPointerOffset - b, row: 0, globalMapPage); + PageBuffer.WriteRecordPointer(clear, format.ReleasedPagesMapPointerOffset - b, row: 1, globalMapPage); + // System-table bootstrap pointers = MSysObjects/ACEs/Queries/Relationships/Accounts/Groups pages. + BinaryPrimitives.WriteInt32LittleEndian(clear[(format.CatalogRootPointerOffset - b)..], objectsPage); + BinaryPrimitives.WriteInt32LittleEndian(clear[(format.AcesRootPointerOffset - b)..], acesPage); + BinaryPrimitives.WriteInt32LittleEndian(clear[(format.QueriesRootPointerOffset - b)..], queriesPage); + BinaryPrimitives.WriteInt32LittleEndian(clear[(format.RelationshipsRootPointerOffset - b)..], relationshipsPage); + BinaryPrimitives.WriteInt32LittleEndian(clear[(format.AccountsRootPointerOffset - b)..], accountsPage); + BinaryPrimitives.WriteInt32LittleEndian(clear[(format.GroupsRootPointerOffset - b)..], groupsPage); + BinaryPrimitives.WriteUInt16LittleEndian(clear[(format.CodePageOffset - b)..], (ushort)codePage); // 0x3C + // 0x3E database key = 0 (unencrypted); leave clear zero. + // 0x72..0x79 creation date (OLE double). + BinaryPrimitives.WriteDoubleLittleEndian(clear.Slice(format.CreationDateOffset - b, sizeof(double)), creationDays); + // The creating engine's build number. + BinaryPrimitives.WriteInt32LittleEndian(clear[(format.EngineBuildOffset - b)..], + format.IsAccdb ? AceEngineBuild : Jet4EngineBuild); + // 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[(format.CollationSortOrderOffset - b)..], (ushort)collation.Order); + clear[format.CollationSortIdOffset - b] = collation.SortId; + clear[format.CollationVersionOffset - b] = collation.Version; + + WriteMasked(page, b, clear, format); + // 0x42..0x69 password, empty: masked by its creation date as any password is, so even the empty one is + // not zero on disk. + WritePassword(page, [], format); + + // --- Post-mask tail (cleartext) --- + BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.PageZeroConstantOffset), format.PageZeroConstant); + Encoding.ASCII.GetBytes(JetFormatBase.Jet40EngineVersion).CopyTo(page, JetFormatBase.EngineVersionOffset); + + // User commit-byte table: 2 bytes per user to the end of the header page — Jet 3.x's 0x600 commit region + // relocated to the end of the 4 KB page (see the Jet locking white paper). Each pair is a per-user + // commit/lock status. A fresh file must seed every slot to the neutral idle value 00 01 — NOT 00 00, + // which Jet reads as "mid-write to disk"; with no matching .ldb user lock that reads as a + // suspect/corrupt database and forces a repair before Access will open it. + for (int i = format.CommitByteTableOffset; i < page.Length; i++) page[i] = (byte)(i & 1); + + return page; } - // The range DateTime.FromOADate accepts, checked before the call so a corrupt page 0 reports as corruption - // rather than as an argument error. Its own documented bounds: 0100-01-01 through 9999-12-31. - private const double MinOleAutomationDate = -657435.0; - private const double MaxOleAutomationDate = 2958466.0; } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Pages/Page.cs b/src/LibRed/LibRed.Core/Pages/Page.cs index 654c36fe..59b7f6dc 100644 --- a/src/LibRed/LibRed.Core/Pages/Page.cs +++ b/src/LibRed/LibRed.Core/Pages/Page.cs @@ -16,5 +16,5 @@ public abstract class Page public abstract PageType Type { get; } /// Decodes this page's fields from the supplied buffer using version-specific offsets. - public abstract void Read(PageBuffer buffer, JetFormatBase format); + internal abstract void Read(PageBuffer buffer, JetFormatBase format); } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs deleted file mode 100644 index 25fbca79..00000000 --- a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs +++ /dev/null @@ -1,451 +0,0 @@ -using LibRed.Catalog; -using LibRed.Formats; -using LibRed.IO; -using System.Text; - -namespace LibRed.Pages; - -/// -/// A table definition (TDEF) page: row count, table type, and the column descriptors -/// and names. Verified against the Jet 4 / ACE layout. May be continued across pages -/// for wide tables (see ). -/// -public sealed class TableDefinitionPage : Page -{ - private const int MaxColumnsPerTable = 255; - private const int MaxIndexesPerTable = 32; - private const int MaxNameBytes = JetName.MaxLength * 2; - private static readonly Encoding StrictUnicode = new UnicodeEncoding( - bigEndian: false, byteOrderMark: false, throwOnInvalidBytes: true); - private readonly List _columns = []; - - public override PageType Type => PageType.TableDefinition; - - public int NextDefinitionPage { get; private set; } - public int RowCount { get; private set; } - - /// The complex-type AutoNumber high-water (header 0x1C) — the next id for a complex - /// (multi-value/attachment) column. Read and carried for faithful round-trip; 0 for every table without - /// such a column (LibRed neither creates nor consumes complex columns). - public int ComplexAutoNumber { get; private set; } - - public TableType TableType { get; private set; } - - /// The column-id high-water (header 0x29) — how many ids the table has handed out over its - /// lifetime, which never decrements. It is what sizes a row's leading count and null bitmap, so it differs - /// from for every table that has dropped a column or burned an id on a retype. - public int ColumnIdHighWater { get; private set; } - - public int VariableColumnCount { get; private set; } - public int ColumnCount { get; private set; } - public int LogicalIndexCount { get; private set; } - public int IndexCount { get; private set; } - - public IReadOnlyList Columns => _columns; - - private readonly List _indexes = []; - public IReadOnlyList Indexes => _indexes; - - private readonly List _logicalIndexes = []; - /// Every logical index, in the order the TDEF lists them — including the relationship names that - /// share a real index with a named one, which keeps only one of. - public IReadOnlyList LogicalIndexes => _logicalIndexes; - - private readonly Dictionary _longValueOwnedMaps = []; - /// Per long-value (memo/OLE) column id → its owned-pages usage-map pointer (record row + - /// page), from the §3.3.2 list after the index names. Used to record a newly allocated LVAL page. - public IReadOnlyDictionary LongValueOwnedMaps => _longValueOwnedMaps; - - private readonly Dictionary _longValueFreeMaps = []; - /// Per long-value column id → its free-pages usage-map pointer (LVAL pages with spare room). - public IReadOnlyDictionary LongValueFreeMaps => _longValueFreeMaps; - - // Index structures (Jet 4 / ACE) follow the column names, in this order: - // IndexCount (0x33) data blocks : 52 bytes each — columns, flags, root page - // LogicalIndexCount (0x2F) info blocks : 28 bytes each — links a name to a data block - // LogicalIndexCount names : 2-byte length + UTF-16 - // A logical index may be a relationship (FK) sharing a data block with a real index. - // Index-block layout and flag values are shared with the writers via IndexBlockFormat / IndexFlags. - - /// - /// Reads a table definition starting at , transparently - /// stitching continuation pages (wide tables whose definition spans multiple pages) - /// into one contiguous buffer before parsing. - /// - public void Read(PageChannel channel, int page) - { - (PageBuffer buffer, _) = TdefChainReader.Read(channel, page); - Read(buffer, channel.Format); - } - - public override void Read(PageBuffer buffer, JetFormatBase format) - { - if (buffer.Length < format.TdefRealIndexBlockOffset) - throw new InvalidDataException( - $"TDEF buffer is {buffer.Length} bytes; the fixed header requires {format.TdefRealIndexBlockOffset}."); - int declaredLength = buffer.ReadInt32(format.TdefLengthOffset); - if (declaredLength < format.TdefRealIndexBlockOffset || declaredLength > buffer.Length) - throw new InvalidDataException( - $"TDEF declares length {declaredLength}, outside the available {buffer.Length}-byte buffer."); - if (declaredLength != buffer.Length) - buffer = new PageBuffer(buffer.Data[..declaredLength], buffer.PageNumber); - - PageNumber = buffer.PageNumber; - - NextDefinitionPage = buffer.ReadInt32(format.TdefNextPageOffset); - RowCount = buffer.ReadInt32(format.TdefRowCountOffset); - ComplexAutoNumber = buffer.ReadInt32(format.TdefComplexAutoNumberOffset); - TableType = (TableType)buffer.ReadByte(format.TdefTableTypeOffset); - VariableColumnCount = buffer.ReadUInt16(format.TdefVariableColumnsOffset); - ColumnIdHighWater = buffer.ReadUInt16(format.TdefMaxColumnsOffset); - ColumnCount = buffer.ReadUInt16(format.TdefColumnCountOffset); - LogicalIndexCount = buffer.ReadInt32(format.TdefLogicalIndexCountOffset); - IndexCount = buffer.ReadInt32(format.TdefIndexCountOffset); - - if (ColumnCount > MaxColumnsPerTable) - throw new InvalidDataException($"TDEF declares {ColumnCount} columns; a table can have at most {MaxColumnsPerTable}."); - if (VariableColumnCount > MaxColumnsPerTable) - throw new InvalidDataException( - $"TDEF declares a variable-column high-water of {VariableColumnCount}; the maximum is {MaxColumnsPerTable}."); - if (IndexCount is < 0 or > MaxIndexesPerTable) - throw new InvalidDataException($"TDEF declares {IndexCount} real indexes; the valid range is 0 through {MaxIndexesPerTable}."); - // Capped at 32 exactly as IndexCount is, and this is the check that matters: a table gains a logical - // block per INCOMING relationship without gaining a data block, so it overruns here while 0x33 stays - // legal. Previously only the sign was checked, which let a file written past the limit read back as - // sound - the one shape where LibRed produces a database Access reports as an unrecognized format - // while seeing nothing wrong with it itself. - if (LogicalIndexCount is < 0 or > MaxIndexesPerTable) - throw new InvalidDataException( - $"TDEF declares {LogicalIndexCount} logical indexes; the valid range is 0 through {MaxIndexesPerTable}."); - - // The column descriptors follow a per-index block sized by the REAL index count at - // 0x33 (IndexCount) — NOT the logical count at 0x2F (LogicalIndexCount). The two are - // equal for MSysObjects but differ for user tables (e.g. logical=2, real=1). - // The buffer here may already be a stitched multi-page definition (see Read(channel, page)). - int columnBlock = CheckedRegionEnd( - format.TdefRealIndexBlockOffset, IndexCount, format.RealIndexEntrySize, buffer.Span.Length, "index statistics"); - _ = CheckedRegionEnd( - columnBlock, ColumnCount, format.ColumnDescriptorSize, buffer.Span.Length, "column descriptors"); - int afterNames = ReadColumns(buffer, format, columnBlock); - ReadIndexes(buffer, format, afterNames); - } - - /// - /// Parses the index structures following the column names into s - /// (one per index-data block): columns + sort order, unique/primary flags, root page, - /// and the index name (resolved from the logical-index info blocks). - /// - private void ReadIndexes(PageBuffer buffer, JetFormatBase format, int blockStart) - { - _indexes.Clear(); - var byColumnId = _columns.ToDictionary(c => c.ColumnId); - int infoStart = CheckedRegionEnd( - blockStart, IndexCount, IndexBlockFormat.DataBlockSize, buffer.Span.Length, "index-data blocks"); - _ = CheckedRegionEnd( - infoStart, LogicalIndexCount, IndexBlockFormat.InfoBlockSize, buffer.Span.Length, "logical-index blocks"); - - // 1. Index-data blocks (one IndexDef each): columns, unique flag, root page. - for (int i = 0; i < IndexCount; i++) - { - int block = blockStart + i * IndexBlockFormat.DataBlockSize; - - // Per-index statistics live in the 12-byte block at TdefRealIndexBlockOffset: - // [+0] total entries (= row count), [+4] unique entry count (cumulative, never - // decremented by Access), [+8] reserved. - int statsBlock = format.TdefRealIndexBlockOffset + i * format.RealIndexEntrySize; - int uniqueEntryCount = buffer.ReadInt32(statsBlock + 4); - - var columns = new List<(ColumnDef Column, bool Ascending)>(); - for (int slot = 0; slot < IndexBlockFormat.MaxColumns; slot++) - { - int entry = block + IndexBlockFormat.ColumnsOffset + slot * IndexBlockFormat.ColumnSlotSize; - short columnId = buffer.ReadInt16(entry); - if (columnId == IndexBlockFormat.ColumnUnused) continue; - // A used slot naming a column this table does not have is corruption, and a silent skip is the - // worst answer: the index reads back over FEWER columns than it was built on, so its keys are - // encoded differently from the ones on its pages and every seek quietly misses. - if (!byColumnId.TryGetValue(columnId, out ColumnDef? column)) - throw new InvalidDataException( - $"Index {i} of the table at page {buffer.PageNumber} names column id {columnId} in slot " - + $"{slot}, which the table does not have."); - columns.Add((column, (buffer.ReadByte(entry + 2) & IndexBlockFormat.ColumnAscending) != 0)); - } - - ushort flags = buffer.ReadUInt16(block + IndexBlockFormat.FlagsOffset); - _indexes.Add(new IndexDef - { - Name = string.Empty, - Columns = columns, - IsUnique = (flags & IndexFlags.Unique) != 0, - IgnoreNulls = (flags & IndexFlags.IgnoreNulls) != 0, - Required = (flags & IndexFlags.Required) != 0, - Flags = flags, - IsPrimaryKey = false, - UniqueEntryCount = uniqueEntryCount, - RootPage = buffer.ReadInt32(block + IndexBlockFormat.RootPageOffset), - RealIndexOrdinal = i, - }); - } - - int afterIndexNames = ResolveIndexNames(buffer, infoStart); - ReadLongValueMaps(buffer, afterIndexNames); - } - - /// Bounds one variable-length TDEF region against the assembled definition, in long so a - /// file-sourced count cannot overflow the multiply back into range. Shared with IndexWriter, which - /// walks the very same regions to reach an index-data block: an unchecked walk there lands the write on - /// the wrong block rather than throwing. - internal static int CheckedRegionEnd( - int start, int count, int itemSize, int bufferLength, string section) - { - long end = (long)start + (long)count * itemSize; - if (start < 0 || count < 0 || end < start || end > bufferLength) - throw new InvalidDataException( - $"TDEF {section} extend past the assembled definition ({start} + {count} * {itemSize} > {bufferLength})."); - return (int)end; - } - - /// Parses the §3.3.2 long-value column usage-map list (after the index names): one 10-byte - /// entry {col_num:2, used_ptr:4, free_ptr:4} per memo/OLE column, terminated by col_num 0xFFFF. Each - /// pointer is a 1-byte record row + 3-byte page. Captures the owned- (used-pages) map pointer. - private void ReadLongValueMaps(PageBuffer buffer, int pos) - { - _longValueOwnedMaps.Clear(); - _longValueFreeMaps.Clear(); - var seen = new HashSet(); - while (true) - { - EnsureAvailable(buffer, pos, 2, "long-value map terminator"); - int colNum = buffer.ReadUInt16(pos); - if (colNum == 0xFFFF) - { - pos += 2; - if (pos != buffer.Length) - throw new InvalidDataException( - $"TDEF has {buffer.Length - pos} trailing bytes after the long-value map terminator."); - return; - } - - EnsureAvailable(buffer, pos, 10, "long-value map entry"); - ColumnDef? column = _columns.FirstOrDefault(c => c.ColumnId == colNum); - if (column is null) - throw new InvalidDataException($"TDEF long-value map references unknown column id {colNum}."); - // A calculated column also gets a map, whatever its declared type: its cached result is stored - // as an envelope in the variable section and spills to an LVAL page when it outgrows the row. - // ACE writes one for a calculated Memo declared as Text, which this guard used to reject — - // and because the catalog loads every TDEF, that made the whole database unopenable. - if (column.Type is not (JetDataType.Memo or JetDataType.Ole) && !column.IsCalculated) - throw new InvalidDataException( - $"TDEF long-value map references non-long-value column '{column.Name}' ({column.Type})."); - if (!seen.Add(colNum)) - throw new InvalidDataException($"TDEF contains duplicate long-value map entries for column id {colNum}."); - - column.HasLongValueMap = true; - _longValueOwnedMaps[colNum] = (buffer.ReadByte(pos + 2), buffer.ReadInt24(pos + 3)); - _longValueFreeMaps[colNum] = (buffer.ReadByte(pos + 6), buffer.ReadInt24(pos + 7)); - pos += 10; - } - } - - /// - /// Reads the logical-index info blocks and their names, then attaches each name (and the - /// primary-key flag) to the index-data block it references. A data block may be referenced - /// by several logical indexes (e.g. a relationship plus the real index); the real index's - /// name wins over a foreign-key relationship's. - /// - private int ResolveIndexNames(PageBuffer buffer, int infoStart) - { - int logicalCount = LogicalIndexCount; // 0x2F — the logical-index (slot) count - var info = new (int DataNumber, bool IsRelationship, byte Type, byte FkType, byte UpdateAction, byte DeleteAction)[logicalCount]; - for (int i = 0; i < logicalCount; i++) - { - int block = infoStart + i * IndexBlockFormat.InfoBlockSize; - info[i] = ( - buffer.ReadInt32(block + IndexBlockFormat.InfoDataNumberOffset), - buffer.ReadInt32(block + IndexBlockFormat.InfoFkTablePageOffset) != 0, - buffer.ReadByte(block + IndexBlockFormat.InfoTypeOffset), - buffer.ReadByte(block + IndexBlockFormat.InfoFkTypeOffset), - buffer.ReadByte(block + IndexBlockFormat.InfoUpdateActionOffset), - buffer.ReadByte(block + IndexBlockFormat.InfoDeleteActionOffset)); - } - - int namePos = infoStart + logicalCount * IndexBlockFormat.InfoBlockSize; - var priority = new int[_indexes.Count]; - _logicalIndexes.Clear(); - for (int i = 0; i < logicalCount; i++) - { - (string name, namePos) = ReadName(buffer, namePos, $"logical index {i}"); - - (int dataNumber, bool isRelationship, byte type, byte fkType, byte updateAction, byte deleteAction) = info[i]; - // Every logical index names the data block that holds its columns and root page. One that names a - // block the table does not have is corruption; skipping it used to make the index vanish from the - // table's schema while its B-tree stayed on disk, maintained by nobody. - if (dataNumber < 0 || dataNumber >= _indexes.Count) - throw new InvalidDataException( - $"Logical index '{name}' of the table at page {buffer.PageNumber} names index-data block " - + $"{dataNumber}, and the table has {_indexes.Count}."); - - _logicalIndexes.Add(new LogicalIndexDef(name, dataNumber, isRelationship, - !isRelationship && type == IndexBlockFormat.TypePrimary, fkType, updateAction, deleteAction)); - - // Prefer a real index name over a relationship's; prefer the primary among real ones. - int p = isRelationship ? 1 : type == IndexBlockFormat.TypePrimary ? 3 : 2; - if (p > priority[dataNumber]) - { - priority[dataNumber] = p; - _indexes[dataNumber] = _indexes[dataNumber] with - { - Name = name, - IsPrimaryKey = !isRelationship && type == IndexBlockFormat.TypePrimary, - }; - } - } - - return namePos; - } - - private int ReadColumns(PageBuffer buffer, JetFormatBase format, int columnBlock) - { - _columns.Clear(); - - // Pass 1: fixed-size column descriptors. - var descriptors = new (JetDataType Type, int ColumnId, byte Flags, byte ExtFlags, int FixedOffset, int Length, byte Precision, byte Scale, int VariableIndex, Collation Collation)[ColumnCount]; - var columnIds = new HashSet(); - for (int i = 0; i < ColumnCount; i++) - { - int entry = columnBlock + i * format.ColumnDescriptorSize; - var type = (JetDataType)buffer.ReadByte(entry + format.ColumnTypeOffset); - int columnId = buffer.ReadUInt16(entry + format.ColumnNumberOffset); - if (!Enum.IsDefined(type)) - throw new InvalidDataException($"TDEF column {i} has unknown type code 0x{(byte)type:X2}."); - if (columnId >= MaxColumnsPerTable) - throw new InvalidDataException( - $"TDEF column {i} has id {columnId}; valid ids are 0 through {MaxColumnsPerTable - 1}."); - if (!columnIds.Add(columnId)) - throw new InvalidDataException($"TDEF contains duplicate column id {columnId}."); - - // Bytes 0x0B/0x0C are precision/scale for a Decimal/Numeric column, the MSysComplexColumns key for a - // Complex one (page-02b §3.4), and the text-collation LCID for everything else; 0x0D is the - // collation's sort-order version. Read whichever applies — a complex column has no collation to read. - bool numeric = type == JetDataType.FixedPoint; - bool complex = type == JetDataType.Complex; - descriptors[i] = ( - type, - columnId, - buffer.ReadByte(entry + format.ColumnFlagsOffset), - buffer.ReadByte(entry + format.ColumnExtendedFlagsOffset), - buffer.ReadUInt16(entry + format.ColumnFixedOffsetOffset), - buffer.ReadUInt16(entry + format.ColumnLengthOffset), - numeric ? buffer.ReadByte(entry + format.ColumnPrecisionOffset) : (byte)0, - numeric ? buffer.ReadByte(entry + format.ColumnScaleOffset) : (byte)0, - // The variable-table index is **stored** in the descriptor (0x07), not derived. Reading it - // (rather than ranking column ids) is what lets a table with a **dropped column** decode: - // ACE's DROP COLUMN removes a descriptor but does NOT renumber the survivors or rewrite - // rows, so a survivor keeps its original variable index even though ranking would shift it. - buffer.ReadUInt16(entry + format.ColumnVariableIndexOffset), - numeric || complex ? 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), - buffer.ReadByte(entry + format.ColumnCollationVersionOffset), - buffer.ReadByte(entry + format.ColumnCollationSortIdOffset))); - } - - // Pass 2: column names, in the same order, immediately after the descriptor block. - // Each name is a 2-byte (little-endian) byte length followed by UTF-16LE text. - int namePos = columnBlock + ColumnCount * format.ColumnDescriptorSize; - for (int i = 0; i < ColumnCount; i++) - { - (string name, namePos) = ReadName(buffer, namePos, $"column {i}"); - - var d = descriptors[i]; - bool isFixed = (d.Flags & JetFormatBase.ColumnFlagFixedLength) != 0; - if ((!isFixed && d.VariableIndex >= VariableColumnCount) - || (isFixed && d.VariableIndex > VariableColumnCount)) - throw new InvalidDataException( - $"TDEF column '{name}' has variable-table index {d.VariableIndex}, " + - $"outside high-water {VariableColumnCount}."); - _columns.Add(new ColumnDef - { - Name = name, - Type = d.Type, - Index = i, - ColumnId = d.ColumnId, - Length = d.Length, - FixedOffset = d.FixedOffset, - VariableIndex = isFixed ? -1 : d.VariableIndex, - // Byte 7 is stored on fixed columns too (the running count of preceding variable columns). - VariableTableIndex = d.VariableIndex, - IsFixedLength = isFixed, - IsAutoNumber = (d.Flags & JetFormatBase.ColumnFlagAutoNumber) != 0, - // Every documented flag bit is modelled (0x0F: updatable/GUID-autonumber/hyperlink; 0x10: - // compressed-Unicode / calculated). - IsUpdatable = (d.Flags & JetFormatBase.ColumnFlagUpdatable) != 0, - IsGuidAutoNumber = (d.Flags & JetFormatBase.ColumnFlagGuidAutoNumber) != 0, - IsHyperlink = (d.Flags & JetFormatBase.ColumnFlagHyperlink) != 0, - SupportsCompressedUnicode = (d.ExtFlags & JetFormatBase.ColumnExtFlagCompressedUnicode) != 0, - IsCalculated = (d.ExtFlags & JetFormatBase.ColumnExtFlagCalculated) != 0, - Precision = d.Precision, - Scale = d.Scale, - Collation = d.Collation, - // The descriptor's bytes as read, fields LibRed does not model included. - RawDescriptor = buffer.Slice(columnBlock + i * format.ColumnDescriptorSize, format.ColumnDescriptorSize).ToArray(), - }); - } - - // AutoNumber seed/increment from the TDEF header: 0x18 = increment, 0x14 = last-assigned value. On a - // freshly created table the last value is Seed-Increment, so Seed = last + increment (matching what a - // no-insert scaffold reports). - // - // At most one column draws on THAT pair — but it is not the only counter a table has. A complex column - // carries the very same 0x04 flag and is allocated from 0x1C (ComplexAutoNumber), so a table can hold an - // ordinary counter and any number of complex columns all reading IsAutoNumber (complex1.accdb's Table1 - // has five). Applying the header pair to those too reports a seed and increment that describe a - // different counter, so skip them: their high-water is the table's ComplexAutoNumber. - int increment = buffer.ReadInt32(format.TdefAutoNumberIncrementOffset); - int lastAuto = buffer.ReadInt32(format.TdefLastAutoNumberOffset); - foreach (ColumnDef column in _columns) - if (column.IsAutoNumber && column.Type != JetDataType.Complex) - { - // Zero is not a counter ACE will write: its own DDL refuses COUNTER(seed, 0) with "Invalid - // argument", because every row would take the same id. A table carrying one is damaged, and - // reading it as 1 — which this did — hides that behind a counter that looks ordinary. - if (increment == 0) - throw new InvalidDataException( - $"The table at page {buffer.PageNumber} has AutoNumber column '{column.Name}' and an " - + "increment of 0, which would hand every row the same id."); - column.Increment = increment; - column.Seed = lastAuto + increment; - } - - return namePos; - } - - private static (string Name, int Next) ReadName(PageBuffer buffer, int pos, string kind) - { - EnsureAvailable(buffer, pos, 2, $"{kind} name length"); - int byteLength = buffer.ReadUInt16(pos); - pos += 2; - if (byteLength == 0 || byteLength > MaxNameBytes || (byteLength & 1) != 0) - throw new InvalidDataException( - $"TDEF {kind} name has invalid UTF-16 byte length {byteLength}; expected an even value from 2 through {MaxNameBytes}."); - EnsureAvailable(buffer, pos, byteLength, $"{kind} name"); - try - { - return (StrictUnicode.GetString(buffer.Slice(pos, byteLength)), pos + byteLength); - } - catch (DecoderFallbackException ex) - { - throw new InvalidDataException($"TDEF {kind} name is not valid UTF-16LE.", ex); - } - } - - private static void EnsureAvailable(PageBuffer buffer, int pos, int length, string section) - { - long end = (long)pos + length; - if (pos < 0 || length < 0 || end > buffer.Length) - throw new InvalidDataException( - $"TDEF {section} extends past the declared definition ({pos} + {length} > {buffer.Length})."); - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs b/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs deleted file mode 100644 index 27dba5a6..00000000 --- a/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs +++ /dev/null @@ -1,88 +0,0 @@ -using LibRed.Formats; -using LibRed.IO; - -namespace LibRed.Pages; - -/// Reads one TDEF chain into its absolute logical coordinate space. -internal static class TdefChainReader -{ - // A valid Jet/ACE table is constrained to 255 columns, 32 real indexes, and 64-character names. - // One MiB is deliberately generous while still preventing a hostile 32-bit length from driving - // process-scale allocation. This is an implementation safety budget, not an on-disk field width. - internal const int MaxDefinitionLength = 1024 * 1024; - - internal static (PageBuffer Buffer, IReadOnlyList ContinuationPages) Read( - PageChannel channel, int firstPage) - { - JetFormatBase format = channel.Format; - ValidatePageNumber(channel, firstPage, "TDEF root"); - PageBuffer first = channel.ReadPage(firstPage); - if (PageHeader.ReadType(first.Span) != PageType.TableDefinition) - throw new InvalidDataException( - $"TDEF root page {firstPage} is type 0x{(ushort)PageHeader.ReadType(first.Span):X4}, expected 0x0102."); - - int definitionLength = first.ReadInt32(format.TdefLengthOffset); - if (definitionLength < format.TdefRealIndexBlockOffset || definitionLength > MaxDefinitionLength) - throw new InvalidDataException( - $"TDEF page {firstPage} declares length {definitionLength}; supported validated range is " + - $"{format.TdefRealIndexBlockOffset} through {MaxDefinitionLength} bytes."); - - // The chain holds the definition AND its 8-byte trailing reserve, which follows the last definition byte - // and spills onto a page of its own when it does not fit — so a continuation page can carry no definition - // bytes at all (verified vs ACE: a 4,090-byte definition has a continuation holding two reserve bytes). - int pageSize = format.PageSize; - int bodySize = pageSize - JetFormatBase.TdefContinuationHeaderSize; - int stored = definitionLength + JetFormatBase.TdefContinuationHeaderSize; - int continuationCount = stored <= pageSize - ? 0 - : (stored - pageSize + bodySize - 1) / bodySize; - - int next = first.ReadInt32(format.TdefNextPageOffset); - var continuationPages = new List(continuationCount); - var continuationBuffers = new List(continuationCount); - var visited = new HashSet { firstPage }; - - for (int i = 0; i < continuationCount; i++) - { - if (next == 0) - throw new InvalidDataException( - $"TDEF page {firstPage} ends after {i} continuation pages but its declared length requires {continuationCount}."); - ValidatePageNumber(channel, next, "TDEF continuation"); - if (!visited.Add(next)) - throw new InvalidDataException($"TDEF page {firstPage} contains a continuation cycle at page {next}."); - - PageBuffer continuation = channel.ReadPage(next); - if (PageHeader.ReadType(continuation.Span) != PageType.TableDefinition) - throw new InvalidDataException( - $"TDEF continuation page {next} is type 0x{(ushort)PageHeader.ReadType(continuation.Span):X4}, expected 0x0102."); - - continuationPages.Add(next); - continuationBuffers.Add(continuation); - next = continuation.ReadInt32(format.TdefNextPageOffset); - } - - if (next != 0) - throw new InvalidDataException( - $"TDEF page {firstPage} has more continuation pages than its declared length permits."); - - var assembled = new byte[definitionLength]; - int written = Math.Min(pageSize, definitionLength); - first.Span[..written].CopyTo(assembled); - foreach (PageBuffer continuation in continuationBuffers) - { - int take = Math.Min(bodySize, definitionLength - written); - continuation.Span.Slice(JetFormatBase.TdefContinuationHeaderSize, take) - .CopyTo(assembled.AsSpan(written)); - written += take; - } - - return (new PageBuffer(assembled, firstPage), continuationPages); - } - - private static void ValidatePageNumber(PageChannel channel, int pageNumber, string role) - { - if (pageNumber <= 0 || pageNumber >= channel.PageCount) - throw new InvalidDataException( - $"{role} page {pageNumber} is outside the database's {channel.PageCount} pages."); - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Pages/TdefRegions.cs b/src/LibRed/LibRed.Core/Pages/TdefRegions.cs deleted file mode 100644 index d4edcb29..00000000 --- a/src/LibRed/LibRed.Core/Pages/TdefRegions.cs +++ /dev/null @@ -1,67 +0,0 @@ -using LibRed.Formats; -using System.Buffers.Binary; - -namespace LibRed.Pages; - -/// -/// Where each variable-length region of a table definition begins (page-02a §3.3). Only the first has a fixed -/// offset; every later one is found by stepping over all of its predecessors, including the column names, -/// whose lengths come out of the file. Anything reaching into a TDEF it did not itself parse — adding an index -/// or a relationship block, repointing a B-tree root, splitting the definition apart to rewrite it — needs the -/// same walk, so they share this one. -/// -/// Header 0x2D: descriptors and names alike. -/// Header 0x33: the real index count, sizing both the statistics and the -/// index-data blocks. -/// Header 0x2F: info blocks and index names. -/// Start of the per-index statistics — the one fixed offset, 0x3F. -/// Start of the 25-byte column descriptors; the names follow them. -/// Start of the 52-byte index-data blocks, past the column names. -/// Start of the 28-byte logical index-info blocks. -/// Start of the index names, and so the end of the info blocks. -internal readonly record struct TdefRegions( - int ColumnCount, - int DataCount, - int LogicalCount, - int Stats, - int ColumnDescriptors, - int DataBlocks, - int InfoBlocks, - int IndexNames) -{ - /// Walks — the whole definition, continuation pages already stitched in, - /// since the regions run straight across a page boundary. - /// - /// Every region is bounded as it is crossed. The counts and name lengths are all file-sourced, and - /// unchecked they carry the walk past the buffer — or, when a multiply overflows, back inside it at the - /// wrong place, which lands a write on some other index's block with no error raised at all. - /// - public static TdefRegions Of(ReadOnlySpan tdef, JetFormatBase format) - { - int columnCount = BinaryPrimitives.ReadUInt16LittleEndian(tdef.Slice(format.TdefColumnCountOffset, 2)); - int dataCount = BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(format.TdefIndexCountOffset, 4)); - int logicalCount = BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(format.TdefLogicalIndexCountOffset, 4)); - - int stats = format.TdefRealIndexBlockOffset; - int descriptors = TableDefinitionPage.CheckedRegionEnd( - stats, dataCount, format.RealIndexEntrySize, tdef.Length, "index statistics"); - int pos = TableDefinitionPage.CheckedRegionEnd( - descriptors, columnCount, format.ColumnDescriptorSize, tdef.Length, "column descriptors"); - - for (int i = 0; i < columnCount; i++) - { - int text = TableDefinitionPage.CheckedRegionEnd(pos, 1, 2, tdef.Length, "a column-name length"); - pos = TableDefinitionPage.CheckedRegionEnd( - text, 1, BinaryPrimitives.ReadUInt16LittleEndian(tdef.Slice(pos, 2)), tdef.Length, "a column name"); - } - - int dataBlocks = pos; - int infoBlocks = TableDefinitionPage.CheckedRegionEnd( - dataBlocks, dataCount, IndexBlockFormat.DataBlockSize, tdef.Length, "index-data blocks"); - int indexNames = TableDefinitionPage.CheckedRegionEnd( - infoBlocks, logicalCount, IndexBlockFormat.InfoBlockSize, tdef.Length, "logical-index blocks"); - - return new TdefRegions( - columnCount, dataCount, logicalCount, stats, descriptors, dataBlocks, infoBlocks, indexNames); - } -} diff --git a/src/LibRed/LibRed.Core/Storage/BitmapBits.cs b/src/LibRed/LibRed.Core/Storage/BitmapBits.cs new file mode 100644 index 00000000..f37ce293 --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/BitmapBits.cs @@ -0,0 +1,79 @@ +using System.Numerics; + +namespace LibRed.Storage; + +/// +/// The bits of the format's bitmaps, which all number their bits the same way — bit i of byte n is +/// bit n×8 + i, least significant first: a usage map's (bit k marking page basePage + k, +/// page-05 §9), a row's null bitmap (bit k for column id k, page-01 §5) and an index page's +/// entry-position mask (bit k ending an entry at offset k, page-03-04 §10.2). Every read and write of +/// a bit in any of them comes through here; the caller passes the bitmap without whatever header carried it. +/// +internal static class BitmapBits +{ + /// The bytes a bitmap of bits takes. + public static int ByteCount(int bits) => (bits + 7) >> 3; + + /// Whether is set. + public static bool Get(ReadOnlySpan bitmap, int bit) => (bitmap[bit >> 3] & (1 << (bit & 7))) != 0; + + /// Sets or clears . + public static void Set(Span bitmap, int bit, bool value) + { + byte mask = (byte)(1 << (bit & 7)); + if (value) bitmap[bit >> 3] |= mask; + else bitmap[bit >> 3] &= (byte)~mask; + } + + /// The lowest set bit at or above , or -1 when there is none. + public static int NextSetBit(ReadOnlySpan bitmap, int from) + { + for (int i = from >> 3; i < bitmap.Length; i++) + { + int bits = i == from >> 3 ? bitmap[i] & (0xFF << (from & 7)) : bitmap[i]; + if (bits != 0) return i * 8 + BitOperations.TrailingZeroCount(bits); + } + return -1; + } + + /// The highest set bit, or -1 when there is none. + public static int LastSetBit(ReadOnlySpan bitmap) + { + for (int i = bitmap.Length - 1; i >= 0; i--) + if (bitmap[i] != 0) return i * 8 + 31 - BitOperations.LeadingZeroCount((uint)bitmap[i]); + return -1; + } + + /// + /// Appends the pages a usage-map marks to . + /// + /// Collects the page numbers, in ascending order. + /// The map's bits, without whatever header carried them. + /// The page bit 0 stands for. + /// + /// The file's page count, to reject a bit naming a page outside it — or null to keep every bit. + /// Which of those is right is a property of the caller, not an oversight. A map read feeds + /// its numbers straight into page reads, so one outside the file is corruption and says so. A map being + /// rewritten must keep bits it cannot currently represent: a free map's window slides with the + /// append tail and can sit above a page it still has to record, so the record widens rather than dropping + /// them (page-05 §9). Rejecting there is not theoretical — it failed an ordinary DROP TABLE on + /// complex1.accdb, whose MSysObjects free map starts at page 2288 while its catalog rows + /// live at page 17. + /// + /// Names the map in the rejection message. + public static void AppendPages( + List pages, ReadOnlySpan bitmap, int basePage, int? rejectBeyond, string what) + { + // The bound is read once by the caller and passed in: this loop runs per set bit on every insert, and + // PageChannel.PageCount used to be a file-length syscall, which made a per-bit test cost a + // non-transactional insert ~1.9x. Nothing here writes, so the count cannot move under it. + for (int bit = NextSetBit(bitmap, 0); bit >= 0; bit = NextSetBit(bitmap, bit + 1)) + { + long page = (long)basePage + bit; + if (rejectBeyond is { } pageCount && (page <= 1 || page >= pageCount)) + throw new InvalidDataException( + $"{what} names page {page}, outside the file's 2..{pageCount - 1} range."); + pages.Add((int)page); + } + } +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/CalculatedValue.cs b/src/LibRed/LibRed.Core/Storage/CalculatedValue.cs index 0de1a0c4..eccaf045 100644 --- a/src/LibRed/LibRed.Core/Storage/CalculatedValue.cs +++ b/src/LibRed/LibRed.Core/Storage/CalculatedValue.cs @@ -1,4 +1,5 @@ using LibRed.Catalog; +using LibRed.Formats; using System.Buffers.Binary; namespace LibRed.Storage; @@ -21,22 +22,23 @@ namespace LibRed.Storage; /// internal static class CalculatedValue { - /// Header bytes before the length: a 4-byte status followed by 12 reserved bytes that are zero - /// in every file measured. - private const int HeaderSize = 16; + /// The status field, at the envelope's start. Zero is the success code; anything else is a VBA + /// runtime error number, and the expression produced no value at all. + internal const int StatusSize = 4; - /// The status field's size. Zero is the success code; anything else is a VBA runtime error - /// number, and the expression produced no value at all. - private const int StatusSize = 4; + /// Header bytes before the length: the status, then 12 reserved bytes that are zero in every file + /// measured. + internal const int HeaderSize = 16; - /// Little-endian payload length. - private const int LengthSize = 4; + /// The little-endian payload length, at ; the payload follows it. + internal const int LengthSize = 4; /// Zero padding after the payload. - private const int PaddingSize = 3; + internal const int PaddingSize = 3; /// The smallest envelope: header, length and padding with an empty payload. - public const int MinimumSize = HeaderSize + LengthSize + PaddingSize; + internal const int MinimumSize = HeaderSize + LengthSize + PaddingSize; + /// Evaluates a calculated column's expression against one row and coerces the result to the /// type its payload is stored in. @@ -83,10 +85,10 @@ public static IReadOnlySet ReferencedIndexes(ColumnDef column, IReadOnlyLis /// Builds the envelope for — the inverse of /// . A Null result is a zero-length payload, not an absent one: the column's /// null-bitmap bit stays set either way (§3.4a). - public static byte[] Encode(ColumnDef column, object? value) + public static byte[] Encode(ColumnDef column, object? value, JetFormatBase format) { JetDataType type = StoredType(column, 0); - byte[] payload = value is null ? [] : EncodePayload(column, type, value); + byte[] payload = value is null ? [] : EncodePayload(column, type, value, format); var envelope = new byte[MinimumSize + payload.Length]; BinaryPrimitives.WriteUInt32LittleEndian(envelope.AsSpan(HeaderSize, LengthSize), (uint)payload.Length); @@ -110,7 +112,7 @@ public static byte[] Encode(ColumnDef column, object? value) /// envelope back under the 64-byte inline limit and so decides whether the value reaches a page at all. /// /// - private static byte[] EncodePayload(ColumnDef column, JetDataType type, object value) + private static byte[] EncodePayload(ColumnDef column, JetDataType type, object value, JetFormatBase format) { var culture = System.Globalization.CultureInfo.InvariantCulture; switch (type) @@ -125,7 +127,7 @@ private static byte[] EncodePayload(ColumnDef column, JetDataType type, object v return Types.JetTypeCodec.TryCompressText(column, text, requireCapableFlag: false) ?? utf16; } default: - return Types.JetTypeCodec.Encode(column, type, value); + return Types.JetTypeCodec.Encode(column, type, value, format); } } diff --git a/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs b/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs deleted file mode 100644 index 19c8f9e6..00000000 --- a/src/LibRed/LibRed.Core/Storage/CatalogWriter.cs +++ /dev/null @@ -1,97 +0,0 @@ -using LibRed.Catalog; -using LibRed.IO; - -namespace LibRed.Storage; - -/// -/// Writes the catalog rows every new object gets whatever it is: its MSysObjects entry, and the -/// MSysACEs rows its container's inheritable grants give it (system-catalog §11). A table, a stored query -/// and a relationship differ only in the type, container and flags they pass. -/// -/// -/// writes the same two tables and does not use this: it is populating them -/// before either has an index or a catalog to resolve names through, and it grants to accounts (Creator, -/// Engine) that no later object's rows name. -/// -internal sealed class CatalogWriter(PageChannel channel, JetCatalog catalog) -{ - /// Inserts the object's MSysObjects row — owner Users, both dates now — maintaining the - /// table's indexes, including the (ParentId, Name) one Access resolves names through. - /// The object's name, unique within its container. - /// A table's is its TDEF page; every other object takes a synthetic negative id. - /// 1 table, 5 query, 8 relationship. - /// The container the object belongs to. - /// Object-class specific; a query's encodes its kind. - /// The object's extended properties, if it has any: a table's per-column - /// DefaultValue/Required and its CHECK constraints all live in this one blob. - public void AddObjectRow( - string name, int objectId, short type, int parentId, int flags, - IReadOnlyList? properties = null) - { - TableDef msysObjects = catalog.RequireTable("MSysObjects"); - DateTime now = DateTime.Now; - var values = new object?[msysObjects.Columns.Count]; - Set(msysObjects, values, "Id", objectId); - Set(msysObjects, values, "ParentId", parentId); - Set(msysObjects, values, "Type", type); - Set(msysObjects, values, "Name", name); - Set(msysObjects, values, "Flags", flags); - Set(msysObjects, values, "Owner", catalog.SecuritySids.Admin); - Set(msysObjects, values, "DateCreate", now); - Set(msysObjects, values, "DateUpdate", now); - - var inserter = new RowInserter(channel, msysObjects); - if (properties is { Count: > 0 }) - { - // Stored as any long value is: inline up to 64 bytes, else packed onto a shared LvProp page as - // Access does. - byte[] reference = inserter.StorePackedLongValue( - msysObjects.RequireColumn("LvProp").ColumnId, PropertyBlob.Write(properties)); - Set(msysObjects, values, "LvProp", new LongValueDescriptor(reference)); - } - - inserter.Insert(values, updateIndexes: true); - } - - /// Inserts the object's MSysACEs rows, maintaining the ObjectId index so Access's security - /// check finds them — without them it warns about permissions on opening the object. - /// The grants are the ones the object's container passes down, derived as ACE derives them - /// (verified): the Creator account's inheritable grant becomes the owner's — the admin user, as whom LibRed - /// creates every object — and every other inheritable grant is copied for its own account, OR'd into the - /// owner's row when it names the owner's account. The owner's row comes first, the rest in the container's - /// order. So the masks follow the database: a table's owner gets 0xF00FE where the Tables container grants the - /// Creator that alone, and 0xFFEFF where it also grants admin 0xFFEFF, as Northwind's does. - public void AddPermissionRows(int objectId, int containerId) - { - TableDef msysAces = catalog.RequireTable("MSysACEs"); - int idIndex = msysAces.RequireColumn("ObjectId").Index, sidIndex = msysAces.RequireColumn("SID").Index; - int acmIndex = msysAces.RequireColumn("ACM").Index, inheritIndex = msysAces.RequireColumn("FInheritable").Index; - (byte[] owner, _, byte[] creator) = catalog.SecuritySids; - - var grants = new List<(byte[] Sid, int Acm)>(); - var aces = new Table(channel, msysAces); - foreach (object?[] row in aces.Rows(aces.DecodeOnly([idIndex, sidIndex, acmIndex, inheritIndex]))) - { - if (row[idIndex] is not int id || id != containerId || row[inheritIndex] is not true) continue; - byte[] sid = (byte[])row[sidIndex]!; - if (sid.AsSpan().SequenceEqual(creator)) sid = owner; - int at = grants.FindIndex(g => g.Sid.AsSpan().SequenceEqual(sid)); - if (at < 0) grants.Add((sid, (int)row[acmIndex]!)); - else grants[at] = (grants[at].Sid, grants[at].Acm | (int)row[acmIndex]!); - } - - foreach ((byte[] sid, int acm) in grants.OrderBy(g => g.Sid.AsSpan().SequenceEqual(owner) ? 0 : 1)) - { - var values = new object?[msysAces.Columns.Count]; - Set(msysAces, values, "ACM", acm); - Set(msysAces, values, "FInheritable", false); - Set(msysAces, values, "ObjectId", objectId); - Set(msysAces, values, "SID", sid); - new RowInserter(channel, msysAces).Insert(values, updateIndexes: true); - } - } - - /// Puts in the row slot occupies. - public static void Set(TableDef table, object?[] values, string column, object? value) => - values[table.RequireColumn(column).Index] = value; -} diff --git a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs deleted file mode 100644 index c7f11349..00000000 --- a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs +++ /dev/null @@ -1,535 +0,0 @@ -using LibRed.Catalog; -using LibRed.Crypto; -using LibRed.Formats; -using LibRed.Pages; -using System.Buffers.Binary; -using System.Globalization; -using System.Text; - -namespace LibRed.Storage; - -/// -/// Builds the pages of a brand-new, empty Jet/ACE database from scratch — the native, cross-platform -/// replacement for the DAO/ADOX file creator. Currently synthesises page 0 (the database definition -/// page); the system catalog follows. -/// -public static class DatabaseCreator -{ - private static readonly DateTime OleEpoch = new(1899, 12, 30); - - /// - /// Synthesises page 0 (the database definition page) — the exact inverse of - /// . Byte-for-byte identical to a real empty file's - /// page 0 for the same parameters (verified against Access-created files). - /// - /// Format version byte (e.g. 0x02 = ACE 12 / Access 2007). - /// true for the ACCDB identifier, false for the MDB (Jet) identifier. - /// ANSI code page of the collation's language (1252 for en-US, 0 for a language - /// with none) — see . - /// The database's default collation — its LCID and sort-order version - /// (1033 / version 0 is General Legacy en-US). - /// Creation timestamp as an OLE-automation date (days since 1899-12-30), passed - /// as the raw double so the exact millisecond-precise bit pattern is preserved — the file's SIDs are masked - /// with a keystream folded from those bits and the rest of the header (page-00 §2.3). - public static byte[] BuildDefinitionPage( - byte version, bool isAccdb, int codePage, Collation collation, double creationDays) - { - var page = new byte[4096]; - - // --- Pre-mask region (0x00..0x17, cleartext) --- - page[0x00] = 0x00; // page type - page[0x01] = 0x01; // observed constant 01 00 00 - string id = isAccdb ? JetFormatBase.AceIdentifier : JetFormatBase.JetIdentifier; - Encoding.ASCII.GetBytes(id).CopyTo(page, JetFormatBase.FormatIdentifierOffset); // 0x04, 15 bytes; 0x13 stays NUL - page[JetFormatBase.VersionOffset] = version; // 0x14 - page[JetFormatBase.MinorVersionOffset] = JetFormatBase.CreatedMinorVersion(version); // 0x15 - - // --- Masked header (0x18..0x97): build the clear image, then XOR the fixed mask over it. --- - int b = JetFormatBase.PageZeroHeaderMaskStart; - Span clear = stackalloc byte[JetFormatBase.PageZeroHeaderMask.Length]; - - // 0x18/0x1C: [row][page] pointers to the global usage maps on page 1 — free pages (row 0), released pages (row 1). - BinaryPrimitives.WriteInt32LittleEndian(clear[(JetFormatBase.FreePagesMapPointerOffset - b)..], 0x00000100); // free map: page 1, row 0 - BinaryPrimitives.WriteInt32LittleEndian(clear[(JetFormatBase.ReleasedPagesMapPointerOffset - b)..], 0x00000101); // released map: page 1, row 1 - // 0x20..0x2C: system-catalog bootstrap pointers = MSysObjects/ACEs/Queries/Relationships pages. - BinaryPrimitives.WriteInt32LittleEndian(clear[(0x20 - b)..], 2); - BinaryPrimitives.WriteInt32LittleEndian(clear[(0x24 - b)..], 3); - BinaryPrimitives.WriteInt32LittleEndian(clear[(0x28 - b)..], 4); - BinaryPrimitives.WriteInt32LittleEndian(clear[(0x2C - b)..], 5); - BinaryPrimitives.WriteUInt16LittleEndian(clear[(JetFormatBase.CodePageOffset - b)..], (ushort)codePage); // 0x3C - // 0x3E database key = 0 (unencrypted); leave clear zero. - // 0x42..0x69 password (empty): on disk the field is additionally masked by a 4-byte value derived - // from the creation date, so the clear image (pre-header-mask) of an empty password is that value - // repeated — reproduce it exactly. - double days = creationDays; - Span dateMask = stackalloc byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(dateMask, (int)days); - for (int i = 0; i < 40; i++) - clear[JetFormatBase.PasswordOffset - b + i] = dateMask[i % 4]; - // 0x6A fixed sentinel constant. - BinaryPrimitives.WriteInt32LittleEndian(clear[(0x6A - b)..], 0x000011A6); - // 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); - - ReadOnlySpan mask = JetFormatBase.PageZeroHeaderMask; - for (int i = 0; i < mask.Length; i++) - page[b + i] = (byte)(clear[i] ^ mask[i]); - - // --- Post-mask tail (cleartext) --- - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(0x98), 0x00000654); // fixed constant - Encoding.ASCII.GetBytes("4.0").CopyTo(page, 0x9C); // engine version string - - // User commit-byte table (0xE00–0xFFF): 256 users × 2 bytes at the end of the header page — Jet 3.x's - // 0x600 commit region relocated to the end of the 4 KB page (see the Jet locking white paper). Each - // pair is a per-user commit/lock status. A fresh file must seed every slot to the neutral idle value - // 00 01 — NOT 00 00, which Jet reads as "mid-write to disk"; with no matching .ldb user lock that - // reads as a suspect/corrupt database and forces a repair before Access will open it. - for (int i = 0xE00; i < 0x1000; i++) page[i] = (byte)(i & 1); - - return page; - } - - // System-column flag bits: Sys = 0x10 (system-catalog column), SysSid = 0x10|0x20 (also a security id). - private const byte Sys = 0x10, SysSid = 0x30; - - // Extended flag 0x10: an attachment value column, in the attachment template as in every attachment flat table. - private const byte AttachmentValue = 0x10; - - // MSysObjects — the system catalog. Declared in the real physical (alphabetical) descriptor order with - // explicit canonical ColumnIds and system flags, exactly as an Access-created file stores it. - private static readonly ColumnSpec[] MSysObjectsColumns = - [ - new("Connect", JetDataType.Memo, 0, false, ColumnId: 9, SystemFlags: Sys), - new("Database", JetDataType.Memo, 0, false, ColumnId: 8, SystemFlags: Sys), - new("DateCreate", JetDataType.DateTime, 8, true, ColumnId: 4, SystemFlags: Sys), - new("DateUpdate", JetDataType.DateTime, 8, true, ColumnId: 5, SystemFlags: Sys), - new("Flags", JetDataType.Int32, 4, true, ColumnId: 7, SystemFlags: Sys), - new("ForeignName", JetDataType.Text, 510, false, ColumnId: 10, SystemFlags: Sys), - new("Id", JetDataType.Int32, 4, true, ColumnId: 0, SystemFlags: Sys), - new("Lv", JetDataType.Ole, 0, false, ColumnId: 13, SystemFlags: Sys), - new("LvExtra", JetDataType.Ole, 0, false, ColumnId: 16, SystemFlags: Sys), - new("LvModule", JetDataType.Ole, 0, false, ColumnId: 15, SystemFlags: Sys), - new("LvProp", JetDataType.Ole, 0, false, ColumnId: 14, SystemFlags: Sys), - new("Name", JetDataType.Text, 510, false, ColumnId: 2, SystemFlags: Sys), - new("Owner", JetDataType.Binary, 510, false, ColumnId: 6, SystemFlags: SysSid), - new("ParentId", JetDataType.Int32, 4, true, ColumnId: 1, SystemFlags: Sys), - new("RmtInfoLong", JetDataType.Ole, 0, false, ColumnId: 12, SystemFlags: Sys), - new("RmtInfoShort", JetDataType.Binary, 510, false, ColumnId: 11, SystemFlags: Sys), - new("Type", JetDataType.Int16, 2, true, ColumnId: 3, SystemFlags: Sys), - ]; - - // MSysACEs — per-object access-control rows. Real physical order + ColumnIds (ObjectId at fixed offset 0). - private static readonly ColumnSpec[] MSysAcesColumns = - [ - new("ACM", JetDataType.Int32, 4, true, ColumnId: 2, SystemFlags: Sys), - new("FInheritable", JetDataType.Boolean, 1, true, ColumnId: 3, SystemFlags: Sys), - new("ObjectId", JetDataType.Int32, 4, true, ColumnId: 0, SystemFlags: Sys), - new("SID", JetDataType.Binary, 510, false, ColumnId: 1, SystemFlags: SysSid), - ]; - - // MSysQueries — stored query/view definitions (empty in a fresh database). - private static readonly ColumnSpec[] MSysQueriesColumns = - [ - new("Attribute", JetDataType.Byte, 1, true, ColumnId: 1, SystemFlags: Sys), - new("Expression", JetDataType.Memo, 0, false, ColumnId: 5, SystemFlags: Sys), - new("Flag", JetDataType.Int16, 2, true, ColumnId: 6, SystemFlags: Sys), - new("LvExtra", JetDataType.Int32, 4, true, ColumnId: 7, SystemFlags: Sys), - new("Name1", JetDataType.Text, 510, false, ColumnId: 3, SystemFlags: Sys), - new("Name2", JetDataType.Text, 510, false, ColumnId: 4, SystemFlags: Sys), - new("ObjectId", JetDataType.Int32, 4, true, ColumnId: 0, SystemFlags: Sys), - new("Order", JetDataType.Binary, 510, false, ColumnId: 2, SystemFlags: Sys), - ]; - - // MSysRelationships — relationship (foreign key) definitions (empty in a fresh database). - private static readonly ColumnSpec[] MSysRelationshipsColumns = - [ - new("ccolumn", JetDataType.Int32, 4, true, ColumnId: 2, SystemFlags: Sys), - new("grbit", JetDataType.Int32, 4, true, ColumnId: 1, SystemFlags: Sys), - new("icolumn", JetDataType.Int32, 4, true, ColumnId: 3, SystemFlags: Sys), - new("szColumn", JetDataType.Text, 510, false, ColumnId: 5, SystemFlags: Sys), - new("szObject", JetDataType.Text, 510, false, ColumnId: 4, SystemFlags: Sys), - new("szReferencedColumn", JetDataType.Text, 510, false, ColumnId: 7, SystemFlags: Sys), - new("szReferencedObject", JetDataType.Text, 510, false, ColumnId: 6, SystemFlags: Sys), - 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. - /// Engine tables, but not part of the catalog, so their columns carry no catalog flag. - private static readonly (string Name, ColumnSpec[] Columns)[] MSysComplexTypeTables = - [ - ("MSysComplexType_UnsignedByte", [new("Value", JetDataType.Byte, 1, true, ColumnId: 0, IsEngineColumn: true)]), - ("MSysComplexType_Short", [new("Value", JetDataType.Int16, 2, true, ColumnId: 0, IsEngineColumn: true)]), - ("MSysComplexType_Long", [new("Value", JetDataType.Int32, 4, true, ColumnId: 0, IsEngineColumn: true)]), - ("MSysComplexType_IEEESingle", [new("Value", JetDataType.Single, 4, true, ColumnId: 0, IsEngineColumn: true)]), - ("MSysComplexType_IEEEDouble", [new("Value", JetDataType.Double, 8, true, ColumnId: 0, IsEngineColumn: true)]), - ("MSysComplexType_GUID", [new("Value", JetDataType.Guid, 16, true, ColumnId: 0, IsEngineColumn: true)]), - ("MSysComplexType_Decimal", [new("Value", JetDataType.FixedPoint, 9, false, ColumnId: 0, IsEngineColumn: true)]), - ("MSysComplexType_Text", [new("Value", JetDataType.Text, 510, false, ColumnId: 0, IsEngineColumn: true)]), - ("MSysComplexType_Attachment", - [ - new("FileData", JetDataType.Ole, 0, false, ColumnId: 3, IsEngineColumn: true, ExtendedFlags: AttachmentValue), - new("FileFlags", JetDataType.Int32, 4, true, ColumnId: 5, IsEngineColumn: true, ExtendedFlags: AttachmentValue), - new("FileName", JetDataType.Text, 510, false, ColumnId: 1, IsEngineColumn: true, ExtendedFlags: AttachmentValue), - new("FileTimeStamp", JetDataType.DateTime, 8, true, ColumnId: 4, IsEngineColumn: true, ExtendedFlags: AttachmentValue), - new("FileType", JetDataType.Text, 510, false, ColumnId: 2, IsEngineColumn: true, ExtendedFlags: AttachmentValue), - new("FileURL", JetDataType.Memo, 0, false, ColumnId: 0, IsEngineColumn: true, ExtendedFlags: AttachmentValue), - ]), - ]; - - /// 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); - - // The default workgroup's account SIDs, as a stock System.mdw's MSysAccounts holds them (the file Access opens - // first to authenticate). The Admins group alone has a 102-byte SID, which Access adds on first open, so it is - // not emitted. On disk each is XOR'd with the file's own keystream, which page 0 determines (page-00 §2.3), so - // the stored bytes are worked out per file from the header written for it — see SidKeystream. - private static readonly byte[] AdminAccount = [0x03, 0x01]; // admin user — owner of what it creates, read grantee on the system tables - private static readonly byte[] UsersAccount = [0x02, 0x01]; // Users group — full grantee - private static readonly byte[] EngineAccount = [0x02, 0x03]; // Engine — owner of the system tables and the DAO containers - private static readonly byte[] CreatorAccount = [0x02, 0x04]; // Creator — the inheritable container grant's placeholder for an object's owner - - /// - /// Creates a new, empty database at from scratch — no DAO/ADOX. Hand-builds the - /// bootstrap for the given — Jet 4 (0x01, the Access 2000 / - /// 2002-2003 .mdb) or any ACCDB version — (page 0, the page-1 free map, and the - /// MSysObjects/MSysACEs TDEFs with their - /// usage maps + self-registering catalog rows), then the file is a normal LibRed database: further tables - /// are added through the ordinary writers. The file opens in the Access GUI: the 0xE00 user - /// commit-byte table is seeded here, and Access adds the system tables it wants (MSysAccessStorage, the - /// navigation-pane objects) itself — hand-creating those was tried and made things worse. - /// - /// Where the new file is written. - /// The format version byte to create it at. - /// - /// 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 — 417 configurations - /// (405 at version 0, and the twelve orders that have a version-1 table), the - /// two General orders, every locale in JetLocaleTailoring and the CJK orders, 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; - - // ACE 15 (0x04) can be read but never created: ACE REFUSES a file carrying the byte, an empty one - // included, and restamping 0x14 to 0x03 opens the identical bytes - // (Ace_refuses_the_0x04_version_byte_and_nothing_else_about_the_file). FromVersionByte still maps 0x04 - // onto the 0x03 layout, so reading one stays supported; only writing it is refused. - if (version == (byte)JetVersion.Version15_2013) - throw new NotSupportedException( - $"Cannot create a database at {nameof(JetVersion.Version15_2013)} (version byte 0x04): it is not a " - + $"valid format for a new database. Use {nameof(JetVersion.Version14_2010)}, the format Access 2013 " - + "databases use. Existing 0x04 files can still be opened for reading."); - - JetFormatBase format = JetFormatBase.FromVersionByte(version); - - // Page 0 carries the ANSI code page of the order's language, as ACE writes it — not a fixed 1252. - int codePage = JetCodePages.For(sortOrder) ?? throw new NotSupportedException( - $"Cannot create a database in {sortOrder}: the code page ACE writes for it has not been measured."); - - // Jet 4 (0x01, the Access 2000 / 2002-2003 `.mdb`) and the ACCDB versions can both be created; the - // identifier follows the version byte in BuildDefinitionPage, so the pair is always consistent and the - // file reopens. Jet 3 is rejected by FromVersionByte already — DAO cannot create one either - // ("Could not find installable ISAM"), so there is nothing to compare against. - // - // A Jet 4 file needs no other difference: DAO's own dbVersion40 database contains exactly the same - // four core system tables this method builds. Access adds MSysAccessStorage, the navigation-pane - // tables and the MSysDb properties (AccessVersion, Build, ProjVer, …) when it first opens the file, - // for a Jet 4 file just as for an ACCDB — measured by diffing a DAO-created Access 2000 database - // before and after Access opened it. - // - // It adds AccessVersion **09.50** and MSysAccessStorage even to a Jet 4 file. The legacy - // MSysAccessObjects store (08.50, and the only place the unmodelled 0x11 type appears) comes from - // Access *creating* the database itself in the Access 2000 generation — a new file from its own New - // dialog. So the generation is chosen at creation by Access, and a file created by anything else gets - // the modern one when Access first opens it. Nothing a creator can or should reproduce: DAO does not, - // and neither does this. - // 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; - - // 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) - - // Created now, as Access would. The SIDs are worked out from the finished page 0 — its creation date, - // collation and empty password field all feed the keystream — so nothing in it may change after this. - byte[] page0 = BuildDefinitionPage(version, format.IsAccdb, codePage, sortOrder, DateTime.Now.ToOADate()); - byte[] sidUsers = SidKeystream.MaskAccount(page0, UsersAccount); - byte[] sidAdmin = SidKeystream.MaskAccount(page0, AdminAccount); - byte[] sidEngine = SidKeystream.MaskAccount(page0, EngineAccount); - byte[] sidCreator = SidKeystream.MaskAccount(page0, CreatorAccount); - - byte[][] seed = - [ - page0, - 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 - ]; - // Creation is an explicit create-new operation. FileMode.Create would silently truncate an existing - // database before any of the format bootstrap work could validate or fail. - using (var fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None)) - foreach (byte[] p in seed) fs.Write(p, 0, format.PageSize); - - // Reopen through the normal stack and self-register the four core tables, so the catalog then finds - // them and every other table can go through TableCreator. - using var db = JetDatabase.Open(path, readOnly: false); - // The DAO catalog hierarchy Access navigates: a root (0x0F000000) parents the three containers - // (Tables/Databases/Relationships); tables live under Tables, MSysDb under Databases. Access looks - // objects up by (ParentId, Name), so these parents must be exact. - const int root = 0x0F000000, tablesC = 0x0F000001, databasesC = 0x0F000002, relationshipsC = 0x0F000003; - - Table msysObjects = db.OpenTableAt(objPage, "MSysObjects"); - Table msysAces = db.OpenTableAt(acesPage, "MSysACEs"); - void Ace(int objectId, byte[] sid, int acm, bool inherit = false) - { - TableDef aces = msysAces.Definition; - var v = new object?[aces.Columns.Count]; - CatalogWriter.Set(aces, v, "ObjectId", objectId); - CatalogWriter.Set(aces, v, "SID", sid); - CatalogWriter.Set(aces, v, "ACM", acm); - CatalogWriter.Set(aces, v, "FInheritable", inherit); - msysAces.Insert(v); - } - - // Catalog rows in the real stored order: DAO containers + MSysDb first, then the system tables, then - // the DAO "SingleRecord" pseudo-object. Access's catalog bootstrap walks MSysObjects in this order. - InsertCatalogRow(msysObjects, tablesC, "Tables", type: 3, SystemFlag, parentId: root, owner: sidEngine); - InsertCatalogRow(msysObjects, databasesC, "Databases", type: 3, SystemFlag, parentId: root, owner: sidEngine); - InsertCatalogRow(msysObjects, relationshipsC, "Relationships", type: 3, SystemFlag, parentId: root, owner: sidEngine); - InsertCatalogRow(msysObjects, 0x10000000, "MSysDb", type: 2, SystemFlag, parentId: databasesC, owner: sidAdmin); - InsertCatalogRow(msysObjects, objPage, "MSysObjects", type: 1, SystemFlag, parentId: tablesC, owner: sidEngine); - InsertCatalogRow(msysObjects, acesPage, "MSysACEs", type: 1, SystemFlag, parentId: tablesC, owner: sidEngine); - InsertCatalogRow(msysObjects, queriesPage, "MSysQueries", type: 1, SystemFlag, parentId: tablesC, owner: sidEngine); - InsertCatalogRow(msysObjects, relPage, "MSysRelationships", type: 1, SystemFlag, parentId: tablesC, owner: sidEngine); - InsertCatalogRow(msysObjects, unchecked((int)0x80000000), "SingleRecord", type: 9, flags: 0x10000000, parentId: relationshipsC); - - // Access-control rows (verified per-object-class masks) in the same object order as the catalog rows. - Ace(tablesC, sidCreator, 0x0F00FE, inherit: true); Ace(tablesC, sidAdmin, 0x060001); Ace(tablesC, sidUsers, 0x0FFEFF, inherit: true); - Ace(databasesC, sidAdmin, 0x060000); - Ace(relationshipsC, sidCreator, 0x0F00FE, inherit: true); Ace(relationshipsC, sidAdmin, 0x060001); Ace(relationshipsC, sidUsers, 0x0FFFFF, inherit: true); - Ace(0x10000000, sidAdmin, 0x06000E); Ace(0x10000000, sidUsers, 0x00000E); // MSysDb - Ace(objPage, sidAdmin, 0x060000); Ace(objPage, sidUsers, 0x000014); // MSysObjects - Ace(acesPage, sidAdmin, 0x060000); // MSysACEs (admin only) - Ace(queriesPage, sidAdmin, 0x060000); Ace(queriesPage, sidUsers, 0x000014); // MSysQueries - Ace(relPage, sidAdmin, 0x0E0000); Ace(relPage, sidUsers, 0x000014); // MSysRelationships - - // The system tables carry the indexes Access uses to navigate the catalog. ParentIdName is the first - // real index, Id (the PK) second — matching real files. - db.CreateIndex("MSysObjects", "ParentIdName", [("ParentId", false), ("Name", false)], isUnique: true); - db.CreateIndex("MSysObjects", "Id", [("Id", false)], isUnique: true, isPrimary: true); - db.CreateIndex("MSysACEs", "ObjectId", [("ObjectId", false)], disallowNull: true); - db.CreateIndex("MSysQueries", "ObjectIdAttribute", [("ObjectId", false), ("Attribute", false), ("Order", false)], isUnique: true, isPrimary: true); - db.CreateIndex("MSysRelationships", "szRelationship", [("szRelationship", 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, sidEngine); - - // 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 - // a table Access's compact rejected ("-1206 Unrecognized database format"). A faithful native file mirrors - // 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, byte[] sidEngine) - { - 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, sidEngine); - - foreach ((string name, ColumnSpec[] columns) in MSysComplexTypeTables) - { - db.CreateTable(name, columns); - MarkAsSystemTable(db, name, ComplexStorageFlags, sidEngine); - } - } - - /// 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, byte[] sidEngine) - { - 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.RowsWhere([idIndex], row => row[idIndex] is { } id - && Convert.ToInt32(id, CultureInfo.InvariantCulture) == definition.DefinitionPage)) - { - 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) - { - TableDef objects = msysObjects.Definition; - var values = new object?[objects.Columns.Count]; - void Set(string col, object? v) => CatalogWriter.Set(objects, values, col, v); - Set("Id", id); - Set("ParentId", parentId); - Set("Name", name); - Set("Type", type); // 1 = table, 2 = database (MSysDb), 3 = DAO container, 9 = SingleRecord - Set("Flags", flags); - if (owner != null) Set("Owner", owner); - Set("DateCreate", DateTime.Now); - Set("DateUpdate", DateTime.Now); - msysObjects.Insert(values); - } - - /// Builds the page-1 global free-pages map: a data page with two 69-byte inline maps. Row 0 is - /// the free map — pages < are used (bit 0); pages from there to the map's - /// 512-page reach are marked free (bit 1), pre-declaring space beyond the file end so an allocator - /// (LibRed's or Access's) grabs a "free" page and grows the file. Row 1 is an all-zeros companion, as real - /// files carry. - private static byte[] BuildFreeMapPage(JetFormatBase format, int usedPages) - { - const int mapLen = 1 + 4 + 64; // inline map: type + start page + 64-byte bitmap (512 pages) - var page = new byte[format.PageSize]; - PageHeader.WriteType(page, PageType.DataPage); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.DataOwnerOffset, 4), 1); // global-map owner (observed) - - int row0 = format.PageSize - mapLen; // free map (highest offset — the one the allocator reads) - int row1 = row0 - mapLen; // all-zeros companion - for (int p = usedPages; p < 64 * 8; p++) // mark pages >= usedPages free - page[row0 + 5 + (p >> 3)] |= (byte)(1 << (p & 7)); - - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset, 2), (ushort)row0); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + 2, 2), (ushort)row1); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2), 2); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), (ushort)(row1 - format.DataRowDirectoryOffset - 4)); - return page; - } - - /// Builds a system table's TDEF page and its owned/free usage-map page. Long-value (Memo/Ole) - /// columns each get an owned+free map on that page (rows 2 onward, after the two data-page maps), so a - /// 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, 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(); - var longValueSpecs = new List(longValueCols.Count); - 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, collation, - longValueColumns: longValueSpecs).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]; - Array.Copy(tdef, tdefPage, format.PageSize); // these system TDEFs fit one page - - byte[] usageMap = BuildUsageMapPage(format, 2 + longValueCols.Count * 2); - return (tdefPage, usageMap); - } - - private static byte[] BuildUsageMapPage(JetFormatBase format, int mapCount) - { - const int mapLen = 1 + 4 + 64; // inline map record: type + start page + 64-byte bitmap - var page = new byte[format.PageSize]; - PageHeader.WriteType(page, PageType.DataPage); - int offset = format.PageSize; - for (int row = 0; row < mapCount; row++) - { - offset -= mapLen; - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + row * 2, 2), (ushort)offset); - } - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2), (ushort)mapCount); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), - (ushort)(offset - format.DataRowDirectoryOffset - mapCount * 2)); - return page; - } - - private static void WriteInt24(byte[] b, int o, int v) - { - b[o] = (byte)v; b[o + 1] = (byte)(v >> 8); b[o + 2] = (byte)(v >> 16); - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/IndexCursor.cs b/src/LibRed/LibRed.Core/Storage/IndexCursor.cs index 7a67b622..9a72afa6 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexCursor.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexCursor.cs @@ -4,16 +4,13 @@ namespace LibRed.Storage; -/// An index entry: the decoded key values (in index column order) and the row they point at. -public readonly record struct IndexEntry(object?[] Key, RowId Row); - /// /// Walks an index B-tree and yields the row pointers in index (key) order. /// /// -/// Each index page has an entry-position bitmask at whose set +/// Each index page has an entry-position bitmask at whose set /// bits give the end offsets of successive entries within the entry-data region that begins -/// at . A leaf entry ends with a 4-byte big-endian row pointer +/// at . A leaf entry ends with a 4-byte big-endian row pointer /// (page in the high 24 bits, row in the low 8); a node entry instead ends with the 4-byte /// child page number. Key bytes are not decoded here — only the trailing pointers are read — /// so the order-preserving key encoding is not needed to enumerate rows in order. @@ -29,8 +26,8 @@ public sealed class IndexCursor(PageChannel channel, int rootPage) /// Yields each entry with its decoded key (per ) in index order. /// Key columns that use Jet's lossy text/binary collation decode as null. /// - public IEnumerable Entries(IReadOnlyList<(ColumnDef Column, bool Ascending)> columns) => - WalkRaw().Select(e => new IndexEntry(IndexKeyDecoder.Decode(columns, e.Key), e.Row)); + public IEnumerable<(object?[] Key, RowId Row)> Entries(IReadOnlyList<(ColumnDef Column, bool Ascending)> columns) => + WalkRaw().Select(e => (Key: IndexKeyCodec.Decode(columns, e.Key), Row: e.Row)); /// /// Yields each entry's full (decompressed) key bytes and row pointer, without decoding — used @@ -50,12 +47,12 @@ public IEnumerable Entries(IReadOnlyList<(ColumnDef Column, bool Asc int pageNumber = pending.Pop(); if (!visited.Add(pageNumber)) throw new InvalidDataException($"Index traversal contains a repeated/cyclic page {pageNumber}."); - CheckedIndexPage page = IndexPageReader.Read(_channel, pageNumber, owner); + IndexTree.CheckedPage page = IndexTree.Read(_channel, pageNumber, owner); owner ??= page.Owner; if (page.Type == PageType.LeafIndexPage) { - foreach ((byte[] key, int pointer) in IndexPageReader.DecodeEntries(page)) + foreach ((byte[] key, int pointer) in IndexTree.DecodeEntries(page)) { // The one invariant a B-tree exists for: walked in order, stored keys never go backwards. // Nothing else checks it, and a walk that silently accepts a descending step is a walk that @@ -67,7 +64,7 @@ public IEnumerable Entries(IReadOnlyList<(ColumnDef Column, bool Asc $"Index page {pageNumber} entry {Convert.ToHexString(key)} sorts before the entry " + $"before it, {Convert.ToHexString(previous)}."); previous = key; - yield return (key, new RowId(pointer >> 8, pointer & 0xFF)); + yield return (key, RowId.FromPacked(pointer)); } continue; } @@ -77,8 +74,8 @@ public IEnumerable Entries(IReadOnlyList<(ColumnDef Column, bool Asc // prefix can cover the first bytes of the trailer, and on a node whose 0x18 is nonzero — which ACE // tolerates — that offset lands on the wrong bytes, or inside the entry bitmask for a stored entry // under 4 bytes, still in bounds. Either way it yields a wrong child page with no exception, and - // bypasses the child-page validation IndexPageReader.Read performs on the reconstructed entries. - var children = IndexPageReader.DecodeEntries(page).Select(e => e.Trailer).ToList(); + // bypasses the child-page validation IndexTree.Read performs on the reconstructed entries. + List children = IndexTree.Trailers(page); for (int i = children.Count - 1; i >= 0; i--) pending.Push(children[i]); } diff --git a/src/LibRed/LibRed.Core/Storage/IndexKeyCodec.cs b/src/LibRed/LibRed.Core/Storage/IndexKeyCodec.cs new file mode 100644 index 00000000..9d14bde4 --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/IndexKeyCodec.cs @@ -0,0 +1,690 @@ +using EntityFrameworkCore.Jet.Data; +using LibRed.Catalog; +using LibRed.Storage.Types; +using System.Buffers.Binary; +using System.Globalization; +using System.Runtime.InteropServices; + +namespace LibRed.Storage; + +/// +/// Reads and writes Jet/ACE index keys, owning their layout, reversible value transforms, +/// collation framing, length limit and truncation checksum. +/// +/// +/// Encoding reads values by ; decoding returns values in index-column order. +/// Text, Binary and DATETIME2 cannot be decoded reliably from a stored key. Decoding stops at the first +/// such column or incomplete value, leaving it and subsequent columns null. The existing text collations +/// supply weights; this codec owns the key structure around them. +/// +public static class IndexKeyCodec +{ + // --- Limits --- + + /// + /// 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. 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 truncated. + /// + internal const int MaxKeyBytes = 510; + + /// The checksum that ends a truncated key, big-endian. + internal const int ChecksumSize = sizeof(ushort); + + /// The bytes of an over-long key ACE keeps ahead of its checksum. + internal const int KeptKeyBytes = MaxKeyBytes - ChecksumSize; + + /// Access indexes only the first 255 characters of a Memo (Long Text) value (verified vs ACE). + internal const int MemoKeyMaxChars = 255; + + // --- Column prefixes and fixed-width values --- + + /// The prefix of a present value in a column of the given direction. + internal static byte Start(bool ascending) => (byte)(ascending ? IndexKeyPrefix.AscStart : IndexKeyPrefix.DescStart); + + /// The prefix of a null in a column of the given direction. + internal static byte Null(bool ascending) => (byte)(ascending ? IndexKeyPrefix.AscNull : IndexKeyPrefix.DescNull); + + /// The byte after a Yes/No column's start prefix: 00 for true and FF for false, so true + /// sorts first, both inverted in a descending column (verified against ACE: 7F 00 / 7F FF, 80 FF / 80 00). + internal static byte Boolean(bool value, bool ascending) + { + byte b = value ? (byte)0x00 : (byte)0xFF; + return ascending ? b : (byte)~b; + } + + /// + /// The key width of a fixed-width column type, or -1 where the key is not fixed-width — TEXT, Binary, + /// GUID and DATETIME2 all encode to a variable, and for the first three lossy, form + /// (page-03-04 §10.4). Here for the same reason as the prefixes above: the encoder and the decoder each had + /// their own copy of this table and they drifted, the decoder never learning the widths the encoder + /// writes for and . + /// + internal static int FixedKeySize(JetDataType type) => type switch + { + JetDataType.Byte => 1, + JetDataType.Int16 => 2, + JetDataType.Int32 => 4, + // A complex (multi-value / attachment) column's key is its Int32 complex id, encoded exactly as an + // Int32 — verified against ACE over 43 entries across 9 such indexes in two files, covering + // attachment, Text and Long element types, with no difference in any byte. + JetDataType.Complex => 4, + JetDataType.Single => 4, + JetDataType.Double or JetDataType.DateTime => 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 => FixedPointMagnitudeOffset + FixedPointMagnitudeSize, + _ => -1, + }; + + /// The sign byte of a non-negative FixedPoint (Decimal/Numeric) key. A negative key is the bitwise + /// complement of the whole positive form, so its sign byte reads 00. + internal const byte FixedPointNonNegative = 0xFF; + + /// A FixedPoint key's magnitude — |value| × 10^scale as one big-endian 128-bit integer — after the sign + /// byte. + internal const int FixedPointMagnitudeOffset = 1; + internal const int FixedPointMagnitudeSize = 16; + + // --- Chunked values (Binary, GUID, DATETIME2) --- + + /// The data bytes in one chunk of a chunked key, the last zero-padded to the full width. + internal const int ChunkSize = 8; + + /// The control byte after a chunk that more follow. Constant in a descending column too, where the + /// final chunk's control byte — its real-byte count — is inverted. + internal const byte ChunkContinues = 0x09; + + // --- Text --- + + /// Appended after a descending text key's inverted bytes (verified against ACE). + internal const byte DescendingTextEnd = 0x00; + + // The bytes inside a text key's body, shared by every collation (General v0, General v1 and the locale + // tailorings over them). [MS-UCODEREF] frames the key as primaries SEP diacritics SEP case SEP extra SEP + // specials TERM; Access leaves the case section empty. + + /// Separates the sections of a text key — primaries, diacritics, case, extra. + internal const byte SectionSeparator = 0x01; + + /// Ends a text key's body. + internal const byte EndKey = 0x00; + + /// The top byte of an inline (positional) record's 16-bit position field. + internal const byte InlineStart = 0x80; + + /// The byte inside the extra section a kana fills: between its small-form codes and its mark codes, + /// inside its closing run, and after that run when inline records follow. What it denotes is not + /// established; where it sits is measured. + internal const byte KanaRunSeparator = 0xFF; + + /// The secondary weight of a character with no accent. + internal const byte DefaultSecondary = 0x02; + + /// The primary ACE gives a mark with nothing to act on — a shadda or an iteration mark with no weight + /// before it — and the unweighted characters of both tables. + internal static ReadOnlySpan UnweightedPrimary => [0xFF, 0xFF]; + + /// What a Han character's four-byte primary starts with under General (v1), before its own alphabetic + /// and diacritic weights (verified against ACE: U+4E00 is 7F FD FF 3C 6A 01 00). + internal static ReadOnlySpan HanPrimaryMarker => [0xFD, 0xFF]; + + /// + /// [MS-UCODEREF] PUNCTUATION, the script member of a word-sort ignorable: it carries no primary weight but + /// is 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. Both versions write it in the inline record. + /// + internal const byte WordSortScriptMember = 6; + /// + /// The per-column prefix byte of an order-preserving index key. Each non-boolean column is prefixed by a + /// start byte (present value) or null byte, with distinct values for ascending vs descending columns so that + /// a lexicographic byte compare matches the index's logical order. + /// + private enum IndexKeyPrefix : byte + { + /// Ascending column, null value. + AscNull = 0x00, + + /// Ascending column, present value. + AscStart = 0x7F, + + /// Descending column, present value. + DescStart = 0x80, + + /// Descending column, null value. + DescNull = 0xFF, + } + + 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); + + // The lists a key is built in, this thread's, cleared rather than allocated — as the collation encoders keep + // theirs: an index-nested-loop join encodes a key for every outer row, and two growing lists per key were most + // of what the seek allocated. Safe because nothing here re-enters Encode. + [ThreadStatic] private static List? t_buffer; + [ThreadStatic] private static List? t_textKey; + + private static byte[] Encode( + IReadOnlyList<(ColumnDef Column, bool Ascending)> columns, object?[] values, bool enforceLengthLimit) + { + List buffer = t_buffer ??= []; + buffer.Clear(); + + for (int i = 0; i < columns.Count; i++) + { + (ColumnDef column, bool ascending) = columns[i]; + object? value = values[column.Index]; + + if (column.Type == JetDataType.Boolean) + { + // The start flag, then the value's byte. The value is read with the row's truthiness — -1 and 7 + // are true — or the key disagrees with the row it indexes. A Yes/No column cannot be null, and a + // null is keyed as the false the row stores for it. + buffer.Add(Start(ascending)); + buffer.Add(Boolean(RowCodec.IsTruthy(value), ascending)); + continue; + } + + if (value is null) + { + buffer.Add(Null(ascending)); + continue; + } + + // Text uses Jet's collation: start flag then the collation key body (weights, inline + // ignorable codes, terminator). Descending inverts every byte of that ascending key + // and appends a 0x00 (verified against ACE). + // + // A Memo (Long Text) column IS indexable in Access, and its key is the *same* collation key + // over only the value's first 255 characters — verified vs ACE: a 256- or 300-character memo + // produces byte-for-byte the key of its 255-character prefix. + if (column.Type is JetDataType.Text or JetDataType.Memo) + { + // 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}" + + (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]; + + List ascendingKey = t_textKey ??= []; + ascendingKey.Clear(); + ascendingKey.Add(Start(ascending: true)); + LocaleTailoring? tailoring = JetLocaleTailoring.For(column.Collation); + bool encoded = column.Collation.Version == Collation.GeneralVersion + ? JetTextCollationV1.TryEncode(text, ascendingKey, tailoring) + : JetTextCollation.TryEncode(text, ascendingKey, tailoring); + 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) + { + buffer.AddRange(ascendingKey); + } + else + { + foreach (byte b in ascendingKey) buffer.Add((byte)~b); + buffer.Add(DescendingTextEnd); + } + continue; + } + + // GUID key (verified against ACE): the 16 GUID bytes in canonical *string* order (NOT the + // mixed-endian .ToByteArray layout), chunked exactly as a 16-byte Binary value is — two full chunks, + // the first followed by the continuation marker and the second by its count, 8. Descending inverts + // every byte except the continuation marker — verified against ACE. + if (column.Type == JetDataType.Guid) + { + Guid guid = value switch + { + Guid g => g, + byte[] b when b.Length == 16 => new Guid(b), + string text when JetTypeCodec.TryParseGuid(text, out Guid parsed) => parsed, + _ => throw new NotSupportedException($"Cannot encode GUID index key from {value.GetType().Name}."), + }; + EncodeBinaryChunked(buffer, Convert.FromHexString(guid.ToString("N")), ascending); + continue; + } + + // Binary key (verified against ACE's EverythingIsBytes fixture): see EncodeBinaryChunked. The old fixed + // 4-byte MSysQueries.Order case is the single-chunk form (7F <4B> 00 00 00 00 04). + if (column.Type == JetDataType.Binary) + { + EncodeBinaryChunked(buffer, (byte[])value, ascending); + 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( + $"Index key encoding for {column.Type} (binary collation) is not supported yet."); + + buffer.Add(Start(ascending)); + byte[] raw = EncodeFixed(column, value, size); + if (!ascending) + Complement(raw); + buffer.AddRange(raw); + } + + // Past MaxKeyBytes ACE keeps the leading KeptKeyBytes 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 (see MaxKeyBytes). + if (!enforceLengthLimit || buffer.Count <= MaxKeyBytes) return [.. buffer]; + + // A discarded word-sort record used to be refused here, on the reasoning that the record sits in the + // part ACE dropped and so what it held is unobservable — and that if ACE recomputed its position when + // truncating, LibRed's reconstruction would be feeding the checksum the wrong bytes. Both halves are + // now measured and neither holds: ACE does NOT recompute the position (the record's position byte + // tracks where the mark actually sat), and the checksum over LibRed's reconstructed key reproduces + // ACE's exactly — 16 of 16 over two mark characters at eight positions each. The record was never + // unobservable; it just could not be checked until the checksum's own arithmetic was pinned down. + // See docs/design/index-key-checksum.md. + byte[] truncated = new byte[MaxKeyBytes]; + buffer.CopyTo(0, truncated, 0, KeptKeyBytes); + BinaryPrimitives.WriteUInt16BigEndian(truncated.AsSpan(KeptKeyBytes), + ComputeChecksum(CollectionsMarshal.AsSpan(buffer)[KeptKeyBytes..])); + return truncated; + } + + /// + /// Appends Jet's order-preserving chunked key: the start flag, then the data in + /// -byte chunks — real bytes left-aligned, the last zero-padded — each + /// followed by a control byte: when another chunk follows, otherwise + /// the real-byte count of this final chunk (1..8). Descending inverts every byte except the continuation + /// markers, which stay constant so the structure stays parseable. Binary, GUID and DATETIME2 keys all take this + /// form (verified against ACE). An empty value is the start flag alone. + /// + private static void EncodeBinaryChunked(List buffer, byte[] data, bool ascending) + { + buffer.Add(Start(ascending)); + if (data.Length == 0) + return; + + int offset = 0; + do + { + int n = Math.Min(ChunkSize, data.Length - offset); + for (int j = 0; j < ChunkSize; j++) + { + byte b = j < n ? data[offset + j] : (byte)0; + buffer.Add(ascending ? b : (byte)~b); + } + offset += n; + + if (offset < data.Length) + buffer.Add(ChunkContinues); + else + buffer.Add(ascending ? (byte)n : (byte)~n); + } + while (offset < data.Length); + } + + /// A fixed-width column's value as its -byte ascending key + /// (). + private static byte[] EncodeFixed(ColumnDef column, object value, int size) + { + var c = CultureInfo.InvariantCulture; + switch (column.Type) + { + case JetDataType.Byte: + return [Convert.ToByte(value, c)]; + case JetDataType.Int16: + return EncodeInteger(Convert.ToInt16(value, c), size); + case JetDataType.Int32: + case JetDataType.Complex: // the complex id, keyed as the Int32 it is + return EncodeInteger(Convert.ToInt32(value, c), size); + case JetDataType.Currency: + return EncodeInteger(JetTypeCodec.CurrencyToScaled(value, c), size); + case JetDataType.Int64: // BIGINT — verified against ACE across 0, ±1, ±42 and both extremes + return EncodeInteger(Convert.ToInt64(value, c), size); + case JetDataType.Single: + return EncodeFloatBits(BitConverter.SingleToInt32Bits(Convert.ToSingle(value, c)), size); + case JetDataType.Double: + return EncodeFloatBits(BitConverter.DoubleToInt64Bits(Convert.ToDouble(value, c)), size); + case JetDataType.DateTime: + return EncodeFloatBits(BitConverter.DoubleToInt64Bits(JetTypeCodec.ToOaDate(column, value, c)), size); + case JetDataType.FixedPoint: + return EncodeFixedPoint(JetDecimalConverter.ToDecimal(value, c), column.Scale, size); + default: + throw new NotSupportedException($"Index key type {column.Type} is not encodable."); + } + } + + /// + /// Encodes a FixedPoint (Numeric/Decimal) index key — a sign byte followed by the value's 16-byte + /// big-endian **unscaled magnitude** (|value| × 10^scale, the same integer the row codec stores). + /// A non-negative value uses sign 0xFF; a negative value is the **bitwise complement of the + /// whole 17-byte positive form** (sign becomes 0x00, magnitude is one's-complemented), so byte + /// order equals numeric order: negatives (sign 0x00) precede non-negatives (0xFF), and complementing + /// makes a larger magnitude sort earlier among negatives. A negative zero — what a value too small for + /// the scale truncates to — keeps its sign, as ACE keys it (7F 00 FF…FF), so the sign is taken with + /// : < 0 is false for -0.0000m. + /// Verified byte-for-byte against ACE (see DecimalKeyEncodingTests). + /// + private static byte[] EncodeFixedPoint(decimal value, byte scale, int size) + { + decimal factor = 1m; + for (int i = 0; i < scale; i++) factor *= 10m; + // Truncated toward zero, matching ACE and — necessarily — JetTypeCodec.EncodeNumeric: quantise a key + // differently from its row and the value is indexed under a number the row does not contain. + decimal magnitude = decimal.Truncate(Math.Abs(value) * factor); + int[] bits = decimal.GetBits(magnitude); // [lo, mid, hi, flags]; magnitude has scale 0 + + var key = new byte[size]; + key[0] = FixedPointNonNegative; + // The 96-bit magnitude as the 128-bit field holds it, so its top 32 bits are always 0. + var unscaled = ((UInt128)(uint)bits[2] << 64) | ((UInt128)(uint)bits[1] << 32) | (uint)bits[0]; + BinaryPrimitives.WriteUInt128BigEndian( + key.AsSpan(FixedPointMagnitudeOffset, FixedPointMagnitudeSize), unscaled); + + if (decimal.IsNegative(value)) + Complement(key); + return key; + } + + /// Complements a key payload for descending order or a negative magnitude. + private static void Complement(Span bytes) + { + for (int i = 0; i < bytes.Length; i++) bytes[i] = (byte)~bytes[i]; + } + + /// Big-endian with the sign bit flipped, so signed values sort lexicographically. + private static byte[] EncodeInteger(long value, int size) + { + var raw = new byte[size]; + for (int i = size - 1; i >= 0; i--) + { + raw[i] = (byte)(value & 0xFF); + value >>= 8; + } + raw[0] ^= 0x80; + return raw; + } + + /// + /// IEEE bits big-endian with the order-preserving transform: positive numbers flip the high + /// bit, negative numbers invert every byte (so negatives sort below positives, descending). + /// + private static byte[] EncodeFloatBits(long bits, int size) + { + var raw = new byte[size]; + for (int i = size - 1; i >= 0; i--) + { + raw[i] = (byte)(bits & 0xFF); + bits >>= 8; + } + + if (raw[0] < 0x80) // sign bit clear → non-negative value + raw[0] ^= 0x80; + else + Complement(raw); + + return raw; + } + public static object?[] Decode(IReadOnlyList<(ColumnDef Column, bool Ascending)> columns, ReadOnlySpan key) + { + var values = new object?[columns.Count]; + int pos = 0; + + for (int i = 0; i < columns.Count; i++) + { + (ColumnDef column, bool ascending) = columns[i]; + if (pos >= key.Length) break; + + byte flag = key[pos++]; + if (flag == Null(ascending)) + { + values[i] = null; + continue; + } + // Otherwise flag is the start flag. + + if (column.Type == JetDataType.Boolean) + { + if (pos >= key.Length) break; + values[i] = key[pos++] == Boolean(true, ascending); + continue; + } + + // GUID key: the 16 bytes of the GUID's canonical string order, chunked (see IndexKeyCodec). + if (column.Type == JetDataType.Guid) + { + if (ReadChunked(key, ref pos, ascending) is not { Length: 16 } guid) break; + values[i] = new Guid(Convert.ToHexString(guid)); + continue; + } + + int size = FixedKeySize(column.Type); + if (size <= 0 || pos + size > key.Length) + break; // text/binary/unsupported (lossy) — cannot reliably continue + + Span raw = key.Slice(pos, size).ToArray(); + pos += size; + values[i] = DecodeFixed(column, raw, ascending); + } + + return values; + } + + /// Reads a chunked value — the inverse of — from just after + /// its start flag, advancing past it. Null when the key ends inside it or a final chunk + /// claims more than a chunk holds. + private static byte[]? ReadChunked(ReadOnlySpan key, ref int pos, bool ascending) + { + var data = new List(); + while (true) + { + if (pos + ChunkSize + 1 > key.Length) return null; + ReadOnlySpan chunk = key.Slice(pos, ChunkSize); + byte control = key[pos + ChunkSize]; + pos += ChunkSize + 1; + + bool more = control == ChunkContinues; + int real = more ? ChunkSize : ascending ? control : (byte)~control; + if (real > ChunkSize) return null; + foreach (byte b in chunk[..real]) data.Add(ascending ? b : (byte)~b); + if (!more) return [.. data]; + } + } + + private static object DecodeFixed(ColumnDef column, Span raw, bool ascending) + { + switch (column.Type) + { + case JetDataType.Byte: + if (!ascending) raw[0] = (byte)~raw[0]; + return raw[0]; + + case JetDataType.Int16: + return (short)DecodeInteger(raw, ascending); + case JetDataType.Int32: + case JetDataType.Complex: // the complex id, keyed as the Int32 it is + return (int)DecodeInteger(raw, ascending); + case JetDataType.Currency: + return JetTypeCodec.CurrencyFromScaled(DecodeInteger(raw, ascending)); + case JetDataType.Int64: + return DecodeInteger(raw, ascending); + + case JetDataType.Single: + return BitConverter.Int32BitsToSingle((int)DecodeFloatBits(raw, ascending)); + case JetDataType.Double: + return BitConverter.Int64BitsToDouble(DecodeFloatBits(raw, ascending)); + case JetDataType.DateTime: + double serial = BitConverter.Int64BitsToDouble(DecodeFloatBits(raw, ascending)); + return JetTypeCodec.TryFromOaDate(serial, out DateTime date) + ? date + : throw new InvalidDataException($"An index key on '{column.Name}' holds {serial}, which is not a date."); + + case JetDataType.FixedPoint: + return DecodeFixedPoint(raw, ascending, column.Scale); + + default: + throw new NotSupportedException($"Index key type {column.Type} is not decodable."); + } + } + + /// Reverses : a negative key is the complement of the positive + /// form, whose sign byte is and whose 16-byte big-endian + /// magnitude is the value times 10^scale. A negative zero keeps its sign, as the key does. + private static decimal DecodeFixedPoint(Span raw, bool ascending, byte scale) + { + if (!ascending) + Complement(raw); + bool negative = raw[0] != FixedPointNonNegative; + if (negative) + Complement(raw); + + UInt128 unscaled = BinaryPrimitives.ReadUInt128BigEndian( + raw.Slice(FixedPointMagnitudeOffset, FixedPointMagnitudeSize)); + if (unscaled >> 96 != 0) + throw new OverflowException("A Decimal index key's magnitude exceeds the 96 bits System.Decimal holds."); + return new decimal((int)(uint)unscaled, (int)(uint)(unscaled >> 32), (int)(uint)(unscaled >> 64), negative, scale); + } + + /// Reverses the integer key transform (descending = bytes inverted; sign bit flipped; big-endian). + private static long DecodeInteger(Span raw, bool ascending) + { + if (!ascending) + Complement(raw); + raw[0] ^= 0x80; + + long value = 0; + bool negative = (raw[0] & 0x80) != 0; + if (negative) value = -1; // sign-extend + foreach (byte b in raw) value = (value << 8) | b; + return value; + } + + /// Reverses the floating-point key transform, returning the raw IEEE bits big-endian. + private static long DecodeFloatBits(Span raw, bool ascending) + { + if (ascending) + { + if ((raw[0] & 0x80) != 0) raw[0] ^= 0x80; // was positive: undo first-bit flip + else Complement(raw); // was negative: undo full invert + } + else + { + if ((raw[0] & 0x80) == 0) // was positive + { + Complement(raw); + raw[0] ^= 0x80; + } + // was negative: stored as-is + } + + long bits = 0; + foreach (byte b in raw) bits = (bits << 8) | b; + return bits; + } + /// + /// 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 ChecksumStepBits => + [ + 0x0580, 0x0F80, 0x1B80, 0x3380, 0x6380, 0xC380, 0x8381, 0x0383, + 0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080, + ]; + + private static readonly ushort[] ChecksumTable = BuildChecksumTable(); + + private static ushort[] BuildChecksumTable() + { + 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 ^= ChecksumStepBits[bit]; + table[value] = result; + } + return table; + } + + /// + /// The checksum over the bytes ACE dropped — everything from on. + /// + /// + /// A key of at most bytes is stored as built. Past that ACE keeps + /// the first 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. + /// + /// + /// It holds where the dropped bytes contain a word-sort record too, which was long assumed + /// uncheckable: the record sits in the part ACE discarded, so what it held looked unobservable, and ACE might + /// have recomputed its position when truncating. Neither is so — the position byte tracks where the mark + /// actually sat, and the checksum over the reconstructed key matches ACE's for both mark characters at + /// positions spread through the value. See . + /// + /// + /// The last byte does not go through a full step. Every byte before it is folded in the usual way, and + /// then the final one is XORed into the high half — it never gets its own shift or table lookup. + /// This was first written as "the terminator is excluded", which is the same thing whenever that + /// byte is 0x00: XOR-ing zero changes nothing. A key ending in text always ends in its 0x00 + /// terminator, and every measurement behind the original rule used one, so the two readings could not be + /// told apart. They diverge the moment the key's last column is numeric — (TEXT, TEXT, LONG) put + /// a data byte there and the keys parted company from ACE's, silently. Re-measured over LONG, CURRENCY + /// and DOUBLE tails across 24 keys: this form matches ACE on every one, and still matches on the all-text + /// keys the old form was derived from. See docs/design/index-key-checksum.md. + /// + /// + private static ushort ComputeChecksum(ReadOnlySpan discarded) + { + ushort crc = 0; + foreach (byte b in discarded[..^1]) crc = (ushort)((crc >> 8) ^ ChecksumTable[crc & 0xFF] ^ b); + return (ushort)(crc ^ (discarded[^1] << 8)); + } +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/IndexKeyDecoder.cs b/src/LibRed/LibRed.Core/Storage/IndexKeyDecoder.cs deleted file mode 100644 index a2d7b152..00000000 --- a/src/LibRed/LibRed.Core/Storage/IndexKeyDecoder.cs +++ /dev/null @@ -1,136 +0,0 @@ -using LibRed.Catalog; -using LibRed.Formats; - -namespace LibRed.Storage; - -/// -/// Decodes the order-preserving key bytes of an index entry back into column values. -/// -/// -/// Each column is prefixed by a flag byte (0x7F start / 0x00 null for ascending; -/// 0x80 / 0xFF for descending). Fixed/numeric types use a reversible transform (sign-bit flip -/// + big-endian for integers; an IEEE transform for floating point). TEXT/Binary/GUID keys use -/// Jet's collation encoding, which is lossy and not reversible — decoding stops at the first -/// such column (its value and any following columns are returned as null). -/// -public static class IndexKeyDecoder -{ - public static object?[] Decode(IReadOnlyList<(ColumnDef Column, bool Ascending)> columns, ReadOnlySpan key) - { - var values = new object?[columns.Count]; - int pos = 0; - - for (int i = 0; i < columns.Count; i++) - { - (ColumnDef column, bool ascending) = columns[i]; - if (pos >= key.Length) break; - - byte flag = key[pos++]; - if (flag == (ascending ? IndexKeyFlags.AscNull : IndexKeyFlags.DescNull)) - { - values[i] = null; - continue; - } - // Otherwise flag is the start flag (0x7F / 0x80). - - // Yes/No: one byte after the flag, 0x00 for true ascending and 0xFF for true descending. - if (column.Type == JetDataType.Boolean) - { - if (pos >= key.Length) break; - byte b = key[pos++]; - values[i] = b == (ascending ? 0x00 : 0xFF); - continue; - } - - // GUID key: 8 bytes, a 0x09 marker, 8 bytes, a terminator — the 16 bytes are the GUID's - // canonical string order (see IndexKeyEncoder). Descending inverts every data byte (the 0x09 - // marker stays constant), so undo that here. - if (column.Type == JetDataType.Guid) - { - if (pos + 18 > key.Length) break; - var s = new byte[16]; - for (int j = 0; j < 8; j++) s[j] = ascending ? key[pos + j] : (byte)~key[pos + j]; - for (int j = 0; j < 8; j++) s[8 + j] = ascending ? key[pos + 9 + j] : (byte)~key[pos + 9 + j]; - pos += 18; - values[i] = new Guid(Convert.ToHexString(s)); - continue; - } - - int size = IndexKeyFlags.FixedKeySize(column.Type); - if (size <= 0 || pos + size > key.Length) - break; // text/binary/unsupported (lossy) — cannot reliably continue - - Span raw = key.Slice(pos, size).ToArray(); - pos += size; - values[i] = DecodeFixed(column.Type, raw, ascending); - } - - return values; - } - - private static object DecodeFixed(JetDataType type, Span raw, bool ascending) - { - switch (type) - { - case JetDataType.Byte: - if (!ascending) raw[0] = (byte)~raw[0]; - return raw[0]; - - case JetDataType.Int16: - return (short)DecodeInteger(raw, ascending); - case JetDataType.Int32: - 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)); - case JetDataType.Double: - return BitConverter.Int64BitsToDouble(DecodeFloatBits(raw, ascending)); - case JetDataType.DateTime: - return DateTime.FromOADate(BitConverter.Int64BitsToDouble(DecodeFloatBits(raw, ascending))); - - default: - throw new NotSupportedException($"Index key type {type} is not decodable."); - } - } - - /// Reverses the integer key transform (descending = bytes inverted; sign bit flipped; big-endian). - private static long DecodeInteger(Span raw, bool ascending) - { - if (!ascending) - for (int i = 0; i < raw.Length; i++) raw[i] = (byte)~raw[i]; - raw[0] ^= 0x80; - - long value = 0; - bool negative = (raw[0] & 0x80) != 0; - if (negative) value = -1; // sign-extend - foreach (byte b in raw) value = (value << 8) | b; - return value; - } - - /// Reverses the floating-point key transform, returning the raw IEEE bits big-endian. - private static long DecodeFloatBits(Span raw, bool ascending) - { - if (ascending) - { - if ((raw[0] & 0x80) != 0) raw[0] ^= 0x80; // was positive: undo first-bit flip - else for (int i = 0; i < raw.Length; i++) raw[i] = (byte)~raw[i]; // was negative: undo full invert - } - else - { - if ((raw[0] & 0x80) == 0) // was positive - { - for (int i = 0; i < raw.Length; i++) raw[i] = (byte)~raw[i]; - raw[0] ^= 0x80; - } - // was negative: stored as-is - } - - long bits = 0; - foreach (byte b in raw) bits = (bits << 8) | b; - return bits; - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs b/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs deleted file mode 100644 index f97e3596..00000000 --- a/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs +++ /dev/null @@ -1,367 +0,0 @@ -using EntityFrameworkCore.Jet.Data; -using LibRed.Catalog; -using LibRed.Formats; -using LibRed.Storage.Types; -using System.Buffers.Binary; -using System.Globalization; -using System.Runtime.InteropServices; - -namespace LibRed.Storage; - -/// -/// Encodes index column values into Jet's order-preserving key bytes — the inverse of -/// . Lexicographic comparison of the produced bytes matches the -/// index's logical order, so a freshly encoded key can be slotted into a leaf by byte compare. -/// -/// -/// Each column is prefixed by a flag byte (0x7F start / 0x00 null ascending; -/// 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 — 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; - - /// - /// 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); - - // The lists a key is built in, this thread's, cleared rather than allocated — as the collation encoders keep - // theirs: an index-nested-loop join encodes a key for every outer row, and two growing lists per key were most - // of what the seek allocated. Safe because nothing here re-enters Encode. - [ThreadStatic] private static List? t_buffer; - [ThreadStatic] private static List? t_textKey; - - private static byte[] Encode( - IReadOnlyList<(ColumnDef Column, bool Ascending)> columns, object?[] values, bool enforceLengthLimit) - { - List buffer = t_buffer ??= []; - buffer.Clear(); - - for (int i = 0; i < columns.Count; i++) - { - (ColumnDef column, bool ascending) = columns[i]; - object? value = values[column.Index]; - - if (column.Type == JetDataType.Boolean) - { - // The start flag, then 0x00 for true and 0xFF for false, so true sorts first; descending - // inverts both bytes (verified against ACE: 7F 00 / 7F FF, 80 FF / 80 00). The value is read - // with the row's truthiness — -1 and 7 are true — or the key disagrees with the row it indexes. - // A Yes/No column cannot be null, and a null is keyed as the false the row stores for it. - byte b = RowEncoder.IsTruthy(value) ? (byte)0x00 : (byte)0xFF; - buffer.Add(ascending ? IndexKeyFlags.AscStart : IndexKeyFlags.DescStart); - buffer.Add(ascending ? b : (byte)~b); - continue; - } - - if (value is null) - { - buffer.Add(ascending ? IndexKeyFlags.AscNull : IndexKeyFlags.DescNull); - continue; - } - - // Text uses Jet's collation: start flag then the collation key body (weights, inline - // ignorable codes, terminator). Descending inverts every byte of that ascending key - // and appends a 0x00 (verified against ACE). - // - // A Memo (Long Text) column IS indexable in Access, and its key is the *same* collation key - // over only the value's first 255 characters — verified vs ACE: a 256- or 300-character memo - // produces byte-for-byte the key of its 255-character prefix. - if (column.Type is JetDataType.Text or JetDataType.Memo) - { - // 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}" + - (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]; - - List ascendingKey = t_textKey ??= []; - ascendingKey.Clear(); - ascendingKey.Add(IndexKeyFlags.AscStart); - LocaleTailoring? tailoring = JetLocaleTailoring.For(column.Collation); - // The word-sort flag is discarded: it existed only to refuse a key whose record the truncation - // would drop, and that refusal is gone — see the note at the truncation below. - bool encoded = column.Collation.Version == Collation.GeneralVersion - ? JetTextCollationV1.TryEncode(text, ascendingKey, tailoring, out _) - : JetTextCollation.TryEncode(text, ascendingKey, tailoring, out _); - 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) - { - buffer.AddRange(ascendingKey); - } - else - { - foreach (byte b in ascendingKey) buffer.Add((byte)~b); - buffer.Add(0x00); - } - continue; - } - - // GUID key (verified against ACE): the start flag, then the 16 GUID bytes in canonical *string* - // order (NOT the mixed-endian .ToByteArray layout), split into two 8-byte halves by a constant - // 0x09 marker, and terminated by 0x08. Fixed 19-byte key; data bytes equal to 0x08/0x09 need no - // escaping because every field is at a fixed offset. Descending inverts every byte EXCEPT the - // 0x09 field marker (which stays constant so the structure is parseable, and doesn't affect - // ordering since it's equal in every key) — verified against ACE. - if (column.Type == JetDataType.Guid) - { - Guid guid = value switch - { - Guid g => g, - byte[] b when b.Length == 16 => new Guid(b), - string text when JetTypeCodec.TryParseGuid(text, out Guid parsed) => parsed, - _ => throw new NotSupportedException($"Cannot encode GUID index key from {value.GetType().Name}."), - }; - byte[] s = Convert.FromHexString(guid.ToString("N")); // 16 bytes, canonical string order - - if (ascending) - { - buffer.Add(IndexKeyFlags.AscStart); // 0x7F - buffer.AddRange(s.AsSpan(0, 8)); - buffer.Add(0x09); - buffer.AddRange(s.AsSpan(8, 8)); - buffer.Add(0x08); - } - else - { - buffer.Add(IndexKeyFlags.DescStart); // 0x80 = ~0x7F - for (int j = 0; j < 8; j++) buffer.Add((byte)~s[j]); - buffer.Add(0x09); // field marker kept as-is - for (int j = 8; j < 16; j++) buffer.Add((byte)~s[j]); - buffer.Add(unchecked((byte)~0x08)); // 0xF7 - } - continue; - } - - // Binary key (verified against ACE's EverythingIsBytes fixture): the start flag, then the raw - // bytes in **8-byte chunks**. Each chunk is 8 bytes (real bytes left-aligned, zero-padded on the - // right) followed by a control byte: 0x09 when another chunk follows (a full 8-byte chunk with - // more to come), otherwise the real-byte count of this final chunk (1..8; 0x08 for a full final - // chunk, 0x00 for empty data). This is the same chunking as the GUID key (a 16-byte value → two - // chunks: 8, 0x09, 8, 0x08); the old fixed 4-byte MSysQueries.Order case is the single-chunk form - // (7F <4B> 00 00 00 00 04). Descending inverts every byte EXCEPT the 0x09 continuation markers - // (which stay constant so structure is parseable and they're equal across keys) — mirrors GUID. - if (column.Type == JetDataType.Binary) - { - EncodeBinaryChunked(buffer, (byte[])value, ascending); - 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 = IndexKeyFlags.FixedKeySize(column.Type); - if (size <= 0) - throw new NotSupportedException( - $"Index key encoding for {column.Type} (binary collation) is not supported yet."); - - buffer.Add(ascending ? IndexKeyFlags.AscStart : IndexKeyFlags.DescStart); - byte[] raw = EncodeFixed(column, value); - if (!ascending) - for (int j = 0; j < raw.Length; j++) raw[j] = (byte)~raw[j]; - buffer.AddRange(raw); - } - - // 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]; - - // A discarded word-sort record used to be refused here, on the reasoning that the record sits in the - // part ACE dropped and so what it held is unobservable — and that if ACE recomputed its position when - // truncating, LibRed's reconstruction would be feeding the checksum the wrong bytes. Both halves are - // now measured and neither holds: ACE does NOT recompute the position (the record's position byte - // tracks where the mark actually sat), and the checksum over LibRed's reconstructed key reproduces - // ACE's exactly — 16 of 16 over two mark characters at eight positions each. The record was never - // unobservable; it just could not be checked until the checksum's own arithmetic was pinned down. - // See docs/design/index-key-checksum.md. - 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; - } - - /// - /// Appends Jet's order-preserving binary index key: start flag, then 8-byte chunks each followed by - /// a control byte (0x09 = "full chunk, more follow"; 1..8 = real-byte count of the final chunk). - /// Descending inverts every byte except the 0x09 continuation markers (verified against ACE). - /// - 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 - { - int n = Math.Min(8, data.Length - offset); - for (int j = 0; j < 8; j++) - { - byte b = j < n ? data[offset + j] : (byte)0; - buffer.Add(ascending ? b : (byte)~b); - } - offset += n; - - if (offset < data.Length) - buffer.Add(0x09); // continuation marker (constant either way) - else - buffer.Add(ascending ? (byte)n : (byte)~n); // terminator = final-chunk length - } - while (offset < data.Length); - } - - private static byte[] EncodeFixed(ColumnDef column, object value) - { - var c = CultureInfo.InvariantCulture; - switch (column.Type) - { - case JetDataType.Byte: - return [Convert.ToByte(value, c)]; - case JetDataType.Int16: - return EncodeInteger(Convert.ToInt16(value, c), 2); - case JetDataType.Int32: - case JetDataType.Complex: // the complex id, keyed as the Int32 it is - return EncodeInteger(Convert.ToInt32(value, c), 4); - case JetDataType.Currency: - return EncodeInteger((long)decimal.Round(JetDecimalConverter.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: - return EncodeFloatBits(BitConverter.DoubleToInt64Bits(Convert.ToDouble(value, c)), 8); - case JetDataType.DateTime: - return EncodeFloatBits(BitConverter.DoubleToInt64Bits(JetTypeCodec.ToOaDate(column, value, c)), 8); - case JetDataType.FixedPoint: - return EncodeFixedPoint(JetDecimalConverter.ToDecimal(value, c), column.Scale); - default: - throw new NotSupportedException($"Index key type {column.Type} is not encodable."); - } - } - - /// - /// Encodes a FixedPoint (Numeric/Decimal) index key — a sign byte followed by the value's 16-byte - /// big-endian **unscaled magnitude** (|value| × 10^scale, the same integer the row codec stores). - /// A non-negative value uses sign 0xFF; a negative value is the **bitwise complement of the - /// whole 17-byte positive form** (sign becomes 0x00, magnitude is one's-complemented), so byte - /// order equals numeric order: negatives (sign 0x00) precede non-negatives (0xFF), and complementing - /// makes a larger magnitude sort earlier among negatives. A negative zero — what a value too small for - /// the scale truncates to — keeps its sign, as ACE keys it (7F 00 FF…FF), so the sign is taken with - /// : < 0 is false for -0.0000m. - /// Verified byte-for-byte against ACE (see DecimalKeyEncodingTests). - /// - private static byte[] EncodeFixedPoint(decimal value, byte scale) - { - decimal factor = 1m; - for (int i = 0; i < scale; i++) factor *= 10m; - // Truncated toward zero, matching ACE and — necessarily — JetTypeCodec.EncodeNumeric: quantise a key - // differently from its row and the value is indexed under a number the row does not contain. - decimal magnitude = decimal.Truncate(Math.Abs(value) * factor); - int[] bits = decimal.GetBits(magnitude); // [lo, mid, hi, flags]; magnitude has scale 0 - - var key = new byte[17]; - key[0] = 0xFF; // non-negative marker - // 16-byte big-endian magnitude: the top 32-bit word is always 0 for a System.Decimal, then hi/mid/lo. - BinaryPrimitives.WriteUInt32BigEndian(key.AsSpan(1, 4), 0); - BinaryPrimitives.WriteUInt32BigEndian(key.AsSpan(5, 4), (uint)bits[2]); - BinaryPrimitives.WriteUInt32BigEndian(key.AsSpan(9, 4), (uint)bits[1]); - BinaryPrimitives.WriteUInt32BigEndian(key.AsSpan(13, 4), (uint)bits[0]); - - if (decimal.IsNegative(value)) - for (int i = 0; i < key.Length; i++) key[i] = (byte)~key[i]; - return key; - } - - /// Big-endian with the sign bit flipped, so signed values sort lexicographically. - private static byte[] EncodeInteger(long value, int size) - { - var raw = new byte[size]; - for (int i = size - 1; i >= 0; i--) - { - raw[i] = (byte)(value & 0xFF); - value >>= 8; - } - raw[0] ^= 0x80; - return raw; - } - - /// - /// IEEE bits big-endian with the order-preserving transform: positive numbers flip the high - /// bit, negative numbers invert every byte (so negatives sort below positives, descending). - /// - private static byte[] EncodeFloatBits(long bits, int size) - { - var raw = new byte[size]; - for (int i = size - 1; i >= 0; i--) - { - raw[i] = (byte)(bits & 0xFF); - bits >>= 8; - } - - if (raw[0] < 0x80) // sign bit clear → non-negative value - raw[0] ^= 0x80; - else - for (int i = 0; i < size; i++) raw[i] = (byte)~raw[i]; - - return raw; - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs b/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs deleted file mode 100644 index de28ae2d..00000000 --- a/src/LibRed/LibRed.Core/Storage/IndexPageReader.cs +++ /dev/null @@ -1,140 +0,0 @@ -using LibRed.IO; -using LibRed.Pages; -using System.Buffers.Binary; - -namespace LibRed.Storage; - -internal sealed record CheckedIndexPage( - PageBuffer Buffer, PageType Type, int Owner, int Previous, int Next, int Tail, - int CompressedByteCount, IReadOnlyList<(int Start, int End)> EntryRanges); - -/// Checks the common header, owner, pointers, and entry boundaries of an index page. -internal static class IndexPageReader -{ - internal const int OwnerOffset = 0x04; - internal const int PrevPageOffset = 0x0C; - internal const int NextPageOffset = 0x10; - internal const int ChildTailOffset = 0x14; - internal const int CompressedByteCountOffset = 0x18; - internal const int EntryMaskOffset = 0x1B; - internal const int EntryDataOffset = 0x1E0; - - public static CheckedIndexPage Read(PageChannel channel, int pageNumber, int? expectedOwner) - { - ValidatePageNumber(channel, pageNumber, "index page"); - PageBuffer buffer = channel.ReadPageShared(pageNumber); - var type = PageHeader.ReadType(buffer.Span); - if (type is not (PageType.LeafIndexPage or PageType.IntermediateIndexPage)) - throw new InvalidDataException( - $"Page {pageNumber} is type 0x{(ushort)type:X4}, not an index page (0x0103/0x0104)."); - - int owner = buffer.ReadInt32(OwnerOffset); - if (expectedOwner is not null && owner != expectedOwner) - throw new InvalidDataException( - $"Index page {pageNumber} belongs to TDEF {owner}, not TDEF {expectedOwner}."); - - int previous = buffer.ReadInt32(PrevPageOffset); - int next = buffer.ReadInt32(NextPageOffset); - int tail = buffer.ReadInt32(ChildTailOffset); - if (type == PageType.LeafIndexPage) - { - ValidateOptionalPageNumber(channel, previous, "previous leaf"); - ValidateOptionalPageNumber(channel, next, "next leaf"); - } - else - { - 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++) - { - byte mask = buffer.ReadByte(i); - for (int bit = 0; bit < 8; bit++) - { - if ((mask & (1 << bit)) == 0) continue; - int end = (i - EntryMaskOffset) * 8 + bit; - 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}) reconstructs to {length} bytes, " + - "too few for its 4-byte trailer."); - ranges.Add((start, end)); - start = end; - } - } - - 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) - throw new InvalidDataException( - $"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 child) in DecodeEntries(page)) - ValidatePageNumber(channel, child, "node child"); - - return page; - } - - public static int ReadInt32BigEndian(PageBuffer page, int offset) => - BinaryPrimitives.ReadInt32BigEndian(page.Slice(offset, 4)); - - /// Decodes a checked page's entries in order, decompressing each entry's shared prefix: the first - /// 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. - /// - /// 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) - { - 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); - } - } - - private static byte[] Concat(ReadOnlySpan a, ReadOnlySpan b) - { - var result = new byte[a.Length + b.Length]; - a.CopyTo(result); - b.CopyTo(result.AsSpan(a.Length)); - return result; - } - - private static void ValidateOptionalPageNumber(PageChannel channel, int pageNumber, string kind) - { - if (pageNumber != 0) ValidatePageNumber(channel, pageNumber, kind); - } - - private static void ValidatePageNumber(PageChannel channel, int pageNumber, string kind) - { - if (pageNumber <= 0 || pageNumber >= channel.PageCount) - throw new InvalidDataException( - $"Index {kind} pointer {pageNumber} is outside the file's 1..{channel.PageCount - 1} range."); - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/IndexWriter.cs b/src/LibRed/LibRed.Core/Storage/IndexTree.cs similarity index 77% rename from src/LibRed/LibRed.Core/Storage/IndexWriter.cs rename to src/LibRed/LibRed.Core/Storage/IndexTree.cs index baf95bfb..d113ddb7 100644 --- a/src/LibRed/LibRed.Core/Storage/IndexWriter.cs +++ b/src/LibRed/LibRed.Core/Storage/IndexTree.cs @@ -20,29 +20,19 @@ namespace LibRed.Storage; /// page and the key is a full leaf key (column key ++ row pointer) used as the separator = the maximum /// key of that child. See §10. /// -public sealed class IndexWriter(PageChannel channel, TableDef table) +internal sealed class IndexTree(PageChannel channel, TableDefinition table) { - private const int FreeSpaceOffset = 0x02; - private const int OwnerOffset = 0x04; - private const int PrevPageOffset = 0x0C; // leaf page: previous (lower-key) leaf - private const int NextPageOffset = 0x10; // leaf page: next (higher-key) leaf — Access walks this for COUNT/scan - private const int ChildTailOffset = 0x14; - private const int CompressedByteCountOffset = 0x18; - private const int LevelOffset = 0x1A; // 0 on a leaf, its height above the leaves on a node - private const int EntryMaskOffset = 0x1B; - private const int EntryDataOffset = 0x1E0; - private readonly PageChannel _channel = channel; - private readonly TableDef _table = table; - private readonly PageAllocator _allocator = new(channel); - private readonly UsageMapWriter _usageMaps = new(channel); + private readonly TableDefinition _table = table; + private readonly PageAllocator _allocator = channel.Allocator; + private readonly UsageMap _usageMaps = new(channel); private readonly record struct Entry(byte[] Key, int Trailer); public void AddEntry(IndexDef index, object?[] values, RowId rowId) { - byte[] key = IndexKeyEncoder.Encode(index.Columns, values); - int pointer = (rowId.Page << 8) | rowId.Row; + byte[] key = IndexKeyCodec.Encode(index.Columns, values); + int pointer = rowId.Packed; byte[] fullKey = WithTrailer(key, pointer); // key ++ 4-byte pointer (what node separators store) var path = Descend(index.RootPage, fullKey); // [root, …, leaf] page numbers @@ -57,7 +47,7 @@ public void AddEntry(IndexDef index, object?[] values, RowId rowId) /// public bool KeyExists(IndexDef index, object?[] values, int? excludePointer = null) { - byte[] key = IndexKeyEncoder.Encode(index.Columns, values); + byte[] key = IndexKeyCodec.Encode(index.Columns, values); int leaf = Descend(index.RootPage, WithTrailer(key, 0), path: null); int steps = 0; while (leaf != 0) @@ -90,7 +80,7 @@ public bool KeyExists(IndexDef index, object?[] values, int? excludePointer = nu /// public IEnumerable Seek(IndexDef index, object?[] values) { - byte[] key = IndexKeyEncoder.Encode(index.Columns, values); + byte[] key = IndexKeyCodec.Encode(index.Columns, values); int leaf = Descend(index.RootPage, WithTrailer(key, 0), path: null); int steps = 0; while (leaf != 0) @@ -104,7 +94,7 @@ public IEnumerable Seek(IndexDef index, object?[] values) { int cmp = CompareBytes(e.Key, key); if (cmp > 0) yield break; // sorted past the key — no more matches - if (cmp == 0) yield return new RowId(e.Trailer >> 8, e.Trailer & 0xFF); + if (cmp == 0) yield return RowId.FromPacked(e.Trailer); } leaf = page.Next; // matches may continue on the next leaf } @@ -119,8 +109,8 @@ public IEnumerable Seek(IndexDef index, object?[] values) /// public IEnumerable SeekRange(IndexDef index, object?[]? low, object?[]? high) { - byte[]? lowKey = low is null ? null : IndexKeyEncoder.Encode(index.Columns, low); - byte[]? highKey = high is null ? null : IndexKeyEncoder.Encode(index.Columns, high); + byte[]? lowKey = low is null ? null : IndexKeyCodec.Encode(index.Columns, low); + byte[]? highKey = high is null ? null : IndexKeyCodec.Encode(index.Columns, high); // A DESC index inverts its key bytes, so the low VALUE is the byte-greater key and the tree is walked // from the high bound down to it. The bounds are stated in values; here on they are byte bounds, so @@ -141,7 +131,7 @@ public IEnumerable SeekRange(IndexDef index, object?[]? low, object?[]? h { if (lowKey is not null && CompareBytes(e.Key, lowKey) < 0) continue; // before the low bound if (highKey is not null && CompareBytes(e.Key, highKey) > 0) yield break; // past the high bound - yield return new RowId(e.Trailer >> 8, e.Trailer & 0xFF); + yield return RowId.FromPacked(e.Trailer); } leaf = page.Next; } @@ -175,12 +165,12 @@ public void DeleteEntry(IndexDef index, object?[] values, RowId rowId) /// public void RemoveEntry(IndexDef index, object?[] values, RowId rowId) { - byte[] key = IndexKeyEncoder.Encode(index.Columns, values); - int pointer = (rowId.Page << 8) | rowId.Row; + byte[] key = IndexKeyCodec.Encode(index.Columns, values); + int pointer = rowId.Packed; List path = Descend(index.RootPage, WithTrailer(key, pointer)); int leafPage = path[^1]; - CheckedIndexPage page = ReadMutationPage(leafPage, PageType.LeafIndexPage); + CheckedPage page = ReadMutationPage(leafPage, PageType.LeafIndexPage); (List entries, _) = Parse(page); int idx = entries.FindIndex(e => e.Trailer == pointer && CompareBytes(e.Key, key) == 0); @@ -215,13 +205,13 @@ public void RemoveEntry(IndexDef index, object?[] values, RowId rowId) /// unlinked without leaving the parent pointing at nothing, a node shape ACE has not been observed to /// write; an empty leaf is a valid one, so the tree keeps it rather than inventing that. /// - private bool UnlinkEmptyLeaf(IndexDef index, List path, CheckedIndexPage leaf) + private bool UnlinkEmptyLeaf(IndexDef index, List path, CheckedPage leaf) { if (path.Count < 2) return false; int leafPage = path[^1]; int parentPage = path[^2]; - CheckedIndexPage parent = ReadMutationPage(parentPage, PageType.IntermediateIndexPage); + CheckedPage parent = ReadMutationPage(parentPage, PageType.IntermediateIndexPage); (List entries, int tail) = Parse(parent); int slot = entries.FindIndex(e => e.Trailer == leafPage); @@ -243,8 +233,10 @@ private bool UnlinkEmptyLeaf(IndexDef index, List path, CheckedIndexPage le $"Index '{index.Name}': node {parentPage} does not point at leaf {leafPage}."); } - if (leaf.Previous != 0) SetSiblingLink(leaf.Previous, NextPageOffset, leaf.Next, PageType.LeafIndexPage); - if (leaf.Next != 0) SetSiblingLink(leaf.Next, PrevPageOffset, leaf.Previous, PageType.LeafIndexPage); + if (leaf.Previous != 0) + SetSiblingLink(leaf.Previous, _channel.Format.IndexNextPageOffset, leaf.Next, PageType.LeafIndexPage); + if (leaf.Next != 0) + SetSiblingLink(leaf.Next, _channel.Format.IndexPrevPageOffset, leaf.Previous, PageType.LeafIndexPage); // Dropping a separator only shrinks the node, so Build never overflows, and the node keeps its prefix // as a leaf does on a delete (see RemoveEntry). A leaf's parent is one level above the leaves by @@ -254,8 +246,7 @@ private bool UnlinkEmptyLeaf(IndexDef index, List path, CheckedIndexPage le // The page leaves the index the way AllocateIndexPage brought it in: its bit out of the index's own // pages map, then released — held until this handle closes, the route ACE takes for a freed page. - (int mapRow, int mapPage) = IndexUsageMapPointer(index); - _usageMaps.SetBit(mapRow, mapPage, leafPage, set: false); + _usageMaps.SetBit(index.UsageMap.Row, index.UsageMap.Page, leafPage, set: false); _allocator.Release(leafPage); return true; } @@ -324,7 +315,7 @@ private ParsedIndexPage ReadIndexPage(int pageNumber) return hit; } - CheckedIndexPage page = IndexPageReader.Read(_channel, pageNumber, _table.DefinitionPage); + CheckedPage page = Read(_channel, pageNumber, _table.DefinitionPage); (List entries, int tail) = Parse(page); var parsed = new ParsedIndexPage( page.Type, page.Owner, entries, tail, page.Next, page.Previous, page.CompressedByteCount); @@ -387,7 +378,7 @@ private void InsertIntoLeaf(IndexDef index, List path, byte[] key, int poin // // Rebuilding at the largest available prefix on every write instead would be smaller, but it is not // what ACE writes, and the tail page of a sequential load is the visible difference. - int share = entries.Count <= 1 ? 0 : CommonPrefixLength(entries[0].Key, entries[^1].Key); + int share = Share(entries); int keep = Math.Min(page.Compressed, share); // the new key may not share the old prefix if (Build(PageType.LeafIndexPage, page.Previous, page.Next, tail: 0, level: 0, entries, keep) is { } asIs) @@ -421,7 +412,7 @@ private void InsertIntoLeaf(IndexDef index, List path, byte[] key, int poin // keeps the prefix: verified both ways.) var old = new List(entries); old.RemoveAt(pos); - int oldShare = old.Count <= 1 ? 0 : CommonPrefixLength(old[0].Key, old[^1].Key); + int oldShare = Share(old); int stored = Math.Max(page.Compressed, oldShare); if (oldShare > page.Compressed) WriteOrThrow(leafPage, Build(PageType.LeafIndexPage, page.Previous, page.Next, 0, 0, old, oldShare)); @@ -440,10 +431,11 @@ private void InsertIntoLeaf(IndexDef index, List path, byte[] key, int poin else { long total = -(long)stored * (old.Count - 1); - foreach (Entry e in old) total += e.Key.Length + TrailerSize; + int trailerSize = _channel.Format.IndexEntryTrailerSize; + foreach (Entry e in old) total += e.Key.Length + trailerSize; int oldLeft = 0; for (long start = 0; oldLeft < old.Count && start * 2 < total; oldLeft++) - start += old[oldLeft].Key.Length + TrailerSize - (oldLeft == 0 ? 0 : stored); + start += old[oldLeft].Key.Length + trailerSize - (oldLeft == 0 ? 0 : stored); splitAt = pos < oldLeft ? oldLeft + 1 : oldLeft; } @@ -511,7 +503,7 @@ private void SplitAndPropagate(IndexDef index, List path, int level, List path, int level, List path, int level, int oldChild, byte[] promoted, int newRight) { int parentPage = path[level]; - CheckedIndexPage page = ReadMutationPage(parentPage, PageType.IntermediateIndexPage); + CheckedPage page = ReadMutationPage(parentPage, PageType.IntermediateIndexPage); (List entries, int tail) = Parse(page); // The old child's pointer is repointed first, so the page ACE compresses in place when the separator @@ -543,7 +535,7 @@ private void InsertSeparator(IndexDef index, List path, int level, int oldC // A node fills, compresses and splits exactly as a leaf does (see InsertIntoLeaf). int parentLevel = path.Count - 1 - level; - int share = CommonPrefixLength(entries[0].Key, entries[^1].Key); + int share = Share(entries); int keep = Math.Min(page.CompressedByteCount, share); if (Build(PageType.IntermediateIndexPage, page.Previous, page.Next, tail, parentLevel, entries, keep) is { } asIs) @@ -561,7 +553,7 @@ private void InsertSeparator(IndexDef index, List path, int level, int oldC } // As a leaf does, the full node is compressed in place before it splits (see InsertIntoLeaf). - int oldShare = old.Count <= 1 ? 0 : CommonPrefixLength(old[0].Key, old[^1].Key); + int oldShare = Share(old); if (oldShare > page.CompressedByteCount) WriteOrThrow(parentPage, Build(PageType.IntermediateIndexPage, page.Previous, page.Next, tail, parentLevel, old, oldShare)); @@ -605,7 +597,7 @@ read with if (!_channel.TryGetParsedPage(pageNumber, out object? cached) || cached is not ParsedIndexPage hit || hit.Owner != _table.DefinitionPage) return null; - CheckedIndexPage page = IndexPageReader.Read(_channel, pageNumber, _table.DefinitionPage); + CheckedPage page = Read(_channel, pageNumber, _table.DefinitionPage); (List entries, int tail) = Parse(page); return hit.Type == page.Type && hit.Owner == page.Owner && hit.Tail == tail && hit.Next == page.Next && hit.Previous == page.Previous && hit.Compressed == page.CompressedByteCount @@ -615,19 +607,19 @@ read with } /// Parses a checked page's entries, decompressing their shared prefix. - private static (List Entries, int Tail) Parse(CheckedIndexPage page) + private static (List Entries, int Tail) Parse(CheckedPage page) { var entries = new List(page.EntryRanges.Count); - foreach ((byte[] key, int trailer) in IndexPageReader.DecodeEntries(page)) + foreach ((byte[] key, int trailer) in DecodeEntries(page)) entries.Add(new Entry(key, trailer)); return (entries, page.Tail); } /// Revalidates a page immediately before mutation, closing the gap between B-tree descent and /// the final read-modify-write operation. - private CheckedIndexPage ReadMutationPage(int pageNumber, PageType expectedType) + private CheckedPage ReadMutationPage(int pageNumber, PageType expectedType) { - CheckedIndexPage page = IndexPageReader.Read(_channel, pageNumber, _table.DefinitionPage); + CheckedPage page = Read(_channel, pageNumber, _table.DefinitionPage); if (page.Type != expectedType) throw new InvalidDataException( $"Index mutation expected page {pageNumber} to be {expectedType}, but found {page.Type}."); @@ -636,7 +628,7 @@ private CheckedIndexPage ReadMutationPage(int pageNumber, PageType expectedType) /// Builds a page from entries; null if they overflow the page. Leaf and node pages alike are /// prefix-compressed at the length the caller gives, and a node carries its height above the leaves at - /// , matching what Access writes. (An isolation test showed a node's height is not + /// , matching what Access writes. (An isolation test showed a node's height is not /// required — Access reads a node with 0x1A=0 just fine; it is kept purely for byte-faithfulness. The /// one hard requirement is a leaf's 0x1A=0 and the leaf-chain offsets at 0x0C/0x10.) /// Leaf or node. @@ -648,39 +640,48 @@ private CheckedIndexPage ReadMutationPage(int pageNumber, PageType expectedType) /// The shared-prefix length to store the entries at. Null computes the largest /// available, which is what a split writes. It must not exceed what the entries actually share. private byte[]? Build(PageType type, int prev, int next, int tail, int level, List entries, - int? prefix = null) + int? prefix = null) => + BuildPage(_channel.Format, _table.DefinitionPage, type, prev, next, tail, level, entries, prefix); + + /// An empty leaf — a new index's root, owned by the table at . + internal static byte[] EmptyLeaf(JetFormatBase format, int owner) => + BuildPage(format, owner, PageType.LeafIndexPage, prev: 0, next: 0, tail: 0, level: 0, [])!; + + /// for a page owned by the table at . + private static byte[]? BuildPage(JetFormatBase format, int owner, PageType type, int prev, int next, int tail, + int level, List entries, int? prefix = null) { - int pageSize = _channel.PageSize; + int pageSize = format.PageSize; var page = new byte[pageSize]; PageHeader.WriteType(page, type); - page[LevelOffset] = (byte)level; // 0 on a leaf; the node's height above the leaves otherwise - WriteInt32Le(page, OwnerOffset, _table.DefinitionPage); - WriteInt32Le(page, PrevPageOffset, prev); - WriteInt32Le(page, NextPageOffset, next); - WriteInt32Le(page, ChildTailOffset, tail); + page[format.IndexLevelOffset] = (byte)level; // 0 on a leaf; the node's height above the leaves otherwise + WriteInt32Le(page, format.IndexOwnerOffset, owner); + WriteInt32Le(page, format.IndexPrevPageOffset, prev); + WriteInt32Le(page, format.IndexNextPageOffset, next); + WriteInt32Le(page, format.IndexChildTailOffset, tail); // A single entry has no common-prefix compression — ACE writes 0 here (the whole key with itself would // otherwise "compress" to its full length, which ACE does not do for one entry). Otherwise the caller // chooses, for a node as for a leaf: see InsertIntoLeaf for when a page is compressed at all. - int compress = entries.Count <= 1 - ? 0 - : prefix ?? CommonPrefixLength(entries[0].Key, entries[^1].Key); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(CompressedByteCountOffset, 2), (ushort)compress); + int compress = entries.Count <= 1 ? 0 : prefix ?? Share(entries); + BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.IndexCompressedByteCountOffset, 2), (ushort)compress); - int pos = EntryDataOffset; + int pos = format.IndexEntryDataOffset; bool first = true; - Span trailer = stackalloc byte[TrailerSize]; + Span trailer = stackalloc byte[format.IndexEntryTrailerSize]; + // Bit k of the entry mask ends an entry at offset k of the entry-data region. + Span entryMask = page.AsSpan(format.IndexEntryMaskOffset, format.IndexEntryDataOffset - format.IndexEntryMaskOffset); foreach (Entry e in entries) { // The prefix covers the entry WHOLE — key ++ trailer — so where many rows share a key it reaches // past the key into the row pointer, and what is stored is the tail of that concatenation. ACE - // writes leaves this way and IndexPageReader.DecodeEntries reads them back the same way; taking + // writes leaves this way and DecodeEntries reads them back the same way; taking // the tail of the key alone throws on exactly those pages. int skip = first ? 0 : compress; first = false; int keySkip = Math.Min(skip, e.Key.Length); int trailerSkip = skip - keySkip; - int len = e.Key.Length - keySkip + TrailerSize - trailerSkip; + int len = e.Key.Length - keySkip + trailer.Length - trailerSkip; if (pos + len > pageSize) return null; // overflow BinaryPrimitives.WriteInt32BigEndian(trailer, e.Trailer); @@ -688,57 +689,48 @@ private CheckedIndexPage ReadMutationPage(int pageNumber, PageType expectedType) trailer[trailerSkip..].CopyTo(page.AsSpan(pos + e.Key.Length - keySkip)); pos += len; - int end = pos - EntryDataOffset; - page[EntryMaskOffset + (end >> 3)] |= (byte)(1 << (end & 7)); + BitmapBits.Set(entryMask, pos - format.IndexEntryDataOffset, true); } - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(FreeSpaceOffset, 2), (ushort)(pageSize - pos)); + BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.IndexFreeSpaceOffset, 2), (ushort)(pageSize - pos)); return page; } - /// Repoints the index-data block's B-tree root (0x26) — when a new table's foreign-key index is given - /// its root (a split never moves one). Walks stats → column descriptors → column names → data blocks to the - /// index's block. + /// Repoints the index-data block's B-tree root — when a new table's foreign-key index is given its + /// root (a split never moves one) — through . /// /// A wide table's definition spans continuation pages, and the data blocks sit past the column names — - /// well beyond the first page for a 255-column table. The walk therefore runs over the stitched - /// definition (the absolute coordinate space the descriptors use), and only the 4 root bytes are written - /// back, mapped to whichever page actually holds them. Nothing changes length, so no re-split is needed. + /// well beyond the first page for a 255-column table. The block is therefore found in the stitched + /// definition (the absolute coordinate space the descriptors use), and only its bytes are written back, + /// mapped to whichever pages actually hold them. Nothing changes length, so no re-split is needed. /// internal void UpdateIndexRoot(IndexDef index, int newRoot) { - (_, IReadOnlyList continuations, int block) = LocateIndexBlock(index); - WriteInt32IntoDefinition(continuations, block + IndexBlockFormat.RootPageOffset, newRoot); + (byte[] tdef, IReadOnlyList continuations, int block) = LocateIndexBlock(index); + Span data = tdef.AsSpan(block, _channel.Format.IndexDataBlockSize); + TableDefinition.WriteIndexTree(data, _channel.Format, newRoot, index.UsageMap.Row, index.UsageMap.Page); + _table.WriteIntoDefinition(_channel, continuations, block, data); // The root pointer is part of the definition every other handle caches: until they reload it they // descend from the old root — by now only a page inside the tree — and an insert through one would // split that page as if it were the root and write it over this one. _channel.MarkSchemaChanged(); } - /// The (row, page) pointer to the index's own pages usage map, read from its data block. - private (int MapRow, int MapPage) IndexUsageMapPointer(IndexDef index) - { - (byte[] tdef, _, int block) = LocateIndexBlock(index); - int pointer = block + IndexBlockFormat.UsageMapRowOffset; // 1-byte row + 3-byte page - int row = tdef[pointer]; - int mapPage = tdef[pointer + 1] | tdef[pointer + 2] << 8 | tdef[pointer + 3] << 16; - return (row, mapPage); - } - /// Walks the stitched definition (stats → column descriptors → column names → data blocks) to - /// the index's 52-byte data block, returning the buffer, its continuation pages, and the block's absolute + /// the index's data block, returning the buffer, its continuation pages, and the block's absolute /// offset. A wide table's blocks sit past the column names, well beyond the first page. private (byte[] Definition, IReadOnlyList Continuations, int BlockOffset) LocateIndexBlock(IndexDef index) { (byte[] tdef, IReadOnlyList continuations) = ReadDefinition(); - TdefRegions regions = TdefRegions.Of(tdef, _channel.Format); + JetFormatBase format = _channel.Format; + TableDefinition.Regions regions = TableDefinition.Regions.Of(tdef, format); // Bounded like every region before it, because this offset decides where UpdateIndexRoot writes: an // ordinal past the blocks would otherwise repoint whatever follows them. - int block = TableDefinitionPage.CheckedRegionEnd( - regions.DataBlocks, index.RealIndexOrdinal + 1, IndexBlockFormat.DataBlockSize, tdef.Length, + int block = TableDefinition.CheckedRegionEnd( + regions.DataBlocks, index.RealIndexOrdinal + 1, format.IndexDataBlockSize, tdef.Length, "index-data blocks") - - IndexBlockFormat.DataBlockSize; + - format.IndexDataBlockSize; return (tdef, continuations, block); } @@ -749,8 +741,7 @@ internal void UpdateIndexRoot(IndexDef index, int newRoot) private int AllocateIndexPage(IndexDef index) { int page = _allocator.Allocate(); - (int mapRow, int mapPage) = IndexUsageMapPointer(index); - _usageMaps.SetBit(mapRow, mapPage, page, set: true); + _usageMaps.SetBit(index.UsageMap.Row, index.UsageMap.Page, page, set: true); return page; } @@ -759,56 +750,16 @@ private int AllocateIndexPage(IndexDef index) private (byte[] Definition, IReadOnlyList ContinuationPages) ReadDefinition() { (PageBuffer buffer, IReadOnlyList continuations) = - TdefChainReader.Read(_channel, _table.DefinitionPage); + TableDefinition.ReadChain(_channel, _table.DefinitionPage); return (buffer.Span.ToArray(), continuations); } - /// Maps an absolute definition offset to the page holding it and the offset within that page. - private (int Page, int Offset) MapDefinitionOffset(IReadOnlyList continuations, int offset) - { - int pageSize = _channel.Format.PageSize; - if (offset < pageSize) return (_table.DefinitionPage, offset); - - int body = pageSize - JetFormatBase.TdefContinuationHeaderSize; - int relative = offset - pageSize; - int index = relative / body; - if (index >= continuations.Count) - throw new InvalidOperationException( - $"Definition offset {offset} lies past the end of table '{_table.Name}'s definition chain."); - return (continuations[index], JetFormatBase.TdefContinuationHeaderSize + relative % body); - } - - /// Writes 4 little-endian bytes at an absolute definition offset, splitting the write when the - /// field straddles a continuation-page boundary. - private void WriteInt32IntoDefinition(IReadOnlyList continuations, int offset, int value) - { - Span bytes = stackalloc byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(bytes, value); - - for (int i = 0; i < 4;) - { - int pageNumber = MapDefinitionOffset(continuations, offset + i).Page; - byte[] page = _channel.ReadPage(pageNumber).Span.ToArray(); - - int j = i; - for (; j < 4; j++) - { - (int target, int within) = MapDefinitionOffset(continuations, offset + j); - if (target != pageNumber) break; - page[within] = bytes[j]; - } - - _channel.WritePage(pageNumber, page); - i = j; - } - } - - /// Patches one end of a neighbouring page's sibling link — or - /// — without disturbing its entries. A split repairs the back-link of the page + /// Patches one end of a neighbouring page's sibling link — + /// or — without disturbing its entries. A split repairs the back-link of the page /// it pushed right, leaf or node; unlinking an emptied leaf repairs both of its neighbours. private void SetSiblingLink(int pageNumber, int offset, int target, PageType type) { - CheckedIndexPage checkedPage = IndexPageReader.Read(_channel, pageNumber, _table.DefinitionPage); + CheckedPage checkedPage = Read(_channel, pageNumber, _table.DefinitionPage); if (checkedPage.Type != type) throw new InvalidDataException( $"Sibling pointer targets page {pageNumber}, a {checkedPage.Type} where a {type} was expected."); @@ -853,7 +804,7 @@ private byte[] ImageOf(int pageNumber) // A leaf turning node — the root, when it splits — still counts as the same page rewritten. ReadOnlySpan existing = _channel.ReadPage(pageNumber).Span; if (PageHeader.ReadType(existing) is not (PageType.LeafIndexPage or PageType.IntermediateIndexPage) - || BinaryPrimitives.ReadInt32LittleEndian(existing[OwnerOffset..]) != _table.DefinitionPage) + || ReadOwner(existing, _channel.Format) != _table.DefinitionPage) return new byte[_channel.PageSize]; return existing.ToArray(); } @@ -870,7 +821,7 @@ private byte[] Merge(byte[] built, byte[] image) /// prefix: ACE writes it into the page and then ends the page before it. private void LeaveMiddleBehind(byte[] leftBytes, int prev, int right, int level, List left, Entry middle) { - int prefix = BinaryPrimitives.ReadUInt16LittleEndian(leftBytes.AsSpan(CompressedByteCountOffset, 2)); + int prefix = ReadCompressedByteCount(leftBytes, _channel.Format); byte[] withMiddle = Build(PageType.IntermediateIndexPage, prev, right, middle.Trailer, level, [.. left, middle], prefix)!; int leftEnd = LiveEnd(leftBytes); withMiddle.AsSpan(leftEnd, LiveEnd(withMiddle) - leftEnd).CopyTo(leftBytes.AsSpan(leftEnd)); @@ -878,14 +829,14 @@ private void LeaveMiddleBehind(byte[] leftBytes, int prev, int right, int level, /// Where a built page's entries end: everything past it is free space. private int LiveEnd(byte[] page) => - _channel.PageSize - BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(FreeSpaceOffset, 2)); - - /// Width of the row/child pointer an entry carries after its key. - private const int TrailerSize = 4; + _channel.PageSize - ReadFreeSpace(page, _channel.Format); + // The trailer is carried as an int and written big-endian (WriteInt32Be), so the helpers below that build or + // compare key ++ trailer without a page work in sizeof(int); the page layout uses the format's + // IndexEntryTrailerSize. private static byte[] WithTrailer(byte[] key, int trailer) { - var result = new byte[key.Length + TrailerSize]; + var result = new byte[key.Length + sizeof(int)]; key.CopyTo(result, 0); WriteInt32Be(result, key.Length, trailer); return result; @@ -1055,13 +1006,16 @@ private void SplitSpine(IndexDef index, List spine, int level, Entry Append(index, spine, level + 1, new Entry(promoted, left)); } + /// The prefix every entry shares — the first and last, since they are in key order. None for a + /// single entry, which ACE stores whole. private static int Share(List entries) => entries.Count <= 1 ? 0 : CommonPrefixLength(entries[0].Key, entries[^1].Key); /// Whether entries totalling of key data fit /// one leaf at prefix — Build's layout, without building anything. private bool LeafFits(int count, long keyBytes, int prefix) => - EntryDataOffset + keyBytes + (long)TrailerSize * count - (long)prefix * (count - 1) <= _channel.PageSize; + _channel.Format.IndexEntryDataOffset + keyBytes + (long)_channel.Format.IndexEntryTrailerSize * count + - (long)prefix * (count - 1) <= _channel.PageSize; /// Orders two entries as their stored key ++ trailer bytes compare, without building either. internal static int CompareEntries(byte[] aKey, int aTrailer, byte[] bKey, int bTrailer) @@ -1104,12 +1058,12 @@ private static int CompareTrailers(int a, int b) /// internal static int CompareWithTrailer(byte[] key, int trailer, ReadOnlySpan other) { - int total = key.Length + TrailerSize; + int total = key.Length + sizeof(int); int n = Math.Min(total, other.Length); for (int i = 0; i < n; i++) { // Past the key, read the trailer's bytes most-significant first, as WriteInt32Be lays them out. - int mine = i < key.Length ? key[i] : (byte)(trailer >> (8 * (TrailerSize - 1 - (i - key.Length)))); + int mine = i < key.Length ? key[i] : (byte)(trailer >> (8 * (sizeof(int) - 1 - (i - key.Length)))); if (mine != other[i]) return mine - other[i]; } @@ -1133,4 +1087,169 @@ private static int CompareBytes(ReadOnlySpan a, ReadOnlySpan b) private static void WriteInt32Le(byte[] page, int offset, int value) => BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(offset, 4), value); private static void WriteInt32Be(byte[] page, int offset, int value) => BinaryPrimitives.WriteInt32BigEndian(page.AsSpan(offset, 4), value); + + /// A checked index page. Each of is a stored entry's offsets within the page. + internal sealed record CheckedPage( + PageBuffer Buffer, JetFormatBase Format, PageType Type, int Owner, int Previous, int Next, int Tail, + int CompressedByteCount, IReadOnlyList<(int Start, int End)> EntryRanges); + + + + /// An index page's owner field: the TDEF page of the table whose index it belongs to. Unchecked — the + /// caller has established that is an index page. + internal static int ReadOwner(ReadOnlySpan page, JetFormatBase format) => + BinaryPrimitives.ReadInt32LittleEndian(page.Slice(format.IndexOwnerOffset, sizeof(int))); + + /// An index page's free-space field: the bytes past the end of its entries. + internal static int ReadFreeSpace(ReadOnlySpan page, JetFormatBase format) => + BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(format.IndexFreeSpaceOffset, sizeof(ushort))); + + /// An index page's compressed-byte count: the prefix every entry after the first shares with the first. + internal static int ReadCompressedByteCount(ReadOnlySpan page, JetFormatBase format) => + BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(format.IndexCompressedByteCountOffset, sizeof(ushort))); + + /// An index page's previous and next sibling pointers, 0 at either end of a level. + internal static (int Previous, int Next) ReadSiblings(ReadOnlySpan page, JetFormatBase format) => + (BinaryPrimitives.ReadInt32LittleEndian(page.Slice(format.IndexPrevPageOffset, sizeof(int))), + BinaryPrimitives.ReadInt32LittleEndian(page.Slice(format.IndexNextPageOffset, sizeof(int)))); + + internal static CheckedPage Read(PageChannel channel, int pageNumber, int? expectedOwner) + { + ValidatePageNumber(channel, pageNumber, "index page"); + PageBuffer buffer = channel.ReadPageShared(pageNumber); + var type = PageHeader.ReadType(buffer.Span); + if (type is not (PageType.LeafIndexPage or PageType.IntermediateIndexPage)) + throw new InvalidDataException( + $"Page {pageNumber} is type 0x{(ushort)type:X4}, not an index page (0x0103/0x0104)."); + + JetFormatBase format = channel.Format; + int owner = ReadOwner(buffer.Span, format); + if (expectedOwner is not null && owner != expectedOwner) + throw new InvalidDataException( + $"Index page {pageNumber} belongs to TDEF {owner}, not TDEF {expectedOwner}."); + + (int previous, int next) = ReadSiblings(buffer.Span, format); + int tail = buffer.ReadInt32(format.IndexChildTailOffset); + if (type == PageType.LeafIndexPage) + { + ValidateOptionalPageNumber(channel, previous, "previous leaf"); + ValidateOptionalPageNumber(channel, next, "next leaf"); + } + else + { + 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 = ReadCompressedByteCount(buffer.Span, format); + + // Bit k of the entry mask ends an entry at offset k of the entry-data region. + int dataOffset = format.IndexEntryDataOffset; + ReadOnlySpan entryMask = buffer.Slice(format.IndexEntryMaskOffset, dataOffset - format.IndexEntryMaskOffset); + var ranges = new List<(int Start, int End)>(); + int start = dataOffset; + for (int bit = BitmapBits.NextSetBit(entryMask, 0); bit >= 0; bit = BitmapBits.NextSetBit(entryMask, bit + 1)) + { + int end = dataOffset + bit; + if (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 < format.IndexEntryTrailerSize) + throw new InvalidDataException( + $"Index page {pageNumber} entry [{start}, {end}) reconstructs to {length} bytes, " + + $"too few for its {format.IndexEntryTrailerSize}-byte trailer."); + ranges.Add((start, end)); + start = end; + } + + 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) + throw new InvalidDataException( + $"Index page {pageNumber} compressed prefix {compressed} exceeds its first entry."); + + var page = new CheckedPage(buffer, format, 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 child in Trailers(page)) + ValidatePageNumber(channel, child, "node child"); + + return page; + } + + /// Decodes a checked page's entries in order, decompressing each entry's shared prefix: the first + /// entry is stored whole and its leading CompressedByteCount bytes are the prefix reapplied to every + /// following entry. Yields the full key bytes and the 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. + /// + /// 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. + /// + internal static IEnumerable<(byte[] Key, int Trailer)> DecodeEntries(CheckedPage page) + { + byte[] prefix = []; + bool first = true; + foreach ((int start, int end) in page.EntryRanges) + { + ReadOnlySpan stored = page.Buffer.Slice(start, end - start); + ReadOnlySpan lead = first ? [] : prefix; + // The key is the reconstructed entry less its trailer, copied straight from the prefix and the stored + // bytes into the one array returned — when the prefix reaches into the trailer, all of it is prefix. + var key = new byte[lead.Length + stored.Length - page.Format.IndexEntryTrailerSize]; + int fromLead = Math.Min(lead.Length, key.Length); + lead[..fromLead].CopyTo(key); + stored[..(key.Length - fromLead)].CopyTo(key.AsSpan(fromLead)); + int trailer = Trailer(lead, stored, page.Format); + if (first) { prefix = stored[..page.CompressedByteCount].ToArray(); first = false; } + yield return (key, trailer); + } + } + + /// A checked page's entry trailers in order — a node's child pages, a leaf's row pointers — read from the + /// reconstructed entries as reads them, without building the keys. + internal static List Trailers(CheckedPage page) + { + var trailers = new List(page.EntryRanges.Count); + ReadOnlySpan lead = []; + for (int i = 0; i < page.EntryRanges.Count; i++) + { + (int start, int end) = page.EntryRanges[i]; + ReadOnlySpan stored = page.Buffer.Slice(start, end - start); + trailers.Add(Trailer(lead, stored, page.Format)); + if (i == 0) lead = stored[..page.CompressedByteCount]; + } + return trailers; + } + + /// The big-endian trailer of the entry stored as after + /// — the shared prefix it omits, empty for the first entry — read from the entry as + /// reconstructed, since the prefix can reach into it. + private static int Trailer(ReadOnlySpan lead, ReadOnlySpan stored, JetFormatBase format) + { + int length = lead.Length + stored.Length, trailer = 0; + for (int i = length - format.IndexEntryTrailerSize; i < length; i++) + trailer = (trailer << 8) | (i < lead.Length ? lead[i] : stored[i - lead.Length]); + return trailer; + } + + private static void ValidateOptionalPageNumber(PageChannel channel, int pageNumber, string kind) + { + if (pageNumber != 0) ValidatePageNumber(channel, pageNumber, kind); + } + + private static void ValidatePageNumber(PageChannel channel, int pageNumber, string kind) + { + if (pageNumber <= 0 || pageNumber >= channel.PageCount) + throw new InvalidDataException( + $"Index {kind} pointer {pageNumber} is outside the file's 1..{channel.PageCount - 1} range."); + } + } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/JetIndexKeyChecksum.cs b/src/LibRed/LibRed.Core/Storage/JetIndexKeyChecksum.cs deleted file mode 100644 index 935bd233..00000000 --- a/src/LibRed/LibRed.Core/Storage/JetIndexKeyChecksum.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace LibRed.Storage; - -/// -/// The two bytes ACE puts at the end of an index key too long to store whole. -/// -/// -/// A key 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. -/// -/// -/// It holds where the dropped bytes contain a word-sort record too, which was long assumed -/// uncheckable: the record sits in the part ACE discarded, so what it held looked unobservable, and ACE might -/// have recomputed its position when truncating. Neither is so — the position byte tracks where the mark -/// actually sat, and the checksum over the reconstructed key matches ACE's for both mark characters at -/// positions spread through the value. 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 last byte does not go through a full step. Every byte before it is folded in the usual way, and - /// then the final one is XORed into the high half — it never gets its own shift or table lookup. - /// This was first written as "the terminator is excluded", which is the same thing whenever that - /// byte is 0x00: XOR-ing zero changes nothing. A key ending in text always ends in its 0x00 - /// terminator, and every measurement behind the original rule used one, so the two readings could not be - /// told apart. They diverge the moment the key's last column is numeric — (TEXT, TEXT, LONG) put - /// a data byte there and the keys parted company from ACE's, silently. Re-measured over LONG, CURRENCY - /// and DOUBLE tails across 24 keys: this form matches ACE on every one, and still matches on the all-text - /// keys the old form was derived from. See docs/design/index-key-checksum.md. - /// - public static ushort Compute(ReadOnlySpan discarded) - { - ushort crc = 0; - foreach (byte b in discarded[..^1]) crc = (ushort)((crc >> 8) ^ Table[crc & 0xFF] ^ b); - return (ushort)(crc ^ (discarded[^1] << 8)); - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs b/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs index 386909a2..c93306f2 100644 --- a/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs +++ b/src/LibRed/LibRed.Core/Storage/JetKanaSection.cs @@ -16,10 +16,10 @@ 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 mark codes. + /// Closes the kana section, after the mark codes. /// 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]; + private static ReadOnlySpan Tail => [0x02, 0x80, IndexKeyCodec.KanaRunSeparator, 0x80]; /// The mark code of a kana that is a letter in its own right. public const byte Letter = 0b01; @@ -51,7 +51,8 @@ public static bool TryGetIterationMark(char character, bool version1, out byte s secondary = character switch { (char)0x3005 => 0x05, // 々 - (char)0x309D or (char)0x30FD or (char)0x3031 or (char)0x30FC or (char)0xFF70 => 0x02, // ゝ ヽ 〱 ー ー + (char)0x309D or (char)0x30FD or (char)0x3031 => 0x02, // ゝ ヽ 〱 + _ when IsProlongedSoundMark(character) => 0x02, // ー ー, with no kana to lengthen (char)0x309E or (char)0x30FE or (char)0x3032 => 0x03, // ゞ ヾ 〲 — voiced (char)0x303B when version1 => 0x05, // 〻 (char)0xA015 when version1 => 0x07, // ꀕ @@ -60,6 +61,25 @@ public static bool TryGetIterationMark(char character, bool version1, out byte s return secondary != 0; } + /// Whether is a prolonged sound mark — ー or halfwidth ー. Right + /// after a kana it lengthens that kana's vowel, taking the vowel's primary and the kana's small flag and marking + /// itself ; with no kana before it, it is an iteration mark. + public static bool IsProlongedSoundMark(char character) => character is (char)0x30FC or (char)0xFF70; + + /// Whether is a halfwidth voicing mark, and the secondary it gives the kana + /// right before it: ゙ voices it (03), ゚ semi-voices it (04). The marks are combining — + /// ACE folds them into that kana rather than weighing them — and with no kana before them they are weighed alone. + public static bool TryGetHalfwidthVoicing(char character, out byte secondary) + { + secondary = character switch + { + (char)0xFF9E => 0x03, + (char)0xFF9F => 0x04, + _ => 0, + }; + return secondary != 0; + } + /// /// Appends 01 01, the packed small/normal flags, the mark codes and the closing constant. /// Emitted whenever the string holds any kana at all, even if every one of them is a normal form. @@ -69,10 +89,10 @@ public static bool TryGetIterationMark(char character, bool version1, out byte s /// Per kana, , or . public static void Append(List output, List small, List marks) { - output.Add(0x01); - output.Add(0x01); + output.Add(IndexKeyCodec.SectionSeparator); // ends the diacritics + output.Add(IndexKeyCodec.SectionSeparator); // ends the empty case section AddCodes(output, small.Count, i => small[i] ? (byte)0b10 : (byte)0b11, unmarked: 0b11); - output.Add(0xFF); + output.Add(IndexKeyCodec.KanaRunSeparator); AddCodes(output, marks.Count, i => marks[i], unmarked: Letter); output.AddRange(Tail); } diff --git a/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs b/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs index a6c621f8..a33ec150 100644 --- a/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs +++ b/src/LibRed/LibRed.Core/Storage/JetLocaleTailoring.cs @@ -1,5 +1,6 @@ using LibRed.Catalog; using System.Collections.Frozen; +using static LibRed.Storage.IndexKeyCodec; namespace LibRed.Storage; @@ -186,12 +187,10 @@ private bool TryGet(ReadOnlySpan key, out TailoredWeight weight) /// 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). +/// whole of printable ASCII, Latin-1 and Latin Extended-A (CreatedDatabaseCollationAccessTests). /// 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. diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs index 36f8627a..43e3f907 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollation.cs @@ -1,3 +1,5 @@ +using static LibRed.Storage.IndexKeyCodec; + namespace LibRed.Storage; /// @@ -14,7 +16,7 @@ namespace LibRed.Storage; /// base letter's primary weight and record the accent in a secondary section (see below); a handful of /// other characters are still reported unencodable. /// -/// The sort-order version is the column descriptor's 0x0D field (spec §3.4). Access 2010+ +/// The sort-order version is the column descriptor's 0x0E field (spec §3.4). Access 2010+ /// introduced a different default "General" order (1033, version 1) with other key bytes; this class /// does not implement it. A version-1 column/index therefore needs a separate weight table — /// see the spec §10.4 note. @@ -22,11 +24,6 @@ namespace LibRed.Storage; /// internal static class JetTextCollation { - private const byte EndPrimary = 0x01; - private const byte EndKey = 0x00; - private const byte InlineStart = 0x80; - private const byte InlineMid = 0x06; - /// ARABIC SHADDA — the gemination mark. Not an ignorable like the harakat around it: it /// doubles the preceding weight. See the rule in TryEncode. private const char Shadda = (char)0x0651; @@ -70,7 +67,6 @@ internal static class JetTextCollation [(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 // letter (verified against ACE: acute weighs 0x0E on a/e/i/o/u/y alike, etc.). @@ -250,19 +246,6 @@ void Add(int ligature, params int[] components) 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. - /// - /// The text to encode. - /// The key body is appended to this. - /// 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 _); - /// The word-sort inline code ACE gives a control character other than NUL and tab through carriage /// return: the 60 of them — U+0001–0008, U+000E–001F, DEL and U+0080–009F — take consecutive codes /// 0x03–0x3D in code-point order (measured against ACE, every one). @@ -280,19 +263,105 @@ private static bool TryGetControlInlineCode(char c, out byte code) return value is not null; } - [ThreadStatic] private static EncodeScratch? t_scratch; + /// + /// Ends a text key the way both sort-order versions end it, from the weights a version has worked out: the + /// primaries, the diacritic section, the kana section, the inline records, and . + /// [MS-UCODEREF] frames the key as primaries SEP diacritics SEP case SEP extra SEP specials TERM, and Access + /// emits that frame leaving the case section EMPTY — which is why case and width fold, since the Case Weight is + /// where width lives. + /// + /// The key being built. + /// The primary weight bytes. + /// One diacritic weight per primary weight. + /// A French-style order, which writes the diacritic section backwards. + /// Per kana, whether it is a small form. + /// Per kana, its mark code. + /// The inline records: each one's position in primary weights, script member and weight. + internal static void AppendSections(List output, List primaries, List secondaries, + bool reverseDiacritics, List kana, List marks, + List<(int Position, byte ScriptMember, byte Weight)> inline) + { + output.AddRange(primaries); + output.Add(SectionSeparator); + + // Diacritic section: only emitted when a character carries a non-default weight; it lists the weight of + // every primary weight from the first up to and including the last accented one. + // + // 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 (reverseDiacritics) + { + 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]); + } + + if (kana.Count > 0) JetKanaSection.Append(output, kana, marks); + + // The inline records follow the remaining separators: the end of the diacritics, an empty case section + // and an empty extra section — 01 01 01. A kana section fills the extra section, and the run is then + // FF 01 (measured from "あ-" and "-あ"; what the FF denotes is not established). + if (inline.Count > 0) + { + if (kana.Count > 0) + { + output.Add(KanaRunSeparator); + output.Add(SectionSeparator); + } + else + { + output.Add(SectionSeparator); + output.Add(SectionSeparator); + output.Add(SectionSeparator); + } + foreach ((int position, byte scriptMember, byte weight) 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(weight); + } + } + output.Add(EndKey); + } - /// The lists builds a key in, - /// kept per thread and reused. - private sealed class EncodeScratch + /// The lists a key is built in — by and by version 1's encoder alike — kept per + /// thread and reused. A text comparison encodes both sides every time, and building five lists per string made + /// a GROUP BY over a text column spend most of its time collecting them. Safe because neither encoder re-enters + /// either, and both are done with the lists once has written the key. + internal sealed class EncodeScratch { + [ThreadStatic] private static EncodeScratch? t_current; + + /// This thread's lists, cleared. + public static EncodeScratch ForThisThread() + { + EncodeScratch scratch = t_current ??= new EncodeScratch(); + scratch.Clear(); + return scratch; + } + public List Primaries { get; } = []; public List Secondaries { get; } = []; - public List<(int Position, byte Code)> Inline { get; } = []; + public List<(int Position, byte ScriptMember, byte Weight)> Inline { get; } = []; public List Kana { get; } = []; public List KanaMarks { get; } = []; - public void Clear() + private void Clear() { Primaries.Clear(); Secondaries.Clear(); @@ -302,26 +371,20 @@ public void Clear() } } + /// + /// Appends the order-preserving collation key body for (everything after the start + /// flag: primary weights, the section separators, any ignorable-char inline codes, and the terminator). + /// Trailing spaces are dropped. Returns false if any character is not yet supported. + /// /// The text to encode. /// The key body is appended to this. - /// Per-character overrides for a locale order other than General; null for General. - /// - /// 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) + /// 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) { - hasWordSortRecord = false; ReadOnlySpan s = value.AsSpan().TrimEnd(' '); - // The working lists are this thread's, cleared rather than allocated: a text comparison encodes both - // sides every time, and building five lists per string made a GROUP BY over a text column spend most of - // its time collecting them. Safe because nothing here re-enters TryEncode. - EncodeScratch scratch = t_scratch ??= new EncodeScratch(); - scratch.Clear(); + EncodeScratch scratch = EncodeScratch.ForThisThread(); // Build the primary weight bytes and a parallel secondary weight per byte (0x02 = no accent). List primaries = scratch.Primaries; @@ -332,7 +395,7 @@ public static bool TryEncode( // "ß-" 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. - List<(int Position, byte Code)> inline = scratch.Inline; + List<(int Position, byte ScriptMember, byte Weight)> inline = scratch.Inline; // One entry per kana in the string: true for a small form. Emitted as a section of its own. List kana = scratch.Kana; // What each kana is — a letter, a prolonged sound mark or an iteration mark. Packed like the small @@ -408,8 +471,8 @@ public static bool TryEncode( TryGetControlInlineCode(c, out code)) && tailoring?.TryMatchSingle(c, out _) != true) { - inline.Add((secondaries.Count, code)); - lastWeight = new CopiedPrimary(InlineMid, code); + inline.Add((secondaries.Count, WordSortScriptMember, code)); + lastWeight = new CopiedPrimary(WordSortScriptMember, code); lastWeightIsKana = false; continue; } @@ -421,7 +484,7 @@ public static bool TryEncode( // 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) + if (JetKanaSection.IsProlongedSoundMark(c) && kanaVowel != 0 && kanaWeight == secondaries.Count - 1) { AddWeight([JetKanaSection.KanaPage, kanaVowel], DefaultSecondary); kana.Add(kanaSmall); @@ -450,9 +513,10 @@ public static bool TryEncode( // 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) + if (JetKanaSection.TryGetHalfwidthVoicing(c, out byte voiced) && kanaWeight >= 0 + && kanaWeight == secondaries.Count - 1) { - secondaries[kanaWeight] = c == (char)0xFF9E ? (byte)0x03 : (byte)0x04; + secondaries[kanaWeight] = voiced; keepRepeatable = true; continue; } @@ -472,7 +536,7 @@ public static bool TryEncode( keepRepeatable = true; if (repeatable is null) { - primaries.AddRange((byte[])[0xFF, 0xFF]); + primaries.AddRange(UnweightedPrimary); continue; } AddWeight(repeatable.Value.ToArray(), markSecondary, final: true); @@ -505,7 +569,7 @@ public static bool TryEncode( if (lastWeight.Length > 0) AddWeight(lastWeight.ToArray(), DefaultSecondary, final: true); else - primaries.AddRange((byte[])[0xFF, 0xFF]); + primaries.AddRange(UnweightedPrimary); continue; } @@ -532,66 +596,7 @@ public static bool TryEncode( return false; // not handled yet } - output.AddRange(primaries); - output.Add(EndPrimary); - - // 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. - // - // 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, marks); - - // 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) - { - 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) - { - // [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); - } - } - output.Add(EndKey); + AppendSections(output, primaries, secondaries, tailoring?.ReverseDiacritics == true, kana, marks, inline); return true; // One character's weights, with no contraction and no tailoring: the path a ligature's components @@ -637,7 +642,7 @@ bool WeighCharacter(char character) // 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 + // `CreatedDatabaseCollationAccessTests` asserts the whole range for the locale cases, so a missed departure // fails rather than writing a silently wrong key. else if (JetTextCollationTableV0.TryGet(character, out TailoredWeight? block)) { diff --git a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs index 4e0ebe9f..7fdf7f67 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextCollationV1.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Compression; using System.Text; +using static LibRed.Storage.IndexKeyCodec; namespace LibRed.Storage; @@ -20,11 +21,6 @@ namespace LibRed.Storage; /// 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. @@ -32,11 +28,7 @@ internal static class JetTextCollationV1 // 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; + // PUNCTUATION 6 is IndexKeyCodec.WordSortScriptMember, shared with version 0. /// [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 @@ -71,31 +63,17 @@ internal static class JetTextCollationV1 /// 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 _); - /// The text to encode. /// The key body is appended to this. /// 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) + public static bool TryEncode(string value, List output, LocaleTailoring? tailoring = null) { - hasWordSortRecord = false; WeightTable table = Table.Value; ReadOnlySpan text = value.AsSpan().TrimEnd(' '); - // The working lists are this thread's, cleared rather than allocated, as version 0's are: a comparison - // encodes both sides every time, and five new lists per string were most of what a key allocated. Safe - // because nothing here re-enters TryEncode. - EncodeScratch scratch = t_scratch ??= new EncodeScratch(); - scratch.Clear(); + JetTextCollation.EncodeScratch scratch = JetTextCollation.EncodeScratch.ForThisThread(); List primaries = scratch.Primaries; List secondaries = scratch.Secondaries; @@ -105,7 +83,7 @@ public static bool TryEncode( // A Han character is the exception: its FD FF marker counts as a weight of its own, so 人- puts the // hyphen at 0x0F and 人人- at 0x17, though each Han character takes one secondary slot. A one-byte // primary still counts once (a Lao vowel then a hyphen is 0x0B), so this is not a byte count halved. - List<(int Position, byte ScriptMember, byte AlphabeticWeight)> inline = scratch.Inline; + List<(int Position, byte ScriptMember, byte Weight)> inline = scratch.Inline; int hanMarkers = 0; // The kana small/normal flags and mark codes, and the running state the prolonged mark needs. @@ -185,7 +163,7 @@ public static bool TryEncode( // 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 && + if (JetKanaSection.IsProlongedSoundMark(character) && kanaVowel != 0 && kanaWeight == secondaries.Count - 1) { primaries.Add(JetKanaSection.KanaPage); @@ -203,10 +181,10 @@ public static bool TryEncode( // 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 && + if (JetKanaSection.TryGetHalfwidthVoicing(character, out byte voiced) && kanaWeight >= 0 && kanaWeight == secondaries.Count - 1) { - secondaries[kanaWeight] = character == (char)0xFF9E ? (byte)0x03 : (byte)0x04; + secondaries[kanaWeight] = voiced; keepRepeatable = true; continue; } @@ -375,8 +353,7 @@ bool Append(char character) // 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.AddRange(HanPrimaryMarker); primaries.Add(alphabetic); primaries.Add(diacritic); secondaries.Add(DefaultSecondary); @@ -417,92 +394,15 @@ bool Append(char character) return true; } - output.AddRange(primaries); - output.Add(EndPrimary); - - // Secondary section: emitted up to and including the last character carrying a non-default accent — - // or BACKWARDS from the last to the first accented one for a French-style order, where the trimming - // mirrors too because the leading defaults become the trailing ones once reversed. Identical to v0's - // rule; ReverseDiacritics lives on the shared LocaleTailoring, and this encoder used to ignore it, - // so a v1 tailoring that set the flag was accepted by IsIndexKeyEncodable and then silently produced - // a forward section. Latent — the one order that sets it (French) is v0 — but a silently wrong key is - // the exact failure this subsystem exists to prevent, so the two encoders agree rather than differ. - if (tailoring?.ReverseDiacritics == true) - { - int firstAccent = secondaries.FindIndex(weight => weight != DefaultSecondary); - if (firstAccent >= 0) - for (int i = secondaries.Count - 1; i >= firstAccent; i--) output.Add(secondaries[i]); - } - else - { - 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, marks); - - // 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); + // ReverseDiacritics lives on the shared LocaleTailoring, and this encoder used to ignore it, so a v1 + // tailoring that set the flag was accepted by IsIndexKeyEncodable and then silently produced a forward + // section. Latent — the one order that sets it (French) is v0 — but the two versions now write the + // trailing sections through one method, so they cannot differ there again. + JetTextCollation.AppendSections(output, primaries, secondaries, tailoring?.ReverseDiacritics == true, + kana, marks, inline); return true; } - [ThreadStatic] private static EncodeScratch? t_scratch; - - /// The lists builds a key in, - /// kept per thread and reused. - private sealed class EncodeScratch - { - public List Primaries { get; } = []; - public List Secondaries { get; } = []; - public List<(int Position, byte ScriptMember, byte AlphabeticWeight)> Inline { get; } = []; - public List Kana { get; } = []; - public List KanaMarks { get; } = []; - - public void Clear() - { - Primaries.Clear(); - Secondaries.Clear(); - Inline.Clear(); - Kana.Clear(); - KanaMarks.Clear(); - } - } - private static WeightTable Load() { using Stream stream = typeof(JetTextCollationV1).Assembly diff --git a/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs b/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs index d26fa9f0..1319cc75 100644 --- a/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs +++ b/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs @@ -104,8 +104,8 @@ private void Encode(string text, List output) output.Clear(); bool encoded = _version1 - ? JetTextCollationV1.TryEncode(text, output, _tailoring, out _) - : JetTextCollation.TryEncode(text, output, _tailoring, out _); + ? JetTextCollationV1.TryEncode(text, output, _tailoring) + : JetTextCollation.TryEncode(text, output, _tailoring); // Both tables cover the whole BMP, and each order handles surrogates (CollationBmpCoverageTests), so // this cannot be reached by any string; refusing is still the answer if it ever is. @@ -113,4 +113,4 @@ private void Encode(string text, List output) throw new NotSupportedException( $"'{text}' holds a character the {Collation.Order} v{Collation.Version} order has no weight for."); } -} +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/LongValueReader.cs b/src/LibRed/LibRed.Core/Storage/LongValueReader.cs deleted file mode 100644 index d383df7d..00000000 --- a/src/LibRed/LibRed.Core/Storage/LongValueReader.cs +++ /dev/null @@ -1,153 +0,0 @@ -using LibRed.Formats; -using LibRed.IO; -using LibRed.Pages; - -namespace LibRed.Storage; - -/// -/// Resolves a long value (Memo / OLE) from its 12-byte in-row descriptor to the full -/// byte payload, following LVAL pages as needed. -/// -/// -/// Descriptor layout: bytes 0-3 = length with storage flags in the high two bits, bytes 4-7 = a -/// row+page pointer to the first LVAL chunk, bytes 8-11 the chain stamp (chained form only, checked against -/// the first chain page — see ). Flags: -/// 0x80 = inline (payload follows the descriptor); 0x40 = single LVAL page (the row is -/// the whole payload); otherwise the payload is chained across LVAL pages, each row -/// beginning with a 4-byte pointer to the next chunk. -/// -public sealed class LongValueReader(PageChannel channel) -{ - private readonly PageChannel _channel = channel; - - public byte[] Resolve(ReadOnlySpan descriptor) => ResolveWithPages(descriptor, out _); - - internal byte[] ResolveWithPages(ReadOnlySpan descriptor, out IReadOnlyList pages) - { - if (descriptor.Length < LongValueFormat.DescriptorSize) - throw new InvalidDataException( - $"Long-value descriptor has {descriptor.Length} bytes; expected at least {LongValueFormat.DescriptorSize}."); - - int length = System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(descriptor) & LongValueFormat.LengthMask; - byte flags = (byte)(descriptor[3] & LongValueFormat.FlagMask); - if (flags is not (LongValueFormat.FlagInline or LongValueFormat.FlagSinglePage or LongValueFormat.FlagChained)) - throw new InvalidDataException($"Long-value descriptor has unsupported flags 0x{flags:X2}."); - - if ((flags & LongValueFormat.FlagInline) != 0) - { - if (length > descriptor.Length - 12) - throw new InvalidDataException( - $"Inline long value declares {length} bytes but only {descriptor.Length - 12} are present."); - pages = []; - return descriptor.Slice(12, length).ToArray(); - } - - int row = descriptor[4]; - int page = descriptor[5] | (descriptor[6] << 8) | (descriptor[7] << 16); - - if ((flags & LongValueFormat.FlagSinglePage) != 0) - { - byte[] value = ReadLvalRow(page, row); - if (value.Length != length) - throw new InvalidDataException( - $"Single-page long value declares {length} bytes but row {page}:{row} has {value.Length}."); - pages = [page]; - return value; - } - - VerifyChainStamp(descriptor, page); - return ReadChain(page, row, length, out pages); - } - - /// - /// A chained descriptor and the first page of its chain carry the same 4-byte stamp, minted when - /// the chain was written. Disagreement means the page is no longer the one this descriptor was written - /// against — the chain was rewritten, or its pages were freed and reused under another value — so the - /// bytes behind the pointer belong to something else and must not be returned as this value. - /// - /// - /// ACE enforces this and reports it as "you and another user are attempting to change the same data at - /// the same time"; the diagnosis is the point, even if the wording is about the cause rather than what - /// was found. LibRed is single-writer, so it cannot produce the interleaving ACE guards against, but it - /// can be handed a file another engine wrote and must not read a stale chain as though it were live. - /// Only the entry page is checked, because every chunk after it is reached from a page already validated. - /// - private void VerifyChainStamp(ReadOnlySpan descriptor, int page) - { - if (page <= 0 || page >= _channel.PageCount) return; // ReadChain reports the bad pointer itself - - uint declared = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian( - descriptor.Slice(LongValueFormat.ChainStampOffset, 4)); - uint stored = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian( - _channel.ReadPageShared(page).Span.Slice(_channel.Format.DataChainStampOffset, 4)); - if (declared != stored) - throw new InvalidDataException( - $"Long-value chain at page {page} carries stamp 0x{stored:X8} but its descriptor declares " - + $"0x{declared:X8}; the chain is not the one this row was written against."); - } - - private byte[] ReadChain(int page, int row, int length, out IReadOnlyList pages) - { - var result = new byte[length]; - int written = 0; - var visited = new HashSet<(int Page, int Row)>(); - var chainPages = new List(); - - while (written < length) - { - if (page == 0) - throw new InvalidDataException( - $"Long-value chain ended after {written} of {length} declared bytes."); - if (!visited.Add((page, row))) - throw new InvalidDataException($"Long-value chain contains a cycle at row {page}:{row}."); - chainPages.Add(page); - - byte[] chunk = ReadLvalRow(page, row); - if (chunk.Length < 4) - throw new InvalidDataException( - $"Long-value chain row {page}:{row} has {chunk.Length} bytes; at least 4 are required for its next pointer."); - - // Each chained chunk starts with a 4-byte pointer (row + 3-byte page) to the next. - int nextRow = chunk[0]; - int nextPage = chunk[1] | (chunk[2] << 8) | (chunk[3] << 16); - - int copy = chunk.Length - 4; - if (copy == 0) - throw new InvalidDataException($"Long-value chain row {page}:{row} makes no payload progress."); - if (copy > length - written) - throw new InvalidDataException( - $"Long-value chain row {page}:{row} exceeds the declared length by {copy - (length - written)} bytes."); - Array.Copy(chunk, 4, result, written, copy); - written += copy; - - if (written == length && nextPage != 0) - throw new InvalidDataException( - $"Long-value chain continues to page {nextPage} after its declared {length} bytes."); - page = nextPage; - row = nextRow; - } - - pages = chainPages; - return result; - } - - private byte[] ReadLvalRow(int page, int row) - { - if (page <= 0 || page >= _channel.PageCount) - throw new InvalidDataException( - $"Long-value page pointer {page} is outside the file's 1..{_channel.PageCount - 1} range."); - - var lval = new DataPage(); - lval.Read(_channel.ReadPage(page), _channel.Format); - if (!lval.IsLongValuePage) - throw new InvalidDataException($"Long-value pointer {page}:{row} targets a non-LVAL data page."); - if (row < 0 || row >= lval.RowCount) - throw new InvalidDataException( - $"Long-value row pointer {page}:{row} is outside the page's 0..{lval.RowCount - 1} range."); - RowSlot slot = lval.Rows[row]; - if (slot.IsDeleted || slot.HasOverflow) - throw new InvalidDataException( - $"Long-value pointer {page}:{row} targets a deleted or overflow row slot."); - return lval.GetRow(row).ToArray(); - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/LongValueStore.cs b/src/LibRed/LibRed.Core/Storage/LongValueStore.cs new file mode 100644 index 00000000..e1811c2d --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/LongValueStore.cs @@ -0,0 +1,333 @@ +using LibRed.Formats; +using LibRed.IO; +using LibRed.Pages; +using System.Buffers.Binary; + +namespace LibRed.Storage; + +/// +/// Reads and writes a long value (Memo / OLE), owning its in-row descriptor and full +/// byte payload, following LVAL pages as needed. +/// +/// +/// The descriptor () carries the length and its , a +/// record pointer to the first LVAL chunk, and the chain stamp (chained form only, checked against the first chain +/// page — see ). An inline payload follows the descriptor; a single-page value is the +/// whole of its row; a chained one runs across LVAL pages, each row beginning with a record pointer to the next +/// chunk. +/// +public sealed class LongValueStore(PageChannel channel) +{ + /// + /// A pre-built long-value (memo/OLE) in-row descriptor, written verbatim by the row encoder instead of + /// inlining. Produced by when a value is stored on an LVAL page. + /// + internal sealed record DescriptorValue(byte[] Bytes); + + private readonly PageChannel _channel = channel; + private readonly PageAllocator _allocator = channel.Allocator; + + public byte[] Resolve(ReadOnlySpan descriptor) => ResolveWithPages(descriptor, out _); + + internal byte[] ResolveWithPages(ReadOnlySpan descriptor, out IReadOnlyList pages) + { + JetFormatBase format = _channel.Format; + (int length, StorageKind storage, int row, int page, uint stamp) = Read(descriptor, format); + + if (storage == StorageKind.Inline) + { + int size = format.LongValueDescriptorSize; + if (length > descriptor.Length - size) + throw new InvalidDataException( + $"Inline long value declares {length} bytes but only {descriptor.Length - size} are present."); + pages = []; + return descriptor.Slice(size, length).ToArray(); + } + + if (storage == StorageKind.SinglePage) + { + byte[] value = ReadLvalRow(page, row); + if (value.Length != length) + throw new InvalidDataException( + $"Single-page long value declares {length} bytes but row {page}:{row} has {value.Length}."); + pages = [page]; + return value; + } + + VerifyChainStamp(stamp, page); + return ReadChain(page, row, length, out pages); + } + + /// + /// A chained descriptor and the first page of its chain carry the same 4-byte stamp, minted when + /// the chain was written. Disagreement means the page is no longer the one this descriptor was written + /// against — the chain was rewritten, or its pages were freed and reused under another value — so the + /// bytes behind the pointer belong to something else and must not be returned as this value. + /// + /// + /// ACE enforces this and reports it as "you and another user are attempting to change the same data at + /// the same time"; the diagnosis is the point, even if the wording is about the cause rather than what + /// was found. LibRed is single-writer, so it cannot produce the interleaving ACE guards against, but it + /// can be handed a file another engine wrote and must not read a stale chain as though it were live. + /// Only the entry page is checked, because every chunk after it is reached from a page already validated. + /// + private void VerifyChainStamp(uint declared, int page) + { + if (page <= 0 || page >= _channel.PageCount) return; // ReadChain reports the bad pointer itself + + uint stored = BinaryPrimitives.ReadUInt32LittleEndian( + _channel.ReadPageShared(page).Span.Slice(_channel.Format.DataChainStampOffset, sizeof(uint))); + if (declared != stored) + throw new InvalidDataException( + $"Long-value chain at page {page} carries stamp 0x{stored:X8} but its descriptor declares " + + $"0x{declared:X8}; the chain is not the one this row was written against."); + } + + private byte[] ReadChain(int page, int row, int length, out IReadOnlyList pages) + { + var result = new byte[length]; + int written = 0; + var visited = new HashSet<(int Page, int Row)>(); + var chainPages = new List(); + + while (written < length) + { + if (page == 0) + throw new InvalidDataException( + $"Long-value chain ended after {written} of {length} declared bytes."); + if (!visited.Add((page, row))) + throw new InvalidDataException($"Long-value chain contains a cycle at row {page}:{row}."); + chainPages.Add(page); + + byte[] chunk = ReadLvalRow(page, row); + if (chunk.Length < PageBuffer.RecordPointerSize) + throw new InvalidDataException( + $"Long-value chain row {page}:{row} has {chunk.Length} bytes; at least " + + $"{PageBuffer.RecordPointerSize} are required for its next pointer."); + + // Each chained chunk starts with a record pointer to the next. + (int nextRow, int nextPage) = PageBuffer.ReadRecordPointer(chunk, 0); + + int copy = chunk.Length - PageBuffer.RecordPointerSize; + if (copy == 0) + throw new InvalidDataException($"Long-value chain row {page}:{row} makes no payload progress."); + if (copy > length - written) + throw new InvalidDataException( + $"Long-value chain row {page}:{row} exceeds the declared length by {copy - (length - written)} bytes."); + Array.Copy(chunk, PageBuffer.RecordPointerSize, result, written, copy); + written += copy; + + if (written == length && nextPage != 0) + throw new InvalidDataException( + $"Long-value chain continues to page {nextPage} after its declared {length} bytes."); + page = nextPage; + row = nextRow; + } + + pages = chainPages; + return result; + } + + private byte[] ReadLvalRow(int page, int row) + { + if (page <= 0 || page >= _channel.PageCount) + throw new InvalidDataException( + $"Long-value page pointer {page} is outside the file's 1..{_channel.PageCount - 1} range."); + + var lval = new DataPage(); + lval.Read(_channel.ReadPage(page), _channel.Format); + if (!lval.IsLongValuePage) + throw new InvalidDataException($"Long-value pointer {page}:{row} targets a non-LVAL data page."); + if (row < 0 || row >= lval.RowCount) + throw new InvalidDataException( + $"Long-value row pointer {page}:{row} is outside the page's 0..{lval.RowCount - 1} range."); + DataPage.RowSlot slot = lval.Rows[row]; + if (slot.IsDeleted || slot.HasOverflow) + throw new InvalidDataException( + $"Long-value pointer {page}:{row} targets a deleted or overflow row slot."); + return lval.GetRow(row).ToArray(); + } + + /// Writes across one or more LVAL pages, returning its descriptor + /// and the pages used (all owned; the last is also free, having spare room). + internal (byte[] Descriptor, IReadOnlyList OwnedPages, int FreePage) Write(byte[] payload) + { + JetFormatBase format = _channel.Format; + ValidateLength(payload.Length, format); + if (payload.Length <= format.LongValueMaxSinglePage) + { + int page = _allocator.Allocate(); + WriteChunkPage(page, payload); // a single-page row is the payload itself (no next pointer) + return ( + Descriptor(format, payload.Length, StorageKind.SinglePage, page), [page], page); + } + + // Chained: split into chunks that each fit a row after the record pointer to the next. + int maxChunkData = format.LongValueMaxRowSize - PageBuffer.RecordPointerSize; + int chunkCount = (payload.Length + maxChunkData - 1) / maxChunkData; + var pages = new int[chunkCount]; + for (int i = 0; i < chunkCount; i++) pages[i] = _allocator.Allocate(); + + // The chain stamp goes in two places and must match in both: here on the first chunk page and in the + // descriptor below. It is a version tag on the CHAIN, minted per write of it, so a reader arriving + // through a descriptor can tell that the pages it is about to follow are the ones that descriptor was + // written against and not a later value's. Only the entry page carries it — every chunk after that is + // reached from a page already validated. ACE uses GetTickCount(); the value is arbitrary and only the + // agreement is checked, so matching its choice keeps our pages the shape Access produces. + uint stamp = (uint)Environment.TickCount; + + for (int i = 0; i < chunkCount; i++) + { + int start = i * maxChunkData; + int len = Math.Min(maxChunkData, payload.Length - start); + int nextPage = i + 1 < chunkCount ? pages[i + 1] : 0; + + var row = new byte[PageBuffer.RecordPointerSize + len]; + PageBuffer.WriteRecordPointer(row, 0, row: 0, nextPage); // always row 0 — one chunk per page + payload.AsSpan(start, len).CopyTo(row.AsSpan(PageBuffer.RecordPointerSize)); + WriteChunkPage(pages[i], row, i == 0 ? stamp : 0); + } + + return ( + Descriptor(format, payload.Length, StorageKind.Chained, pages[0], stamp: stamp), + pages, FreePage: 0); + } + + /// Allocates a fresh LVAL page, writes as its row 0, and returns the + /// page number — the caller records it in the column's usage maps. is as + /// for . + internal int WriteNewPage(byte[] row, byte[]? uncompressed = null) + { + int page = _allocator.Allocate(); + WriteChunkPage(page, row, uncompressed: uncompressed); + return page; + } + + /// Appends to an existing LVAL page if it has room, returning the new + /// row index and the page's remaining free space (null if it does not fit). Lets several small long + /// values share one page, the way Access packs them. + /// + /// is the value before compression, when is its + /// compressed form. ACE places such a value as though it were uncompressed — the page must have room for the + /// uncompressed bytes — writes those bytes where they would go, then the compressed row over their upper end, + /// so the rest of the uncompressed image stays behind in the page's free space. Both are measured + /// (long-values.md). + /// + internal (int Row, int RemainingFree)? TryAppend(int pageNumber, byte[] row, byte[]? uncompressed = null) + { + JetFormatBase format = _channel.Format; + if (pageNumber <= 0 || pageNumber >= _channel.PageCount) + throw new InvalidDataException($"LVAL append page {pageNumber} is outside the physical file."); + if (row.Length > format.LongValueMaxRowSize) + throw new ArgumentOutOfRangeException(nameof(row), + $"An LVAL row cannot exceed {format.LongValueMaxRowSize} bytes."); + + PageBuffer buffer = _channel.ReadPage(pageNumber); + var parsed = new DataPage(); + parsed.Read(buffer, format); + if (!parsed.IsLongValuePage) + throw new InvalidDataException($"LVAL append target {pageNumber} is not owned by the LVAL store."); + + int rowCount = parsed.RowCount; + // A long-value descriptor addresses its row with a ONE-BYTE field (the row byte of its record pointer), + // exactly as an index entry addresses a data row — so the same 256-slot ceiling applies, and passing it + // would alias one value's descriptor onto another's row with no error. Today it is unreachable: a + // payload of 64 bytes or less inlines and never arrives here, and a page leaves the free-pages map once + // it has no room left for a MinLvalValue-byte value and its slot (RowInserter), which caps a page at 104 rows + // even for the smallest thing + // that can reach it (a 33-character memo compressed to 35 bytes — compression is applied AFTER the + // inline test, so the floor is lower than the 65-byte inline limit suggests). That margin is emergent, + // not stated: it moves if the inline limit or the free-map threshold changes. Refusing the page here + // costs nothing and makes the ceiling structural — the caller allocates a fresh page, as it does when + // the page is out of room. + if (rowCount >= format.MaxRowsPerPage) return null; + + int slotSize = format.DataRowDirectoryEntrySize; + byte[] page = buffer.Span.ToArray(); + int lowest = DataPage.LowestRowOffset(page, format); + int physicalFree = lowest - DataPage.DirectoryEnd(format, rowCount); + if (parsed.FreeSpace != physicalFree) + throw new InvalidDataException( + $"LVAL page {pageNumber} declares {parsed.FreeSpace} free bytes but its row geometry has {physicalFree}."); + // Row data + its directory entry; a compressed value needs room for its uncompressed bytes. + if (physicalFree < Math.Max(row.Length, uncompressed?.Length ?? 0) + slotSize) return null; + + uncompressed?.CopyTo(page.AsSpan(lowest - uncompressed.Length)); + DataPage.TryAppendRow(page, format, row, RowSlotFlags.None, out int newRow); // room proven above + _channel.WritePage(pageNumber, page); + return (newRow, DataPage.ReadFreeSpace(page, format)); + } + + /// Writes one row () to a fresh LVAL data page, packed from the page end. + /// is the chain stamp for the first page of a chain, and zero everywhere else — + /// which is what ACE writes on a single-page value and on every chunk after the first. + /// is as for . + private void WriteChunkPage(int pageNumber, byte[] row, uint stamp = 0, byte[]? uncompressed = null) + { + JetFormatBase format = _channel.Format; + byte[] page = DataPage.NewPage(format, JetFormatBase.LongValuePageMarker); + BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.DataChainStampOffset, 4), stamp); + + uncompressed?.CopyTo(page.AsSpan(format.PageSize - uncompressed.Length)); + DataPage.LayRows(page, format, [row], [RowSlotFlags.None]); // one row on an empty page always fits + _channel.WritePage(pageNumber, page); + } + + + internal static void ValidateLength(int length, JetFormatBase format) + { + if ((uint)length > (uint)format.LongValueLengthMask) + throw new ArgumentOutOfRangeException(nameof(length), "The long-value length would overwrite its storage flags."); + } + + /// A descriptor: the length with its bits, the record pointer to the value's + /// row or first chunk, and the chain stamp. The pointer is zero on an inline value, and the stamp is non-zero + /// only on a chained one, where it repeats the first chain page's own. + internal static byte[] Descriptor(JetFormatBase format, int length, StorageKind storage, + int page = 0, int row = 0, uint stamp = 0) + { + ValidateLength(length, format); + var d = new byte[format.LongValueDescriptorSize]; + BinaryPrimitives.WriteUInt32LittleEndian(d, (uint)length | (uint)storage); + PageBuffer.WriteRecordPointer(d, format.LongValueDescriptorPointerOffset, row, page); + BinaryPrimitives.WriteUInt32LittleEndian(d.AsSpan(format.LongValueDescriptorChainStampOffset, sizeof(uint)), stamp); + return d; + } + + /// A descriptor's length, storage, record pointer and chain stamp, after proving it has all its bytes + /// and a storage form that exists. Byte 3 carries the length's top byte AND the storage bits — the length runs + /// to , and a 16 MB value puts 0x01 there — so the two are + /// masked apart. + internal static (int Length, StorageKind Storage, int Row, int Page, uint Stamp) Read( + ReadOnlySpan descriptor, JetFormatBase format) + { + if (descriptor.Length < format.LongValueDescriptorSize) + throw new InvalidDataException( + $"Long-value descriptor has {descriptor.Length} bytes; expected at least {format.LongValueDescriptorSize}."); + + uint head = BinaryPrimitives.ReadUInt32LittleEndian(descriptor); + var storage = (StorageKind)(head & ~(uint)format.LongValueLengthMask); + if (storage is not (StorageKind.Inline or StorageKind.SinglePage or StorageKind.Chained)) + throw new InvalidDataException($"Long-value descriptor has unsupported flags 0x{(uint)storage >> 24:X2}."); + (int row, int page) = PageBuffer.ReadRecordPointer(descriptor, format.LongValueDescriptorPointerOffset); + uint stamp = BinaryPrimitives.ReadUInt32LittleEndian( + descriptor.Slice(format.LongValueDescriptorChainStampOffset, sizeof(uint))); + return ((int)(head & (uint)format.LongValueLengthMask), storage, row, page, stamp); + } + + /// How a long value is stored: the bits of its descriptor's first 4 bytes above + /// . + internal enum StorageKind : uint + { + /// Across several LVAL pages, one chunk per page, each beginning with a record pointer to the + /// next. + Chained = 0x00000000, + + /// A single LVAL page row: the row is the whole payload. + SinglePage = 0x40000000, + + /// In the row, after the descriptor: no LVAL page. + Inline = 0x80000000, + } + +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs b/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs deleted file mode 100644 index 8ae0903c..00000000 --- a/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs +++ /dev/null @@ -1,206 +0,0 @@ -using LibRed.Formats; -using LibRed.IO; -using LibRed.Pages; -using System.Buffers.Binary; - -namespace LibRed.Storage; - -/// -/// A pre-built long-value (memo/OLE) in-row descriptor, written verbatim by the row encoder instead of -/// inlining. Produced by when a value is stored on an LVAL page. -/// -public sealed record LongValueDescriptor(byte[] Bytes); - -/// -/// The result of writing a long value to LVAL page(s): the 12-byte in-row descriptor, the pages it now -/// occupies (to record in the column's owned-pages map), and the one page that still has spare room, to be -/// recorded in the free-pages map — or 0 when no page may be shared. -/// -/// -/// A chained value reports 0. Its last chunk usually leaves room, but a chained value owns its pages -/// outright: freeing the value frees every page in the chain, so a small value packed onto the tail would be -/// destroyed under a live descriptor when the chain's owner is updated or deleted. The spare room in a chain -/// tail is the price of the chain. -/// -public sealed record LongValueResult(byte[] Descriptor, IReadOnlyList OwnedPages, int FreePage); - -/// -/// Writes a long value (memo/OLE) to LVAL page(s) and returns its 12-byte in-row reference descriptor. -/// A value up to one page is a single page (flag 0x40): the row is the payload. A -/// larger value is chained (flag 0x00) across several pages: each page holds one chunk row -/// that begins with a 4-byte [row:1][page:3] pointer to the next chunk (zero on the last). -/// -/// -/// An LVAL page is a data page (type 0x01) whose owner field is the ASCII marker "LVAL"; a chunk is -/// stored as its row 0. A value up to 64 bytes never comes here: it is inline in its row. -/// -public sealed class LongValueWriter(PageChannel channel) -{ - - // Access caps an LVAL page row at MAX_LONG_VALUE_ROW_SIZE (Jackcess) — 4076 on Jet4 (Jet3 = 2032), - // 4 bytes short of the page's usable space. A single-page value up to this fits in one row; a chained - // chunk row is this size (a 4-byte next-pointer + up to 4072 data bytes). Verified against ACE's own - // chained OLE values (Northwind Employee photos: 4076, 4076, 2606-byte chunk rows). - private const int MaxLvalRowSize = 4076; - private const int MaxChunkData = MaxLvalRowSize - 4; - - private readonly PageChannel _channel = channel; - private readonly PageAllocator _allocator = new(channel); - - /// Writes across one or more LVAL pages, returning its descriptor - /// and the pages used (all owned; the last is also free, having spare room). - public LongValueResult Write(byte[] payload) - { - LongValueFormat.ValidateLength(payload.Length); - if (payload.Length <= LongValueFormat.MaxSinglePageValue) - { - int page = _allocator.Allocate(); - WriteChunkPage(page, payload); // a single-page row is the payload itself (no next pointer) - return new LongValueResult(Descriptor(payload.Length, LongValueFormat.FlagSinglePage, page), [page], page); - } - - // Chained: split into chunks that each fit on a page after a 4-byte next-pointer. - int chunkCount = (payload.Length + MaxChunkData - 1) / MaxChunkData; - var pages = new int[chunkCount]; - for (int i = 0; i < chunkCount; i++) pages[i] = _allocator.Allocate(); - - // The chain stamp goes in two places and must match in both: here on the first chunk page and in the - // descriptor below. It is a version tag on the CHAIN, minted per write of it, so a reader arriving - // through a descriptor can tell that the pages it is about to follow are the ones that descriptor was - // written against and not a later value's. Only the entry page carries it — every chunk after that is - // reached from a page already validated. ACE uses GetTickCount(); the value is arbitrary and only the - // agreement is checked, so matching its choice keeps our pages the shape Access produces. - uint stamp = (uint)Environment.TickCount; - - for (int i = 0; i < chunkCount; i++) - { - int start = i * MaxChunkData; - int len = Math.Min(MaxChunkData, payload.Length - start); - int nextPage = i + 1 < chunkCount ? pages[i + 1] : 0; - - var row = new byte[4 + len]; - row[0] = 0; // next row (always row 0 — one chunk per page) - row[1] = (byte)nextPage; - row[2] = (byte)(nextPage >> 8); - row[3] = (byte)(nextPage >> 16); - payload.AsSpan(start, len).CopyTo(row.AsSpan(4)); - WriteChunkPage(pages[i], row, i == 0 ? stamp : 0); - } - - return new LongValueResult( - Descriptor(payload.Length, LongValueFormat.FlagChained, pages[0], stamp: stamp), pages, FreePage: 0); - } - - /// Allocates a fresh LVAL page, writes as its row 0, and returns the - /// page number — the caller records it in the column's usage maps. is as - /// for . - public int WriteNewPage(byte[] row, byte[]? uncompressed = null) - { - int page = _allocator.Allocate(); - WriteChunkPage(page, row, uncompressed: uncompressed); - return page; - } - - /// Appends to an existing LVAL page if it has room, returning the new - /// row index and the page's remaining free space (null if it does not fit). Lets several small long - /// values share one page, the way Access packs them. - /// - /// is the value before compression, when is its - /// compressed form. ACE places such a value as though it were uncompressed — the page must have room for the - /// uncompressed bytes — writes those bytes where they would go, then the compressed row over their upper end, - /// so the rest of the uncompressed image stays behind in the page's free space. Both are measured - /// (long-values.md). - /// - public (int Row, int RemainingFree)? TryAppend(int pageNumber, byte[] row, byte[]? uncompressed = null) - { - JetFormatBase format = _channel.Format; - if (pageNumber <= 0 || pageNumber >= _channel.PageCount) - throw new InvalidDataException($"LVAL append page {pageNumber} is outside the physical file."); - if (row.Length > MaxLvalRowSize) - throw new ArgumentOutOfRangeException(nameof(row), - $"An LVAL row cannot exceed {MaxLvalRowSize} bytes."); - - PageBuffer buffer = _channel.ReadPage(pageNumber); - var parsed = new DataPage(); - parsed.Read(buffer, format); - if (!parsed.IsLongValuePage) - throw new InvalidDataException($"LVAL append target {pageNumber} is not owned by the LVAL store."); - - int rowCount = parsed.RowCount; - // A long-value descriptor addresses its row with a ONE-BYTE field (Descriptor writes `d[4] = (byte)row`), - // exactly as an index entry addresses a data row — so the same 256-slot ceiling applies, and passing it - // would alias one value's descriptor onto another's row with no error. Today it is unreachable: a - // payload of 64 bytes or less inlines and never arrives here, and a page leaves the free-pages map once - // it has under MinLvalRow bytes left, which caps a page at 104 rows even for the smallest thing - // that can reach it (a 33-character memo compressed to 35 bytes — compression is applied AFTER the - // inline test, so the floor is lower than the 65-byte inline limit suggests). That margin is emergent, - // not stated: it moves if the inline limit or the free-map threshold changes. Refusing the page here - // costs nothing and makes the ceiling structural — the caller allocates a fresh page, as it does when - // the page is out of room. - if (rowCount >= RowPointer.MaxRowsPerPage) return null; - - int lowest = parsed.Rows.Count == 0 ? format.PageSize : parsed.Rows.Min(r => r.Offset); - int directoryEnd = format.DataRowDirectoryOffset + rowCount * 2; - int physicalFree = lowest - directoryEnd; - if (parsed.FreeSpace != physicalFree) - throw new InvalidDataException( - $"LVAL page {pageNumber} declares {parsed.FreeSpace} free bytes but its row geometry has {physicalFree}."); - // Row data + its 2-byte directory entry; a compressed value needs room for its uncompressed bytes. - if (physicalFree < Math.Max(row.Length, uncompressed?.Length ?? 0) + 2) return null; - - byte[] page = buffer.Span.ToArray(); - - uncompressed?.CopyTo(page.AsSpan(lowest - uncompressed.Length)); - int offset = lowest - row.Length; - row.CopyTo(page.AsSpan(offset)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + rowCount * 2, 2), (ushort)offset); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2), (ushort)(rowCount + 1)); - int remaining = physicalFree - row.Length - 2; - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), (ushort)remaining); - _channel.WritePage(pageNumber, page); - return (rowCount, remaining); - } - - /// The single-page (0x40) descriptor for a value stored at (, - /// ) — used when a value is packed onto an existing page at a non-zero row. - public static byte[] SinglePageDescriptor(int length, int page, int row) => - Descriptor(length, LongValueFormat.FlagSinglePage, page, row); - - /// Writes one row () to a fresh LVAL data page, packed from the page end. - /// is the chain stamp for the first page of a chain, and zero everywhere else — - /// which is what ACE writes on a single-page value and on every chunk after the first. - /// is as for . - private void WriteChunkPage(int pageNumber, byte[] row, uint stamp = 0, byte[]? uncompressed = null) - { - JetFormatBase format = _channel.Format; - var page = new byte[format.PageSize]; - PageHeader.WriteType(page, PageType.DataPage); - BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.DataOwnerOffset, 4), LongValueFormat.LvalMarker); - BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.DataChainStampOffset, 4), stamp); - - uncompressed?.CopyTo(page.AsSpan(format.PageSize - uncompressed.Length)); - int offset = format.PageSize - row.Length; - row.CopyTo(page.AsSpan(offset)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset, 2), (ushort)offset); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2), 1); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), - (ushort)(offset - format.DataRowDirectoryOffset - 2)); - _channel.WritePage(pageNumber, page); - } - - /// Builds the 12-byte descriptor: 4-byte length with storage flags, row/page, chain stamp. The - /// stamp is non-zero only on the chained form, where it repeats the first chain page's own — ACE leaves it - /// zero on the inline and single-page forms, and checks it on neither. - private static byte[] Descriptor(int length, byte flag, int firstPage, int row = 0, uint stamp = 0) - { - LongValueFormat.ValidateLength(length); - var d = new byte[12]; - BinaryPrimitives.WriteUInt32LittleEndian(d, (uint)length | ((uint)flag << 24)); - d[4] = (byte)row; - d[5] = (byte)firstPage; - d[6] = (byte)(firstPage >> 8); - d[7] = (byte)(firstPage >> 16); - BinaryPrimitives.WriteUInt32LittleEndian(d.AsSpan(LongValueFormat.ChainStampOffset, 4), stamp); - return d; - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/PageAllocator.cs b/src/LibRed/LibRed.Core/Storage/PageAllocator.cs index fafaf58e..5b8ce5b3 100644 --- a/src/LibRed/LibRed.Core/Storage/PageAllocator.cs +++ b/src/LibRed/LibRed.Core/Storage/PageAllocator.cs @@ -1,7 +1,6 @@ +using LibRed.Formats; using LibRed.IO; using LibRed.Pages; -using System.Buffers.Binary; -using System.Numerics; namespace LibRed.Storage; @@ -14,58 +13,58 @@ namespace LibRed.Storage; /// /// Pages set in the **global released-pages map** (named at 0x1C) are never allocated, as ACE never /// allocates them: they were released by a session that has not yet merged them back into the free map. Both -/// maps are located only through their page-0 pointers, row included — page 1 rows 0 and 1 in every file ACE -/// writes, but ACE follows the pointers wherever they lead (docs/format/page-05-usage-maps.md §9.1). +/// maps are located only through their page-0 pointers, row included — page 1 rows 0 and 1 where ACE creates +/// them, but ACE follows the pointers wherever they lead (docs/format/page-05-usage-maps.md §9.1). /// Freed pages take one of two routes, as ACE's do: makes a page reusable at once, and /// holds it until runs when the handle closes. /// -public sealed class PageAllocator(PageChannel channel) +internal sealed class PageAllocator { - private const byte InlineMapType = 0x00; - private const byte ReferenceMapType = 0x01; - /// Bytes preceding the bitmap on a dedicated usage-bitmap page (type 0x0105). - private const int BitmapPageHeaderSize = 4; + private readonly PageChannel _channel; - /// A reference map is a fixed 69-byte record: the type byte + 17 bitmap-page pointers (17 being - /// exactly enough to span Jet's 2 GB ceiling). See the usage-maps spec (§9). - private const int ReferenceMapSlots = 17; + internal PageAllocator(PageChannel channel) => _channel = channel; - private readonly PageChannel _channel = channel; + /// + /// Allocates a fresh page by growing the file by one page, returning its number. Jet also + /// recycles freed pages via usage maps; appending at the end is always valid since the page + /// count is simply the file length divided by the page size. + /// + internal int Append() + { + if (_channel.IsReadOnly) + throw new InvalidOperationException("This channel was opened read-only."); + + int pageNumber = _channel.PageCount; + _channel.WritePage(pageNumber, new byte[_channel.PageSize]); + return pageNumber; + } /// One of the two global map records, as read through its page-0 pointer. - private sealed record MapRecord(string Name, int PageNumber, int Row, byte[] Page, RowSlot Slot) + private sealed record MapRecord(string Name, int PageNumber, int Row, byte[] Page, DataPage.RowSlot Slot) { public ReadOnlySpan Record => Page.AsSpan(Slot.Offset, Slot.Length); - public byte Type => Page[Slot.Offset]; + public UsageMapType Type => UsageMap.RecordType(Record); } public int Allocate() { (MapRecord free, MapRecord released) = ReadGlobalMaps(); var releasedPages = new ReleasedPages(this, released); - if (free.Type == ReferenceMapType) + if (free.Type == UsageMapType.Reference) return AllocateFromReferenceMap(free, released, releasedPages); - byte[] page = free.Page; - int mapOffset = free.Slot.Offset; - int startPage = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(mapOffset + 1, 4)); - int bitmapStart = mapOffset + 5; - int bitmapEnd = mapOffset + free.Slot.Length; - for (int i = bitmapStart; i < bitmapEnd; i++) + JetFormatBase format = _channel.Format; + int startPage = UsageMap.StartPage(free.Record, format); + Span bitmap = UsageMap.InlineBits(free.Page.AsSpan(free.Slot.Offset, free.Slot.Length), format); + for (int bit = BitmapBits.NextSetBit(bitmap, 0); bit >= 0; bit = BitmapBits.NextSetBit(bitmap, bit + 1)) { - int bits = page[i]; - while (bits != 0) - { - int bit = BitOperations.TrailingZeroCount(bits); - bits &= bits - 1; - int allocated = startPage + (i - bitmapStart) * 8 + bit; - if (releasedPages.Contains(allocated)) continue; // released, not yet reusable - ValidateReusablePage(allocated, "inline free bit", free, released, AppendBoundary(releasedPages)); - EnsurePhysicalAllocation(allocated, releasedPages); - page[i] &= (byte)~(1 << bit); // no longer free - _channel.WritePage(free.PageNumber, page); - return allocated; - } + int allocated = startPage + bit; + if (releasedPages.Contains(allocated)) continue; // released, not yet reusable + ValidateReusablePage(allocated, "inline free bit", free, released, AppendBoundary(releasedPages)); + EnsurePhysicalAllocation(allocated, releasedPages); + BitmapBits.Set(bitmap, bit, false); // no longer free + _channel.WritePage(free.PageNumber, free.Page); + return allocated; } // An unrepresented page is not safely recorded as used. Grow the global map before appending. @@ -80,29 +79,28 @@ public void Free(int page) (MapRecord free, MapRecord released) = ReadGlobalMaps(); ValidateReusablePage(page, "page being freed", free, released, _channel.PageCount - 1); - if (free.Type == ReferenceMapType) + if (free.Type == UsageMapType.Reference) { FreeInReferenceMap(free, released, page); return; } - byte[] p = free.Page; - int mapOffset = free.Slot.Offset; - int startPage = BinaryPrimitives.ReadInt32LittleEndian(p.AsSpan(mapOffset + 1, 4)); + JetFormatBase format = _channel.Format; + int startPage = UsageMap.StartPage(free.Record, format); + Span bitmap = UsageMap.InlineBits(free.Page.AsSpan(free.Slot.Offset, free.Slot.Length), format); int bit = page - startPage; - int byteIndex = mapOffset + 5 + bit / 8; // A page the map has no bit for cannot be recorded as free, and dropping it here is how a page is lost // for good — nothing else remembers it. It does not arise in a well-formed file: allocation extends the // map to the file's frontier, and ACE's own map covers its whole file (measured: a 1,761-page file // carries a 229-byte record from page 0, and records pages freed above the original 512-page window). // So this is a malformed or foreign map, and it says so rather than quietly leaking the page. - if (bit < 0 || byteIndex >= mapOffset + free.Slot.Length) + if (bit < 0 || bit >= bitmap.Length * 8) throw new InvalidDataException( $"Cannot record page {page} as free: the global free-pages map covers pages {startPage} through " - + $"{startPage + (free.Slot.Length - 5) * 8 - 1}, so the page has no bit in it."); + + $"{startPage + bitmap.Length * 8 - 1}, so the page has no bit in it."); - p[byteIndex] |= (byte)(1 << (bit % 8)); - _channel.WritePage(free.PageNumber, p); + BitmapBits.Set(bitmap, bit, true); + _channel.WritePage(free.PageNumber, free.Page); } /// @@ -130,7 +128,9 @@ public void ReturnReleasedPages() SortedSet pages = [.. _channel.PagesReleasedAtClose]; if (pages.Count == 0 && !_channel.HasPublishedWrites) return; (_, MapRecord released) = ReadGlobalMaps(); - pages.UnionWith(ReleasedMapPages(released)); + // Every bit kept, none bounded by the file: a released page past its end was never materialized, and is + // skipped below. + pages.UnionWith(UsageMap.PagesInRecord(_channel, released.Record, rejectBeyond: null, "The global released-pages map")); if (pages.Count == 0) return; bool ownTransaction = !_channel.InTransaction; @@ -158,49 +158,23 @@ public void ReturnReleasedPages() } } - /// The pages set in the global released-pages map, inline or reference form. - private List ReleasedMapPages(MapRecord released) - { - ReadOnlySpan record = released.Record; - var pages = new List(); - if (released.Type == InlineMapType) - { - int start = BinaryPrimitives.ReadInt32LittleEndian(record.Slice(1, 4)); - for (int i = 5; i < record.Length; i++) - for (int bits = record[i]; bits != 0; bits &= bits - 1) - pages.Add(start + (i - 5) * 8 + BitOperations.TrailingZeroCount(bits)); - return pages; - } - - int pagesPerBitmap = (_channel.PageSize - BitmapPageHeaderSize) * 8; - for (int slot = 0; slot < ReferenceMapSlots; slot++) - { - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(record.Slice(1 + slot * 4, 4)); - if (bitmapPage == 0) continue; - ReadOnlySpan bitmap = _channel.ReadPage(bitmapPage).Span; - for (int i = BitmapPageHeaderSize; i < bitmap.Length; i++) - for (int bits = bitmap[i]; bits != 0; bits &= bits - 1) - pages.Add(slot * pagesPerBitmap + (i - BitmapPageHeaderSize) * 8 + BitOperations.TrailingZeroCount(bits)); - } - return pages; - } - /// /// Sizes the released-pages map for the way ACE's close does, before they are merged. /// An inline record that already covers them is left alone. Otherwise it is lengthened, keeping its start - /// page, to the shortest that covers the highest page — the 5-byte header, then the bitmap in whole 4-byte - /// words — as long as its holder keeps 4 bytes free. When that is too long, the window moves instead: the - /// start becomes the lowest page released rounded down to a byte, and the record is sized from there. When - /// even that is too long, the record is grown at its old start to cover the highest released page it can - /// reach, the released pages it covers are marked in it, and it is converted to reference form: a bitmap page is allocated for each range - /// holding a released page, in range order, and a 69-byte reference record takes its place, the longer - /// record's bytes staying on the page below it. A map already in reference form gains a bitmap page for each - /// range holding a released page that it has none for. + /// page, to the shortest that covers the highest page — the header, then the bitmap in whole growth steps — + /// as long as its holder keeps bytes free. When that is too + /// long, the window moves instead: the start becomes the lowest page released rounded down to a byte, and the + /// record is sized from there. When even that is too long, the record is grown at its old start to cover the + /// highest released page it can reach, the released pages it covers are marked in it, and it is converted to + /// reference form: a bitmap page is allocated for each range holding a released page, in range order, and a + /// reference record takes its place, the longer record's bytes staying on the page below it. A map already in + /// reference form gains a bitmap page for each range holding a released page that it has none for. /// private void SizeReleasedMap(SortedSet pages) { + JetFormatBase format = _channel.Format; (_, MapRecord released) = ReadGlobalMaps(); - if (released.Type == ReferenceMapType) + if (released.Type == UsageMapType.Reference) { byte[] existing = released.Record.ToArray(); if (!AddReleasedBitmapPages(existing, pages)) return; @@ -209,50 +183,47 @@ private void SizeReleasedMap(SortedSet pages) return; } - int start = BinaryPrimitives.ReadInt32LittleEndian(released.Record.Slice(1, 4)); + int headerSize = format.UsageMapInlineHeaderSize; + int start = UsageMap.StartPage(released.Record, format); int lowest = pages.Min, highest = pages.Max; - int Covering(int from) => 5 + ((highest - from) / 8 + 1 + 3) / 4 * 4; - if (lowest >= start && highest < start + (released.Slot.Length - 5) * 8) return; + int Covering(int from) => headerSize + UsageMap.InlineBitmapBytes(format, highest - from + 1); + if (lowest >= start && highest < start + (released.Slot.Length - headerSize) * 8) return; var holder = new DataPage(); - holder.Read(new PageBuffer(released.Page, released.PageNumber), _channel.Format); + holder.Read(new PageBuffer(released.Page, released.PageNumber), format); int others = 0; for (int row = 0; row < holder.RowCount; row++) if (row != released.Row) others += holder.Rows[row].Length; - int room = _channel.PageSize - (_channel.Format.DataRowDirectoryOffset + holder.RowCount * 2) - others - 4; - int longest = Math.Max(released.Slot.Length, 5 + (room - 5) / 4 * 4); + int room = format.PageSize - DataPage.DirectoryEnd(format, holder.RowCount) - others - format.UsageMapHolderReserve; + int growth = format.UsageMapInlineGrowthSize; + int longest = Math.Max(released.Slot.Length, headerSize + (room - headerSize) / growth * growth); if (lowest >= start && Covering(start) <= longest) { - var grown = new byte[Covering(start)]; - released.Record[..5].CopyTo(grown); - LayMapRecord(released, grown); + LayMapRecord(released, UsageMap.NewInlineRecord(format, start, Covering(start) - headerSize)); return; } int moved = lowest / 8 * 8; if (Covering(moved) <= longest) { - var window = new byte[Math.Max(Covering(moved), released.Slot.Length)]; - BinaryPrimitives.WriteInt32LittleEndian(window.AsSpan(1, 4), moved); - LayMapRecord(released, window); + LayMapRecord(released, + UsageMap.NewInlineRecord(format, moved, Math.Max(Covering(moved), released.Slot.Length) - headerSize)); return; } // Grown only as far as the highest released page it can still cover — the whole of its longest length only // when released pages reach that far. - int reach = start + (longest - 5) * 8 - 1; + int reach = start + (longest - headerSize) * 8 - 1; SortedSet reachable = pages.GetViewBetween(Math.Min(start, reach), reach); int covered = reachable.Count == 0 ? released.Slot.Length - : Math.Max(released.Slot.Length, 5 + ((reachable.Max - start) / 8 + 1 + 3) / 4 * 4); - var record = new byte[covered]; - released.Record[..5].CopyTo(record); - foreach (int page in pages.GetViewBetween(start, start + (record.Length - 5) * 8 - 1)) - record[5 + (page - start) / 8] |= (byte)(1 << ((page - start) % 8)); + : Math.Max(released.Slot.Length, headerSize + UsageMap.InlineBitmapBytes(format, reachable.Max - start + 1)); + byte[] record = UsageMap.NewInlineRecord(format, start, covered - headerSize); + foreach (int page in pages.GetViewBetween(start, start + (covered - headerSize) * 8 - 1)) + BitmapBits.Set(UsageMap.InlineBits(record, format), page - start, true); LayMapRecord(released, record); - var reference = new byte[1 + ReferenceMapSlots * 4]; - reference[0] = ReferenceMapType; + byte[] reference = UsageMap.NewReferenceRecord(format); AddReleasedBitmapPages(reference, pages); (_, released) = ReadGlobalMaps(); LayMapRecord(released, reference); @@ -263,60 +234,45 @@ private void SizeReleasedMap(SortedSet pages) /// was added. private bool AddReleasedBitmapPages(byte[] reference, SortedSet pages) { - int pagesPerBitmap = (_channel.PageSize - BitmapPageHeaderSize) * 8; + JetFormatBase format = _channel.Format; + int pagesPerBitmap = format.UsageMapPagesPerBitmapPage; bool added = false; foreach (int slot in pages.Select(p => p / pagesPerBitmap).Distinct()) { - if (slot >= ReferenceMapSlots) + if (slot >= format.UsageMapReferenceSlots) throw new InvalidDataException($"Released page {pages.Max} lies past the global map's bitmap slots."); - if (BinaryPrimitives.ReadInt32LittleEndian(reference.AsSpan(1 + slot * 4)) != 0) continue; + if (UsageMap.ReferencePointer(reference, slot, format) != 0) continue; int bitmapPage = Allocate(); - var bitmap = new byte[_channel.PageSize]; - PageHeader.WriteType(bitmap, PageType.PageUsageBitmap); - _channel.WritePage(bitmapPage, bitmap); - BinaryPrimitives.WriteInt32LittleEndian(reference.AsSpan(1 + slot * 4), bitmapPage); + _channel.WritePage(bitmapPage, UsageMap.NewBitmapPage(format)); + UsageMap.WriteReferencePointer(reference, slot, format, bitmapPage); added = true; } return added; } - /// Replaces a global map record, repacking its holder's records from the page end, and lays the result - /// over the page as it stands: bytes a moved record vacates are not cleared, as ACE leaves them. + /// Replaces a global map record, repacking its holder's records from the page end. private void LayMapRecord(MapRecord map, byte[] record) { + JetFormatBase format = _channel.Format; byte[] page = _channel.ReadPage(map.PageNumber).Span.ToArray(); var holder = new DataPage(); - holder.Read(new PageBuffer(page, map.PageNumber), _channel.Format); - byte[] repacked = UsageMapWriter.ReplaceMapRecord(page, holder, _channel.Format, map.Row, record, out _) + holder.Read(new PageBuffer(page, map.PageNumber), format); + byte[] repacked = UsageMap.ReplaceMapRecord(page, holder, format, map.Row, record, out _) ?? throw new InvalidDataException($"Global {map.Name} map cannot fit its holder page."); - - var laid = new DataPage(); - laid.Read(new PageBuffer(repacked, map.PageNumber), _channel.Format); - repacked.AsSpan(0, _channel.Format.DataRowDirectoryOffset + laid.RowCount * 2).CopyTo(page); - foreach (RowSlot slot in laid.Rows) - repacked.AsSpan(slot.Offset, slot.Length).CopyTo(page.AsSpan(slot.Offset)); - _channel.WritePage(map.PageNumber, page); + _channel.WritePage(map.PageNumber, repacked); } /// Clears every bit of the global released-pages map: in place for an inline record, and on each /// bitmap page, header kept, for a reference record. private void ClearReleasedMap(MapRecord released) { - if (released.Type == ReferenceMapType) + if (released.Type == UsageMapType.Reference) { - ReadOnlySpan map = released.Record; - for (int slot = 0; slot < ReferenceMapSlots; slot++) - { - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(map.Slice(1 + slot * 4, 4)); - if (bitmapPage == 0) continue; - byte[] bitmap = _channel.ReadPage(bitmapPage).Span.ToArray(); - bitmap.AsSpan(BitmapPageHeaderSize).Clear(); - _channel.WritePage(bitmapPage, bitmap); - } + new UsageMap(_channel).ClearBitmapPages(released.Record); return; } - released.Page.AsSpan(released.Slot.Offset + 5, released.Slot.Length - 5).Clear(); + UsageMap.InlineBits(released.Page.AsSpan(released.Slot.Offset, released.Slot.Length), _channel.Format).Clear(); _channel.WritePage(released.PageNumber, released.Page); } @@ -329,48 +285,44 @@ private void ClearReleasedMap(MapRecord released) /// Allocates from a reference-type global free map (huge databases): the record is a list of /// pointers to dedicated bitmap pages (type 0x0105), pointer k covering the page range starting at - /// k × (pageSize − 4) × 8. A **set bit is a free page** (the global map's sense, opposite of a + /// k × UsageMapPagesPerBitmapPage. A **set bit is a free page** (the global map's sense, opposite of a /// per-table owned map). Finds the first free page, clears its bit on the bitmap page, and returns it; /// grows the file when no bitmap records a free page. private int AllocateFromReferenceMap(MapRecord free, MapRecord released, ReleasedPages releasedPages) { - var format = _channel.Format; + JetFormatBase format = _channel.Format; ReadOnlySpan map = free.Record; - int pagesPerBitmap = (format.PageSize - BitmapPageHeaderSize) * 8; + int pagesPerBitmap = format.UsageMapPagesPerBitmapPage; HashSet bitmapPages = ReferenceBitmapPages(free, released); - for (int slot = 0; slot < ReferenceMapSlots; slot++) + for (int slot = 0; slot < format.UsageMapReferenceSlots; slot++) { - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(map.Slice(1 + slot * 4, 4)); + int bitmapPage = UsageMap.ReferencePointer(map, slot, format); if (bitmapPage == 0) continue; // no bitmap page allocated for this range - byte[] bitmap = ValidateBitmapPage(bitmapPage, free, released); - for (int i = BitmapPageHeaderSize; i < format.PageSize; i++) + byte[] page = ValidateBitmapPage(bitmapPage, free, released); + Span bitmap = UsageMap.BitmapPageBits(page, format); + for (int bit = BitmapBits.NextSetBit(bitmap, 0); bit >= 0; bit = BitmapBits.NextSetBit(bitmap, bit + 1)) { - int bits = bitmap[i]; - while (bits != 0) - { - int bit = BitOperations.TrailingZeroCount(bits); - bits &= bits - 1; - int allocated = slot * pagesPerBitmap + (i - BitmapPageHeaderSize) * 8 + bit; - if (releasedPages.Contains(allocated)) continue; // released, not yet reusable - ValidateReusablePage(allocated, $"reference-map slot {slot} free bit", free, released, - AppendBoundary(releasedPages)); - if (bitmapPages.Contains(allocated)) - throw new InvalidDataException($"Global free map marks bitmap page {allocated} itself as free."); - EnsurePhysicalAllocation(allocated, releasedPages); - bitmap[i] &= (byte)~(1 << bit); // no longer free - _channel.WritePage(bitmapPage, bitmap); - return allocated; - } + int allocated = slot * pagesPerBitmap + bit; + if (releasedPages.Contains(allocated)) continue; // released, not yet reusable + ValidateReusablePage(allocated, $"reference-map slot {slot} free bit", free, released, + AppendBoundary(releasedPages)); + if (bitmapPages.Contains(allocated)) + throw new InvalidDataException($"Global free map marks bitmap page {allocated} itself as free."); + EnsurePhysicalAllocation(allocated, releasedPages); + BitmapBits.Set(bitmap, bit, false); // no longer free + _channel.WritePage(bitmapPage, page); + return allocated; } } return GrowAndAllocate(); } - /// Extends allocation metadata before the physical file: four-byte inline growth, four spare - /// bytes left on the holder page before promoting to reference form, and a reference bitmap allocated + /// Extends allocation metadata before the physical file: inline growth in + /// steps, + /// spare bytes left on the holder page before promoting to reference form, and a reference bitmap allocated /// before the first data page in its range. All three measured against ACE and asserted by /// GlobalMapGrowthTests. private int GrowAndAllocate() @@ -392,30 +344,32 @@ private int GrowAndAllocate() private int GrowAndAllocateCore() { + JetFormatBase format = _channel.Format; (MapRecord free, MapRecord released) = ReadGlobalMaps(); var releasedPages = new ReleasedPages(this, released); byte[] record = free.Record.ToArray(); int frontier = _channel.PageCount; - if (record[0] == InlineMapType) + if (free.Type == UsageMapType.Inline) { - int start = BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(1)); + int headerSize = format.UsageMapInlineHeaderSize; + int start = UsageMap.StartPage(record, format); if (start != 0) throw new NotSupportedException("Cannot grow a global inline map with a nonzero start page."); - int bitmapBytes = ((frontier / 8 + 1 + 3) / 4) * 4; - if (bitmapBytes <= record.Length - 5) + int bitmapBytes = UsageMap.InlineBitmapBytes(format, frontier + 1); + if (bitmapBytes <= record.Length - headerSize) return AppendUnreleasedPage(releasedPages); // already represented as used - var grown = new byte[5 + bitmapBytes]; + var grown = new byte[headerSize + bitmapBytes]; // Preserve existing free bits; newly covered physical pages are already used. Only future // pages start free. The requested frontier is cleared by the ordinary allocation path. record.CopyTo(grown, 0); for (int bit = frontier; bit < bitmapBytes * 8; bit++) - grown[5 + bit / 8] |= (byte)(1 << (bit % 8)); + BitmapBits.Set(UsageMap.InlineBits(grown, format), bit, true); var holder = new DataPage(); - holder.Read(_channel.ReadPage(free.PageNumber), _channel.Format); - byte[]? rewritten = UsageMapWriter.ReplaceMapRecord(free.Page, holder, _channel.Format, free.Row, grown, out _); + holder.Read(_channel.ReadPage(free.PageNumber), format); + byte[]? rewritten = UsageMap.ReplaceMapRecord(free.Page, holder, format, free.Row, grown, out _); if (rewritten is not null && - BinaryPrimitives.ReadUInt16LittleEndian(rewritten.AsSpan(_channel.Format.DataFreeSpaceOffset)) >= 4) + DataPage.ReadFreeSpace(rewritten, format) >= format.UsageMapHolderReserve) { _channel.WritePage(free.PageNumber, rewritten); return Allocate(); @@ -423,36 +377,33 @@ private int GrowAndAllocateCore() // Inline exhausted: every existing page is used (Allocate already searched all free bits). // Reserve the bitmap pages first so their own bits are clear in the finished map. - record = new byte[1 + ReferenceMapSlots * 4]; - record[0] = ReferenceMapType; - int span = (_channel.PageSize - BitmapPageHeaderSize) * 8; + record = UsageMap.NewReferenceRecord(format); + int span = format.UsageMapPagesPerBitmapPage; for (int range = 0; range <= _channel.PageCount / span; range++) { - if (range >= ReferenceMapSlots) + if (range >= format.UsageMapReferenceSlots) throw new NotSupportedException("Global allocation map has no remaining bitmap slots."); - int bitmap = AppendUnreleasedPage(releasedPages); - BinaryPrimitives.WriteInt32LittleEndian(record.AsSpan(1 + range * 4), bitmap); + UsageMap.WriteReferencePointer(record, range, format, AppendUnreleasedPage(releasedPages)); } - for (int range = 0; range < ReferenceMapSlots; range++) + for (int range = 0; range < format.UsageMapReferenceSlots; range++) { - int bitmap = BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(1 + range * 4)); + int bitmap = UsageMap.ReferencePointer(record, range, format); if (bitmap != 0) WriteNewGlobalBitmap(bitmap, range); } - WriteGlobalRecord(free, record); + LayMapRecord(free, record); return Allocate(); } - int pagesPerBitmap = (_channel.PageSize - BitmapPageHeaderSize) * 8; - int rangeIndex = frontier / pagesPerBitmap; - if (rangeIndex >= ReferenceMapSlots) + int rangeIndex = frontier / format.UsageMapPagesPerBitmapPage; + if (rangeIndex >= format.UsageMapReferenceSlots) throw new NotSupportedException("Global allocation map has no remaining bitmap slots."); - if (BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(1 + rangeIndex * 4)) != 0) + if (UsageMap.ReferencePointer(record, rangeIndex, format) != 0) return AppendUnreleasedPage(releasedPages); // represented range, bit already clear int newBitmap = AppendUnreleasedPage(releasedPages); - BinaryPrimitives.WriteInt32LittleEndian(record.AsSpan(1 + rangeIndex * 4), newBitmap); + UsageMap.WriteReferencePointer(record, rangeIndex, format, newBitmap); WriteNewGlobalBitmap(newBitmap, rangeIndex); - WriteGlobalRecord(free, record); + LayMapRecord(free, record); return Allocate(); } @@ -461,50 +412,40 @@ private int GrowAndAllocateCore() private int AppendUnreleasedPage(ReleasedPages releasedPages) { while (releasedPages.Contains(_channel.PageCount)) - _channel.AllocatePage(); - return _channel.AllocatePage(); + Append(); + return Append(); } private void WriteNewGlobalBitmap(int number, int range) { - var bitmap = new byte[_channel.PageSize]; - PageHeader.WriteType(bitmap, PageType.PageUsageBitmap); - int span = (_channel.PageSize - BitmapPageHeaderSize) * 8; + JetFormatBase format = _channel.Format; + byte[] bitmap = UsageMap.NewBitmapPage(format); + int span = format.UsageMapPagesPerBitmapPage; int firstFree = Math.Clamp(_channel.PageCount - range * span, 0, span); for (int bit = firstFree; bit < span; bit++) - bitmap[BitmapPageHeaderSize + bit / 8] |= (byte)(1 << (bit % 8)); + BitmapBits.Set(UsageMap.BitmapPageBits(bitmap, format), bit, true); _channel.WritePage(number, bitmap); } - private void WriteGlobalRecord(MapRecord free, byte[] record) - { - byte[] page = _channel.ReadPage(free.PageNumber).Span.ToArray(); - var holder = new DataPage(); - holder.Read(_channel.ReadPage(free.PageNumber), _channel.Format); - byte[] rewritten = UsageMapWriter.ReplaceMapRecord(page, holder, _channel.Format, free.Row, record, out _) - ?? throw new InvalidDataException("Global allocation map cannot fit its holder page."); - _channel.WritePage(free.PageNumber, rewritten); - } - /// Returns a page to a reference-type global free map by setting its bit on the bitmap page for /// its range. If that range has no bitmap page (e.g. a page grown past the map's coverage), the page is /// left unrecorded — it simply won't be reused, matching the pre-existing inline-window behaviour. private void FreeInReferenceMap(MapRecord free, MapRecord released, int page) { - var format = _channel.Format; + JetFormatBase format = _channel.Format; ReadOnlySpan map = free.Record; - int pagesPerBitmap = (format.PageSize - BitmapPageHeaderSize) * 8; + int pagesPerBitmap = format.UsageMapPagesPerBitmapPage; int slot = page / pagesPerBitmap; - if (slot < 0 || slot >= ReferenceMapSlots) return; // beyond the map's ~2 GB reach + if (slot < 0 || slot >= format.UsageMapReferenceSlots) return; // beyond the map's ~2 GB reach - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(map.Slice(1 + slot * 4, 4)); + int bitmapPage = UsageMap.ReferencePointer(map, slot, format); if (bitmapPage == 0) return; // range has no bitmap page — nothing to record into int bitInRange = page - slot * pagesPerBitmap; byte[] bitmap = ValidateBitmapPage(bitmapPage, free, released); if (page == bitmapPage) throw new InvalidDataException($"Usage-map bitmap page {page} cannot be marked globally free."); - bitmap[BitmapPageHeaderSize + bitInRange / 8] |= (byte)(1 << (bitInRange % 8)); + BitmapBits.Set(UsageMap.BitmapPageBits(bitmap, format), bitInRange, true); _channel.WritePage(bitmapPage, bitmap); } @@ -513,10 +454,11 @@ private void FreeInReferenceMap(MapRecord free, MapRecord released, int page) private (MapRecord Free, MapRecord Released) ReadGlobalMaps() { ReadOnlySpan page0 = _channel.ReadPage(0).Span; + JetFormatBase format = _channel.Format; (int Row, int Page) freePointer = - DatabaseDefinitionPage.ReadMapPointer(page0, Formats.JetFormatBase.FreePagesMapPointerOffset); + DatabaseDefinitionPage.ReadMapPointer(page0, format.FreePagesMapPointerOffset, format); (int Row, int Page) releasedPointer = - DatabaseDefinitionPage.ReadMapPointer(page0, Formats.JetFormatBase.ReleasedPagesMapPointerOffset); + DatabaseDefinitionPage.ReadMapPointer(page0, format.ReleasedPagesMapPointerOffset, format); if (freePointer == releasedPointer) throw new InvalidDataException( $"Page 0 names the same record (page {freePointer.Page}, row {freePointer.Row}) for the global " + @@ -530,54 +472,26 @@ private void FreeInReferenceMap(MapRecord free, MapRecord released, int page) private MapRecord ReadMapRecord(string name, (int Row, int Page) pointer) { - if (pointer.Page <= 0 || pointer.Page >= _channel.PageCount) - throw new InvalidDataException( - $"Page 0's global {name} map pointer names page {pointer.Page}, outside the file's pages 1..{_channel.PageCount - 1}."); - PageBuffer buffer = _channel.ReadPage(pointer.Page); - if (PageHeader.ReadType(buffer.Span) != PageType.DataPage) - throw new InvalidDataException( - $"Page 0's global {name} map pointer names page {pointer.Page}, which is not a data page."); - var data = new DataPage(); - data.Read(buffer, _channel.Format); - if (pointer.Row >= data.RowCount) - throw new InvalidDataException( - $"Page 0's global {name} map pointer names row {pointer.Row} of page {pointer.Page}, which has {data.RowCount} rows."); - RowSlot slot = data.Rows[pointer.Row]; - if (slot.IsDeleted || slot.HasOverflow || slot.Length == 0) - throw new InvalidDataException( - $"Global {name} map (page {pointer.Page}, row {pointer.Row}) is deleted, overflowed, or empty."); - - var record = new MapRecord(name, pointer.Page, pointer.Row, buffer.Span.ToArray(), slot); - if (record.Type == InlineMapType) - { - if (slot.Length < 5) - throw new InvalidDataException($"Global inline {name} map is shorter than its 5-byte header."); - } - else if (record.Type == ReferenceMapType) - { - if (slot.Length != 1 + ReferenceMapSlots * 4) - throw new InvalidDataException( - $"Global reference {name} map must be exactly {1 + ReferenceMapSlots * 4} bytes; got {slot.Length}."); - } - else - { - throw new InvalidDataException($"Global {name} map has unknown type 0x{record.Type:X2}."); - } - return record; + // The global maps' holder belongs to no table either, but its owner field reads 1 rather than 0 (observed), + // so it takes only the common checks. The page is copied: Allocate and Free write the record in place. + (PageBuffer page, _, DataPage.RowSlot slot) = UsageMap.ReadRecord( + _channel, pointer.Row, pointer.Page, $"Page 0's global {name} map pointer"); + return new MapRecord(name, pointer.Page, pointer.Row, page.Span.ToArray(), slot); } /// The bitmap pages the two reference-form maps own, each validated; a page may belong to only /// one slot of one map. private HashSet ReferenceBitmapPages(MapRecord free, MapRecord released) { + JetFormatBase format = _channel.Format; var pages = new HashSet(); foreach (MapRecord map in new[] { free, released }) { - if (map.Type != ReferenceMapType) continue; + if (map.Type != UsageMapType.Reference) continue; ReadOnlySpan record = map.Record; - for (int slot = 0; slot < ReferenceMapSlots; slot++) + for (int slot = 0; slot < format.UsageMapReferenceSlots; slot++) { - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(record.Slice(1 + slot * 4, 4)); + int bitmapPage = UsageMap.ReferencePointer(record, slot, format); if (bitmapPage == 0) continue; ValidateBitmapPage(bitmapPage, free, released); if (!pages.Add(bitmapPage)) @@ -590,10 +504,7 @@ private HashSet ReferenceBitmapPages(MapRecord free, MapRecord released) private byte[] ValidateBitmapPage(int pageNumber, MapRecord free, MapRecord released) { ValidateReusablePage(pageNumber, "usage-map bitmap pointer", free, released, _channel.PageCount - 1); - byte[] page = _channel.ReadPage(pageNumber).Span.ToArray(); - if (PageHeader.ReadType(page) != PageType.PageUsageBitmap || page[2] != 0 || page[3] != 0) - throw new InvalidDataException($"Global map pointer {pageNumber} does not target a valid bitmap page."); - return page; + return UsageMap.ReadBitmapPage(_channel, pageNumber).Span.ToArray(); } /// Page 0 and the pages holding the two global maps are never allocatable; nor is a page past @@ -622,10 +533,10 @@ private void EnsurePhysicalAllocation(int page, ReleasedPages releasedPages) if (!releasedPages.Contains(_channel.PageCount)) throw new InvalidDataException( $"Global free map selected page {page} past a gap at page {_channel.PageCount} that is neither free nor released."); - _channel.AllocatePage(); + Append(); } if (page < _channel.PageCount) return; - int allocated = _channel.AllocatePage(); + int allocated = Append(); if (allocated != page) throw new InvalidDataException( $"Global free map selected append page {page}, but contiguous allocation produced page {allocated}."); @@ -647,27 +558,26 @@ public ReleasedPages(PageAllocator owner, MapRecord map) public bool Contains(int page) { if (page < 0) return false; + JetFormatBase format = _owner._channel.Format; ReadOnlySpan record = _map.Record; - if (_map.Type == InlineMapType) + if (_map.Type == UsageMapType.Inline) { - int start = BinaryPrimitives.ReadInt32LittleEndian(record.Slice(1, 4)); - long bit = (long)page - start; - if (bit < 0 || bit / 8 >= record.Length - 5) return false; - return (record[5 + (int)(bit / 8)] & (1 << (int)(bit % 8))) != 0; + ReadOnlySpan bits = record[format.UsageMapInlineHeaderSize..]; + long bit = (long)page - UsageMap.StartPage(record, format); + return bit >= 0 && bit < bits.Length * 8L && BitmapBits.Get(bits, (int)bit); } - int pagesPerBitmap = (_owner._channel.PageSize - BitmapPageHeaderSize) * 8; + int pagesPerBitmap = format.UsageMapPagesPerBitmapPage; int slot = page / pagesPerBitmap; - if (slot >= ReferenceMapSlots) return false; + if (slot >= format.UsageMapReferenceSlots) return false; if (!_bitmaps.TryGetValue(slot, out byte[]? bitmap)) { - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(record.Slice(1 + slot * 4, 4)); - bitmap = bitmapPage == 0 ? null : _owner._channel.ReadPage(bitmapPage).Span.ToArray(); + int bitmapPage = UsageMap.ReferencePointer(record, slot, format); + bitmap = bitmapPage == 0 ? null : UsageMap.ReadBitmapPage(_owner._channel, bitmapPage).Span.ToArray(); _bitmaps[slot] = bitmap; } - if (bitmap is null) return false; - int inRange = page - slot * pagesPerBitmap; - return (bitmap[BitmapPageHeaderSize + inRange / 8] & (1 << (inRange % 8))) != 0; + return bitmap is not null + && BitmapBits.Get(UsageMap.BitmapPageBits(bitmap, format), page - slot * pagesPerBitmap); } } } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/RowCodec.cs b/src/LibRed/LibRed.Core/Storage/RowCodec.cs new file mode 100644 index 00000000..0f2eb49a --- /dev/null +++ b/src/LibRed/LibRed.Core/Storage/RowCodec.cs @@ -0,0 +1,668 @@ +using LibRed.Catalog; +using LibRed.Formats; +using LibRed.Storage.Types; +using System.Buffers.Binary; + +namespace LibRed.Storage; + +/// +/// Encodes and decodes CLR values and owns the Jet 4 / ACE inline row layout and declaration limits. +/// The null bitmap marks present (non-null) columns; a Boolean column has no data and its +/// bit carries the value. Variable columns are laid out in ascending VariableIndex order with +/// an end-first offset table. A memo/OLE column's value is written as an *inline* long-value +/// (12-byte descriptor + payload, §8) when it is small enough; anything larger is stored on LVAL +/// pages by before the row reaches here, so only the descriptor is encoded. +/// A calculated column is the exception, because its value is derived here rather than supplied: an +/// oversized result is spilled through the spillCalculated callback the writer passes in. +/// +/// +/// Decoding a long value needs , and says so. A memo or OLE column +/// holds a 12-byte descriptor rather than its data, so without page access the decode can only hand back the +/// pointer — a byte[] where the caller expects a string, which is an error nowhere and arrives at an +/// index key encoder as one. Rather than return it, refuses. The reader stays optional +/// because a row with no long value in it needs no pages, which is what lets the row codec be tested without +/// a file; it is only the value that cannot be faked. +/// The two operations that want the stored descriptors rather than the values — +/// LongValueDescriptors and CalculatedSlots — are static. They need no reader, so +/// they are not reached through an instance that might lack one, and no caller has to decide what an +/// instance "mode" means. Each takes either the row alone or a the caller has already +/// parsed, so a caller wanting both off one row derives the trailer arithmetic once. +/// +public sealed class RowCodec(IReadOnlyList columns, JetFormatBase format, + int? fixedDataLength = null, int? variableColumnCount = null, + Func? spillCalculated = null, int? columnIdHighWater = null, + LongValueStore? longValues = null, bool[]? decode = null) +{ + private readonly IReadOnlyList _columns = columns; + + // The row's field sizes, the long-value descriptor sizes and the inline limit all come from here. + private readonly JetFormatBase _format = format; + + // How an oversized calculated result reaches an LVAL page, returning the in-row descriptor. A calculated + // column is the one value the encoder DERIVES rather than receives, so it cannot have been materialised + // before the row arrived here the way a plain memo is; the writer that owns the pages supplies this + // instead. Null for a standalone encode, which has no channel to write to and so must refuse. + private readonly Func? _spillCalculated = spillCalculated; + + // How many variable slots a row carries. The TDEF's 0x2B when the caller has it — a high-water that + // never decrements — else the tight maximum over the live columns, which is the same number until the + // LAST variable column is dropped and is all a standalone encode can know. + private readonly int? _variableColumnCount = variableColumnCount; + + // How many column ids the row's leading count and null bitmap span: the TDEF's 0x29 high-water when the + // caller has it, which is what ACE writes even after the highest-id column has been dropped — else the + // highest live id + 1, all a standalone encode can know. + private readonly int? _columnIdHighWater = columnIdHighWater; + + // Fixed (non-boolean) columns occupy a contiguous region; its length is defined by the + // table definition. Default to the tight max so a standalone encode round-trips; INSERT + // passes the TDEF's actual fixed-row size so the on-disk layout matches Access. + private readonly int _fixedDataLength = fixedDataLength ?? ComputeFixedDataLength(columns); + + public byte[] Encode(object?[] values) => Encode(values, null); + + /// Encodes a row, recomputing its calculated columns. + /// The row's values, one per column in table order. + /// Envelopes to carry over verbatim, keyed by + /// . An UPDATE that touches nothing a calculated column reads must leave + /// the cached value exactly as it was: ACE recomputes only when a referenced column is written, so + /// recomputing unconditionally would write bytes ACE would not have (§3.4a). Null on INSERT, where every + /// calculated column is computed fresh. + /// The row as its columns hold it, for the calculated columns to read, when + /// already carries long values as their on-disk descriptors. A memo's text is + /// what an expression reads, not the descriptor that points at it. Null when the two are the same. + public byte[] Encode(object?[] values, IReadOnlyDictionary? preservedCalculated, + object?[]? logicalValues = null) + { + if (values.Length != _columns.Count) + throw new ArgumentException($"Expected {_columns.Count} values, got {values.Length}.", nameof(values)); + + // The leading count and the null-bitmap width span every column id the table has ever handed out — the + // TDEF's 0x29 high-water — NOT the live column count, and not the highest live id either. The three + // coincide while ids are contiguous (fresh table / ADD COLUMN) and diverge once one is dead: a burned + // type-change id, or a DROP COLUMN gap. Measured vs ACE: after dropping the only other column of a + // two-column table, ACE still writes count 2 (spec §5). AssembleRow derives the bitmap width from this. + int maxColumnId = Math.Max( + (_columnIdHighWater ?? 0) - 1, + _columns.Count == 0 ? -1 : _columns.Max(c => c.ColumnId)); + + // And the variable section is addressed the same way: by VariableIndex, NOT by position among the + // live variable columns. DROP COLUMN leaves a hole in the index space — the TDEF's 0x2B count is a + // high-water mark that never decrements, and a column added later takes the next index above it — so + // packing the chunks densely puts every column after the hole one slot too low. The decoder reads + // VarChunk(column.VariableIndex) and so does ACE, which is what makes it silent: the row is written + // and read back happily by nothing at all. + // The trailer survives the loss of the last variable column, too: ACE keeps writing it, with numVar at + // the 0x2B high-water, once a table has ever had one (measured — a table whose only TEXT column was + // dropped still gets `06 00 06 00 | 01 00`). Only a table that never had one has no trailer at all. + var varCols = _columns.Where(c => !c.IsFixedLength).ToList(); + int numVar = Math.Max( + _variableColumnCount ?? 0, + varCols.Count == 0 ? 0 : varCols.Max(c => c.VariableIndex) + 1); + + // Encode each region's payload first so we can size the row exactly. + var fixedRegion = new byte[_fixedDataLength]; + foreach (ColumnDef column in _columns) + { + if (column.Type == JetDataType.Boolean || !column.IsFixedLength) continue; + object? v = values[column.Index]; + if (v is null) continue; // null fixed value: leave its slot zeroed, clear the bit below + byte[] encoded = JetTypeCodec.Encode(column, v, _format); + if (encoded.Length != column.Length) + throw new InvalidOperationException($"Column '{column.Name}' encoded to {encoded.Length} bytes, expected {column.Length}."); + encoded.CopyTo(fixedRegion.AsSpan(column.FixedOffset)); + } + + var varChunks = new byte[numVar][]; + Array.Fill(varChunks, []); // a dropped column's slot stays present, and empty + foreach (ColumnDef column in varCols) + { + if (column.IsCalculated) + { + varChunks[column.VariableIndex] = EncodeCalculated(column, logicalValues ?? values, preservedCalculated); + continue; + } + object? v = values[column.Index]; + varChunks[column.VariableIndex] = v is null ? [] : JetTypeCodec.Encode(column, v, _format); + } + + return AssembleRow(_format, maxColumnId, fixedRegion, varChunks, _columns, values); + } + + /// The slot for a calculated column: the envelope holding the result, wrapped in a long-value + /// descriptor when the column owns a long-value map (which is how ACE stores a calculated Memo). + private byte[] EncodeCalculated(ColumnDef column, object?[] values, + IReadOnlyDictionary? preservedCalculated) + { + if (preservedCalculated is not null && preservedCalculated.TryGetValue(column.Index, out byte[]? kept)) + return kept; + + byte[] envelope = CalculatedValue.Encode(column, CalculatedValue.Evaluate(column, _columns, values), _format); + if (!column.HasLongValueMap) return envelope; + + // A memo-backed result inlines while it fits and spills to an LVAL page once it does not — the same + // 64-byte boundary a plain memo uses, and for the same reason: Access reads an inlined long value + // back but refuses one that should have been on a page. What goes on the page is the WHOLE envelope, + // header and padding included, because that is what the in-row slot would otherwise have held. + if (envelope.Length <= _format.LongValueMaxInline) + return JetTypeCodec.EncodeInlineLongValue(envelope, _format); + + return _spillCalculated is not null + ? _spillCalculated(column, envelope) + : throw new NotSupportedException( + $"Calculated column '{column.Name}' produced a {envelope.Length}-byte value, too large to " + + "store inline. Encoding it needs a writer that can allocate long-value pages, which a " + + "standalone RowEncoder has no channel for — insert or update through RowInserter."); + } + + /// Rejects a variable TEXT/BINARY value longer than its column's declared width, as ACE does + /// (measured in ColumnLengthAccessTests); without it LibRed wrote rows Access will not read back. + /// Memo/OLE are exempt — they encode to a long-value descriptor whose size is unrelated to + /// . Fixed columns are checked by + /// before the codec pads them, since padding to width would otherwise hide an over-long value. + private static void EnsureFitsDeclaredLength(ColumnDef column, byte[] encoded) + { + if (column.Type is not (JetDataType.Text or JetDataType.Binary or JetDataType.BigBinary)) return; + if (column.Length <= 0) return; + + // In the column's own units: TEXT declares characters, BINARY bytes. A TEXT value is counted by + // decoding it, because WITH COMPRESSION stores a Latin-1 value one byte per character — counted in + // bytes, a TEXT(5) would take 8 characters, where ACE refuses the sixth. Neither encoding spends + // fewer than one byte per character, so a value within the declared count of BYTES is within the + // declared count of characters and needs no decoding — which is every ordinary value. + bool text = column.Type == JetDataType.Text; + int declared = text ? column.Length / 2 : column.Length; + if (encoded.Length <= declared) return; + + int actual = text ? JetTypeCodec.DecodeText(encoded).Length : encoded.Length; + if (actual <= declared) return; + throw new InvalidOperationException( + $"The field '{column.Name}' is too small to accept the amount of data you attempted to add: " + + $"{actual} {(text ? "characters" : "bytes")} into a column declared to hold {declared}."); + } + + /// + /// Smallest fixed region ACE writes in a row that has no variable trailer. It is a floor, not an + /// alignment: measured against ACE, a region of 0 bytes (a table of only Booleans, which occupy none) + /// is padded to 2 and 1 byte (a lone BYTE column) to 2, while 3 stays 3 — a three-BYTE table's + /// row is 6 bytes, odd region and all. A row that carries a variable trailer is exempt: ACE leaves a + /// TEXT-only table's fixed region at 0. + /// + /// + /// Matching it is not cosmetic. Without the pad an all-Boolean table of eight columns or fewer encodes to a + /// 3-byte record, and ACE misreads that record — every Boolean in it comes back False, whichever + /// engine created the table. Measured both ways round: ACE's own table filled by LibRed read False, and + /// LibRed's table filled by ACE read True, which is what pins the fault to the record rather than the TDEF. + /// The cliff is at 4 bytes — a 16-Boolean row (2-byte bitmap, so 4 bytes) reads back correctly — but ACE's + /// own writer never emits a record under 5, so the short form is simply a shape its reader has never met. + /// The TDEF's fixed-row length keeps the true, unpadded value: ACE stores 1 for a BYTE table while + /// writing 5-byte rows into it, so this rounding happens at row-write time and nowhere else. + /// + private const int MinFixedRegion = 2; + + /// Assembles the on-disk row bytes from a prepared fixed region and the ordered variable chunks: + /// [count][fixed][var data][var-offset table][numVar] (the variable section is omitted entirely when + /// there are none) then [null bitmap]. The count and bitmap width are maxColumnId + 1; a + /// column's bit is set when present (Boolean = its truthy value), and a dead id's (a gap below the max, from a + /// burned/dropped id) is taken from — the row's bitmap before an ALTER COLUMN + /// re-lay — or left clear for a new row — all verified vs ACE (§5). Shared by Encode and the ALTER + /// COLUMN row re-lay so the two can never drift. + /// + /// The declared-width check runs HERE rather than in Encode. It used to sit above this call, + /// which meant the ALTER COLUMN re-lay — the other caller — never got it, and a narrowing retype could + /// write rows Access refuses. A guard that both paths must pass through belongs on the shared path. + /// + internal static byte[] AssembleRow(JetFormatBase format, int maxColumnId, ReadOnlySpan fixedRegion, + IReadOnlyList varChunks, IReadOnlyList columns, object?[] values, + ReadOnlySpan priorBitmap = default) + { + // A calculated column is exempt from the declared-width check: its length field is a constant ACE + // writes (39 for a value type, 509 for any Text, whatever size was asked for), not a limit — a + // Text(5) calculated column stores a far longer result and ACE reads it back in full (§3.4a). + foreach (ColumnDef column in columns) + if (!column.IsFixedLength && !column.IsCalculated + && column.VariableIndex >= 0 && column.VariableIndex < varChunks.Count) + EnsureFitsDeclaredLength(column, varChunks[column.VariableIndex]); + + int countSize = format.RowColumnCountSize; + int offsetSize = format.RowVariableOffsetSize, numVarSize = format.RowVariableCountSize; + int count = maxColumnId + 1; + int nullBitmapSize = BitmapBits.ByteCount(count); + int numVar = varChunks.Count; + int varDataLength = 0; + for (int j = 0; j < numVar; j++) varDataLength += varChunks[j].Length; + int varSectionLen = numVar > 0 ? varDataLength + (numVar + 1) * offsetSize + numVarSize : 0; + + // ACE pads an all-fixed row's fixed region out to MinFixedRegion; a row with a variable trailer is + // left alone. The pad is zero bytes between the fixed values and the null bitmap. + int fixedLen = numVar > 0 ? fixedRegion.Length : Math.Max(fixedRegion.Length, MinFixedRegion); + + var row = new byte[countSize + fixedLen + varSectionLen + nullBitmapSize]; + BinaryPrimitives.WriteUInt16LittleEndian(row.AsSpan(0, countSize), (ushort)count); + fixedRegion.CopyTo(row.AsSpan(countSize)); + + if (numVar > 0) + { + int varDataStart = countSize + fixedLen; + int pos = varDataStart; + for (int j = 0; j < numVar; j++) { varChunks[j].CopyTo(row.AsSpan(pos)); pos += varChunks[j].Length; } + + // End-first offset table: entry[numVar] = var-data start, entry[numVar-j-1] = end of var col j. + int tableStart = pos; + BinaryPrimitives.WriteUInt16LittleEndian(row.AsSpan(tableStart + numVar * offsetSize, offsetSize), (ushort)varDataStart); + int running = varDataStart; + for (int j = 0; j < numVar; j++) + { + running += varChunks[j].Length; + BinaryPrimitives.WriteUInt16LittleEndian( + row.AsSpan(tableStart + (numVar - j - 1) * offsetSize, offsetSize), (ushort)running); + } + BinaryPrimitives.WriteUInt16LittleEndian( + row.AsSpan(tableStart + (numVar + 1) * offsetSize, numVarSize), (ushort)numVar); + } + + // The null bitmap ends the row whatever precedes it. + Span nullBitmap = row.AsSpan(row.Length - nullBitmapSize); + var liveIds = new HashSet(); + foreach (ColumnDef column in columns) + { + liveIds.Add(column.ColumnId); + // A calculated column is always present, even when its expression evaluated to Null — ACE marks + // the bit and stores a zero-length payload, so here the bit means "has an envelope" and says + // nothing about the value (§3.4a). It is also why a calculated Boolean cannot use the bit as its + // value the way a real one does. + bool present = column.IsCalculated + || (column.Type == JetDataType.Boolean ? IsTruthy(values[column.Index]) : values[column.Index] is not null); + if (present) BitmapBits.Set(nullBitmap, column.ColumnId, true); + } + // A dead id's bit depends on which route wrote the row, and both are measured against ACE: the ALTER + // COLUMN re-lay carries the old row's bit forward — set where the retyped column held a value, clear + // where it was NULL — while a row INSERTED afterwards leaves it clear: the same statement pair gives + // ACE 0x0F for a re-laid row with a value and 0x0D for the next insert. + for (int id = 0; id <= maxColumnId && id < priorBitmap.Length * 8; id++) + if (!liveIds.Contains(id) && BitmapBits.Get(priorBitmap, id)) + BitmapBits.Set(nullBitmap, id, true); + return row; + } + + /// Access truthiness for a Boolean (bit) value being stored: a bool is itself, any non-zero + /// number is true, 0 / null is false. The index key uses the same rule, so a key always agrees with its row. + internal static bool IsTruthy(object? value) => value switch + { + null => false, + bool b => b, + _ => Convert.ToBoolean(value, System.Globalization.CultureInfo.InvariantCulture), + }; + + private static int ComputeFixedDataLength(IReadOnlyList columns) + { + int length = 0; + foreach (ColumnDef c in columns) + if (c.IsFixedLength && c.Type != JetDataType.Boolean) + length = Math.Max(length, c.FixedOffset + c.Length); + return length; + } + + private readonly bool[]? _decode = decode; + + private readonly LongValueStore? _longValues = longValues; + + // Decode runs once per row, so it walks an array rather than enumerating the interface (a boxed + // enumerator per row), and answers Layout.HasVariableSection from the lowest variable column id + // instead of re-scanning the columns: a row has a variable section exactly when its stored count + // reaches past that id. + private readonly ColumnDef[] _columnArray = [.. columns]; + private readonly int _lowestVariableId = LowestVariableId(columns); + + private static readonly object BoxedTrue = true; + private static readonly object BoxedFalse = false; + + private static int LowestVariableId(IReadOnlyList columns) + { + int lowest = int.MaxValue; + foreach (ColumnDef column in columns) + if (!column.IsFixedLength && column.ColumnId < lowest) + lowest = column.ColumnId; + return lowest; + } + + /// Decodes the row into one value per column (aligned to ). + public object?[] Decode(ReadOnlySpan row) + { + var values = new object?[_columnArray.Length]; + + // The null-bitmap width comes from the row's own leading count (= max id + 1), NOT the live column + // count — they differ once ids have a gap (a burned type-change id / DROP COLUMN gap). Reading the + // stored count is exactly how ACE sizes it, and is robust to any id scheme (spec §5). A real inline + // row is at least: column count + an empty var table (1 entry) + var count + null bitmap; anything + // shorter is an overflow/lookup pointer slot the caller should have skipped. + bool hasVar = _lowestVariableId < Layout.ReadColumnCount(row, _format); + Layout layout = Layout.Parse(row, _format, hasVar); + + foreach (ColumnDef column in _columnArray) + { + if (_decode is not null && !_decode[column.Index]) + continue; + + bool present = layout.IsPresent(column.ColumnId); + + // Jet stores Boolean (YesNo) columns with no fixed/variable data: the value + // IS the null-bitmap bit (set = true). Booleans are never null. + // + // A CALCULATED Boolean is the exception, and a silent one: it carries a real variable-length + // slot, and its bitmap bit means "present" like every other calculated column, so it is set + // for a stored False too. Reading the bit here would report True for every row. + if (column.Type == JetDataType.Boolean && !column.IsCalculated) + { + values[column.Index] = present ? BoxedTrue : BoxedFalse; + continue; + } + + if (!present) + { + values[column.Index] = null; + continue; + } + + ReadOnlySpan raw = column.IsFixedLength + ? FixedSlice(row, layout, column) + : layout.VarChunk(column.VariableIndex); + + // A calculated column stores ACE's cached result wrapped in an envelope, routed through a + // long-value descriptor when the column owns a long-value map (which is how a calculated Memo + // arrives — ACE declares it Text, so the Memo branch below would never fire for it). + if (column.IsCalculated) + { + ReadOnlySpan envelope = column.HasLongValueMap + ? Reader(column).Resolve(raw) + : raw; + values[column.Index] = CalculatedValue.Decode(column, envelope); + continue; + } + + // Memo / OLE columns store a long-value descriptor, not the data itself. + if (column.Type is JetDataType.Memo or JetDataType.Ole) + { + byte[] data = Reader(column).Resolve(raw); + values[column.Index] = column.Type == JetDataType.Memo + ? JetTypeCodec.DecodeText(data) + : data; + continue; + } + + values[column.Index] = JetTypeCodec.Decode(column, raw); + } + + return values; + } + + /// Returns the raw in-row long-value descriptor bytes for each present memo/OLE column (keyed by + /// ), WITHOUT resolving the value. Used by UPDATE/DELETE to preserve an + /// unchanged column's descriptor verbatim (avoiding a needless re-materialise) and to free a replaced or + /// deleted value's LVAL pages. + public static Dictionary LongValueDescriptors( + IReadOnlyList columns, JetFormatBase format, ReadOnlySpan row) => + LongValueDescriptors(columns, ParseLayout(columns, format, row), row); + + /// + /// Takes a layout the caller has already parsed — an UPDATE wants this and + /// off one row, and + /// exists so that arithmetic is done once. + internal static Dictionary LongValueDescriptors( + IReadOnlyList columns, Layout layout, ReadOnlySpan row) + { + var result = new Dictionary(); + + // Keyed on owning a long-value map rather than on the declared type: a calculated Memo is declared + // Text and still stores a descriptor, so a type test alone walks past its pages and orphans them. + foreach (ColumnDef column in columns) + if ((column.Type is JetDataType.Memo or JetDataType.Ole || column.HasLongValueMap) + && layout.IsPresent(column.ColumnId)) + result[column.Index] = layout.VarChunk(column.VariableIndex).ToArray(); + + return result; + } + + /// Returns each calculated column's stored slot verbatim (keyed by ) + /// — the envelope, or the long-value descriptor wrapping it — WITHOUT decoding or resolving it. An UPDATE + /// that touches nothing the expression reads writes these back unchanged, which is what ACE does. + public static Dictionary CalculatedSlots( + IReadOnlyList columns, JetFormatBase format, ReadOnlySpan row) => + CalculatedSlots(columns, ParseLayout(columns, format, row), row); + + /// + /// Takes a layout the caller has already parsed; see the + /// overload. + internal static Dictionary CalculatedSlots( + IReadOnlyList columns, Layout layout, ReadOnlySpan row) + { + var result = new Dictionary(); + + foreach (ColumnDef column in columns) + if (column.IsCalculated && !column.IsFixedLength + && layout.IsPresent(column.ColumnId) + && column.VariableIndex >= 0 && column.VariableIndex < layout.NumVar) + result[column.Index] = layout.VarChunk(column.VariableIndex).ToArray(); + + return result; + } + + private ReadOnlySpan FixedSlice(ReadOnlySpan row, Layout layout, ColumnDef column) + { + int start = _format.RowColumnCountSize + column.FixedOffset; + long end = (long)start + column.Length; + if (column.FixedOffset < 0 || column.Length < 0 + || end > _format.RowColumnCountSize + (long)layout.FixedRegionLength) + throw new InvalidDataException( + $"Row fixed value for column '{column.Name}' extends outside the fixed-data region."); + return row.Slice(start, column.Length); + } + + /// The long-value reader, or a refusal naming the column that needed it. Silently returning the + /// descriptor instead is the failure this exists to prevent: it is a byte[] that looks like a + /// value, and it travels — into an index key, into a comparison, into another row — before anything + /// notices. + private LongValueStore Reader(ColumnDef column) => + _longValues ?? throw new InvalidOperationException( + $"Column '{column.Name}' stores its value on long-value pages, so decoding it needs a " + + "LongValueReader; this RowDecoder was constructed without one. " + + $"Use {nameof(LongValueDescriptors)} to read the stored descriptors instead."); + + internal static Layout ParseLayout(IReadOnlyList columns, JetFormatBase format, ReadOnlySpan row) => + Layout.Parse(row, format, Layout.HasVariableSection(row, columns, format)); + + + /// The widest a single non-Memo/OLE column may be declared: 255 Text characters, or 510 bytes + /// of Binary. + public const int MaxFieldBytes = 510; + + /// The widest a BigBinary column may be declared: BIGBINARY(4001) gives ACE's "Size of + /// field is too long". + public const int MaxBigBinaryBytes = 4000; + + /// Throws if a column is declared wider than ACE stores. Memo and OLE are exempt — their data + /// lives on long-value pages and the in-row descriptor is a fixed size. + public static void ValidateFieldWidth(string columnName, JetDataType type, int lengthBytes) + { + if (type is JetDataType.Memo or JetDataType.Ole) return; + if (type == JetDataType.BigBinary) + { + if (lengthBytes > MaxBigBinaryBytes) + throw new NotSupportedException( + $"Column '{columnName}' is declared {lengthBytes} bytes wide; a BigBinary column holds at most " + + $"{MaxBigBinaryBytes}."); + return; + } + if (lengthBytes > MaxFieldBytes) + throw new NotSupportedException( + $"Column '{columnName}' is declared {lengthBytes} bytes wide; a column holds at most {MaxFieldBytes} " + + $"bytes ({MaxFieldBytes / 2} Text characters). Use Memo or OLE for anything longer."); + } + + /// The largest record the declaration can produce: the row header, the fixed region, the + /// variable section's overhead and the null bitmap. Mirrors 's layout + /// except that a table with no variable columns still gets ACE's 4-byte allowance for the section. + /// Sum of the fixed columns' widths, excluding Boolean (which has no data). + /// Number of variable-length columns, Memo and OLE included. + /// The column-id high-water plus one — what sizes the null bitmap. + /// The file's format, for the row's field sizes. + /// The variable section is budgeted even with no variable columns — one offset entry and the count, + /// which is ACE's allowance for that case. + public static int WidestRecord(int fixedBytes, int variableColumns, int columnCount, JetFormatBase format) => + format.RowColumnCountSize + + fixedBytes + + (variableColumns + 1) * format.RowVariableOffsetSize + format.RowVariableCountSize + + BitmapBits.ByteCount(columnCount); + + /// Throws if the declaration's widest possible record is one ACE would refuse the file for. + /// may be null where the caller does not know it (the table is still being + /// built), in which case the message just says "the table". + public static void ValidateRecordFits( + string? tableName, int fixedBytes, int variableColumns, int columnCount, JetFormatBase format) + { + int widest = WidestRecord(fixedBytes, variableColumns, columnCount, format); + if (widest > format.MaxRecordSize) + throw new NotSupportedException( + $"{(tableName is null ? "The table" : $"Table '{tableName}'")} declares {fixedBytes} bytes of " + + $"fixed-length columns over {columnCount} column ids, so its widest record would be {widest} " + + $"bytes; a record holds at most {format.MaxRecordSize}. Make the wide columns variable-length, " + + "or move them to Memo/OLE, which live on their own pages."); + } + + /// + /// Parses the structural trailer of an inline row record once (spec §5), so the several call sites that + /// need to locate a row's regions don't each re-derive the offset arithmetic. Layout: + /// + /// [count] [fixed data] [var data] [varOffsetTable: numVar+1 entries] [numVar] [nullBitmap] + /// + /// The field sizes are the format's (, + /// , ). The leading + /// count is maxColumnId + 1 and sets the null-bitmap width, one bit per column id. A table with NO variable + /// columns omits the whole variable section (offset table + numVar) — such a row can't self-describe that, so the + /// caller passes hasVar from the schema. + /// + internal readonly ref struct Layout + { + private readonly ReadOnlySpan _row; + private readonly int _offsetSize; + + /// The leading column count (= max column id + 1). + public int ColumnCount { get; } + /// Null-bitmap width in bytes, from the leading count. + public int NullBitmapSize { get; } + /// Number of variable columns stored (0 when the table has no variable section). + public int NumVar { get; } + /// Offset of the variable-offset table, or -1 when there is no variable section. + public int VarTableStart { get; } + /// Length of the fixed-data region (bytes between the leading count and the variable data). + public int FixedRegionLength { get; } + + private Layout(ReadOnlySpan row, JetFormatBase format, bool hasVar) + { + int countSize = format.RowColumnCountSize; + _row = row; + _offsetSize = format.RowVariableOffsetSize; + ColumnCount = ReadColumnCount(row, format); + NullBitmapSize = BitmapBits.ByteCount(ColumnCount); + int minimum = MinimumLength(format, ColumnCount, hasVar); + if (row.Length < minimum) + throw new InvalidDataException( + $"Row is {row.Length} bytes, shorter than the {minimum} its {NullBitmapSize}-byte null bitmap" + + (hasVar ? " and variable-column trailer need." : " needs.")); + + if (!hasVar) + { + NumVar = 0; + VarTableStart = -1; + FixedRegionLength = row.Length - countSize - NullBitmapSize; + return; + } + + int numVarSize = format.RowVariableCountSize; + int numVarPos = row.Length - NullBitmapSize - numVarSize; + NumVar = BinaryPrimitives.ReadUInt16LittleEndian(row.Slice(numVarPos, numVarSize)); + long tableStart = (long)numVarPos - ((long)NumVar + 1) * _offsetSize; + if (tableStart < countSize || tableStart > numVarPos) + throw new InvalidDataException( + $"Row declares {NumVar} variable slots, placing its offset table outside the row."); + VarTableStart = (int)tableStart; + + int previous = VarOffset(0); + if (previous < countSize || previous > VarTableStart) + throw new InvalidDataException( + $"Row variable-data end {previous} is outside the data region ending at {VarTableStart}."); + for (int entry = 1; entry <= NumVar; entry++) + { + int current = VarOffset(entry); + if (current < countSize || current > previous) + throw new InvalidDataException( + $"Row variable offset {entry} ({current}) is outside or above its preceding boundary {previous}."); + previous = current; + } + // The last offset-table entry is the variable-data start (= count field + fixed region). + FixedRegionLength = previous - countSize; + } + + /// Parses ; is whether the row carries a variable + /// section (). + public static Layout Parse(ReadOnlySpan row, JetFormatBase format, bool hasVar) => new(row, format, hasVar); + + /// The fewest bytes a row of column ids can take: the count, the null + /// bitmap, and — when it has a variable section — the offset table's one mandatory entry (the variable-data + /// start) and the variable-column count. Fixed data and variable data may both be empty. + public static int MinimumLength(JetFormatBase format, int columnCount, bool hasVar) => + format.RowColumnCountSize + BitmapBits.ByteCount(columnCount) + + (hasVar ? format.RowVariableOffsetSize + format.RowVariableCountSize : 0); + + /// The row's leading column count: the highest column id the table had handed out when the row was + /// written, plus one. + public static int ReadColumnCount(ReadOnlySpan row, JetFormatBase format) + { + int countSize = format.RowColumnCountSize; + if (row.Length < countSize) + throw new InvalidDataException($"Row is too short to contain its {countSize}-byte column count."); + return BinaryPrimitives.ReadUInt16LittleEndian(row[..countSize]); + } + + /// Whether this row carries a variable section — the argument every + /// caller needs, derived once here rather than at each call site. + /// + /// It is not "does the schema have a variable column": a row written before the table's first variable + /// ADD COLUMN has no trailer even though the current schema does, because ADD COLUMN is metadata-only. + /// The row's own stored count is what dates it — a column id at or above the count did not exist when + /// the row was written. Get this wrong and the parse reads the last bytes of FIXED data as numVar and an + /// offset table, which the bounds checks above usually catch, but not always. + /// + public static bool HasVariableSection(ReadOnlySpan row, IReadOnlyList columns, JetFormatBase format) + { + int storedCount = ReadColumnCount(row, format); + foreach (ColumnDef column in columns) + if (!column.IsFixedLength && column.ColumnId < storedCount) return true; + return false; + } + + /// The row's null bitmap: one bit per column id below , set when the column + /// holds a value — or, for a Boolean, when it is true. + public ReadOnlySpan NullBitmap => _row[^NullBitmapSize..]; + + /// Whether column 's null-bitmap bit is set. A column id the row predates — + /// at or above its stored count — has no bit, and reads as clear. + public bool IsPresent(int columnId) => + columnId >= 0 && columnId < ColumnCount && BitmapBits.Get(NullBitmap, columnId); + + /// The raw bytes of variable column (end-first offset table). + public ReadOnlySpan VarChunk(int variableIndex) + { + if (variableIndex < 0 || variableIndex >= NumVar) + throw new InvalidDataException( + $"Row has {NumVar} variable slots but column metadata requests slot {variableIndex}."); + int start = VarOffset(NumVar - variableIndex); + int end = VarOffset(NumVar - variableIndex - 1); + return _row[start..end]; + } + + private int VarOffset(int entry) => + BinaryPrimitives.ReadUInt16LittleEndian(_row.Slice(VarTableStart + entry * _offsetSize, _offsetSize)); + } + +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/RowDecoder.cs b/src/LibRed/LibRed.Core/Storage/RowDecoder.cs deleted file mode 100644 index 1f3e7151..00000000 --- a/src/LibRed/LibRed.Core/Storage/RowDecoder.cs +++ /dev/null @@ -1,226 +0,0 @@ -using LibRed.Catalog; -using LibRed.Formats; -using LibRed.Storage.Types; -using System.Buffers.Binary; - -namespace LibRed.Storage; - -/// -/// Decodes a single raw row record into CLR values. Implements the Jet 4 / ACE row -/// layout (verified against real data): -/// -/// [colCount:2] [fixed data] [var data] [varOffsetTable:(numVar+1)x2] [numVar:2] [nullBitmap] -/// -/// The null bitmap is indexed by column id (bit set = value present). Variable columns -/// are addressed via the trailing offset table in ascending column-id order. -/// Jet 4 / ACE uses 2-byte variable offsets at any row size — there is no Jet 3-style -/// jump table (1-byte offsets), so rows larger than 256 bytes decode the same way. -/// -/// -/// Decoding a long value needs , and says so. A memo or OLE column -/// holds a 12-byte descriptor rather than its data, so without page access the decode can only hand back the -/// pointer — a byte[] where the caller expects a string, which is an error nowhere and arrives at an -/// index key encoder as one. Rather than return it, refuses. The reader stays optional -/// because a row with no long value in it needs no pages, which is what lets the row codec be tested without -/// a file; it is only the value that cannot be faked. -/// The two operations that want the stored descriptors rather than the values — -/// LongValueDescriptors and CalculatedSlots — are static. They need no reader, so -/// they are not reached through an instance that might lack one, and no caller has to decide what an -/// instance "mode" means. Each takes either the row alone or a the caller has already -/// parsed, so a caller wanting both off one row derives the trailer arithmetic once. -/// -/// The table's columns. -/// The file's format. -/// Page access for memo/OLE values; see the remarks. -/// Which columns to decode, by , or null for all of them. A -/// column left out is null in every row, whatever is stored — so this is only for a reader that provably never -/// looks at it (a query that does not name it), never for a row that is going to be written back. -public sealed class RowDecoder( - IReadOnlyList columns, JetFormatBase format, LongValueReader? longValues = null, bool[]? decode = null) -{ - private readonly bool[]? _decode = decode; - - private readonly JetFormatBase _format = format; - private readonly LongValueReader? _longValues = longValues; - - // Decode runs once per row, so it walks an array rather than enumerating the interface (a boxed - // enumerator per row), and answers RowLayout.HasVariableSection from the lowest variable column id - // instead of re-scanning the columns: a row has a variable section exactly when its stored count - // reaches past that id. - private readonly ColumnDef[] _columnArray = [.. columns]; - private readonly int _lowestVariableId = LowestVariableId(columns); - - private static readonly object BoxedTrue = true; - private static readonly object BoxedFalse = false; - - private static int LowestVariableId(IReadOnlyList columns) - { - int lowest = int.MaxValue; - foreach (ColumnDef column in columns) - if (!column.IsFixedLength && column.ColumnId < lowest) - lowest = column.ColumnId; - return lowest; - } - - /// Decodes the row into one value per column (aligned to ). - public object?[] Decode(ReadOnlySpan row) - { - var values = new object?[_columnArray.Length]; - - // The null-bitmap width comes from the row's own leading count (= max id + 1), NOT the live column - // count — they differ once ids have a gap (a burned type-change id / DROP COLUMN gap). Reading the - // stored count is exactly how ACE sizes it, and is robust to any id scheme (spec §5). A real inline - // row is at least: column count + an empty var table (1 entry) + var count + null bitmap; anything - // shorter is an overflow/lookup pointer slot the caller should have skipped. - if (row.Length < _format.RowColumnCountSize) - throw new InvalidDataException("Row is too short to be an inline record."); - bool hasVar = _lowestVariableId < BinaryPrimitives.ReadUInt16LittleEndian(row); - RowLayout layout = RowLayout.Parse(row, _format.RowColumnCountSize, hasVar); - int nullBitmapSize = layout.NullBitmapSize; - - ReadOnlySpan nullBitmap = row[^nullBitmapSize..]; - - foreach (ColumnDef column in _columnArray) - { - if (_decode is not null && !_decode[column.Index]) - continue; - - bool present = IsPresent(nullBitmap, layout.ColumnCount, column.ColumnId); - - // Jet stores Boolean (YesNo) columns with no fixed/variable data: the value - // IS the null-bitmap bit (set = true). Booleans are never null. - // - // A CALCULATED Boolean is the exception, and a silent one: it carries a real variable-length - // slot, and its bitmap bit means "present" like every other calculated column, so it is set - // for a stored False too. Reading the bit here would report True for every row. - if (column.Type == JetDataType.Boolean && !column.IsCalculated) - { - values[column.Index] = present ? BoxedTrue : BoxedFalse; - continue; - } - - if (!present) - { - values[column.Index] = null; - continue; - } - - ReadOnlySpan raw = column.IsFixedLength - ? FixedSlice(row, layout, column) - : layout.VarChunk(column.VariableIndex); - - // A calculated column stores ACE's cached result wrapped in an envelope, routed through a - // long-value descriptor when the column owns a long-value map (which is how a calculated Memo - // arrives — ACE declares it Text, so the Memo branch below would never fire for it). - if (column.IsCalculated) - { - ReadOnlySpan envelope = column.HasLongValueMap - ? Reader(column).Resolve(raw) - : raw; - values[column.Index] = CalculatedValue.Decode(column, envelope); - continue; - } - - // Memo / OLE columns store a long-value descriptor, not the data itself. - if (column.Type is JetDataType.Memo or JetDataType.Ole) - { - byte[] data = Reader(column).Resolve(raw); - values[column.Index] = column.Type == JetDataType.Memo - ? JetTypeCodec.DecodeText(data) - : data; - continue; - } - - values[column.Index] = JetTypeCodec.Decode(column, raw); - } - - return values; - } - - /// Returns the raw in-row long-value descriptor bytes for each present memo/OLE column (keyed by - /// ), WITHOUT resolving the value. Used by UPDATE/DELETE to preserve an - /// unchanged column's descriptor verbatim (avoiding a needless re-materialise) and to free a replaced or - /// deleted value's LVAL pages. - public static Dictionary LongValueDescriptors( - IReadOnlyList columns, JetFormatBase format, ReadOnlySpan row) => - LongValueDescriptors(columns, ParseLayout(columns, format, row), row); - - /// - /// Takes a layout the caller has already parsed — an UPDATE wants this and - /// off one row, and - /// exists so that arithmetic is done once. - internal static Dictionary LongValueDescriptors( - IReadOnlyList columns, RowLayout layout, ReadOnlySpan row) - { - var result = new Dictionary(); - int nullBitmapSize = layout.NullBitmapSize; - - ReadOnlySpan nullBitmap = row[^nullBitmapSize..]; - - // Keyed on owning a long-value map rather than on the declared type: a calculated Memo is declared - // Text and still stores a descriptor, so a type test alone walks past its pages and orphans them. - foreach (ColumnDef column in columns) - if ((column.Type is JetDataType.Memo or JetDataType.Ole || column.HasLongValueMap) - && IsPresent(nullBitmap, layout.ColumnCount, column.ColumnId)) - result[column.Index] = layout.VarChunk(column.VariableIndex).ToArray(); - - return result; - } - - /// Returns each calculated column's stored slot verbatim (keyed by ) - /// — the envelope, or the long-value descriptor wrapping it — WITHOUT decoding or resolving it. An UPDATE - /// that touches nothing the expression reads writes these back unchanged, which is what ACE does. - public static Dictionary CalculatedSlots( - IReadOnlyList columns, JetFormatBase format, ReadOnlySpan row) => - CalculatedSlots(columns, ParseLayout(columns, format, row), row); - - /// - /// Takes a layout the caller has already parsed; see the - /// overload. - internal static Dictionary CalculatedSlots( - IReadOnlyList columns, RowLayout layout, ReadOnlySpan row) - { - var result = new Dictionary(); - ReadOnlySpan nullBitmap = row[^layout.NullBitmapSize..]; - - foreach (ColumnDef column in columns) - if (column.IsCalculated && !column.IsFixedLength - && IsPresent(nullBitmap, layout.ColumnCount, column.ColumnId) - && column.VariableIndex >= 0 && column.VariableIndex < layout.NumVar) - result[column.Index] = layout.VarChunk(column.VariableIndex).ToArray(); - - return result; - } - - private ReadOnlySpan FixedSlice(ReadOnlySpan row, RowLayout layout, ColumnDef column) - { - int start = _format.RowColumnCountSize + column.FixedOffset; - long end = (long)start + column.Length; - if (column.FixedOffset < 0 || column.Length < 0 - || end > _format.RowColumnCountSize + (long)layout.FixedRegionLength) - throw new InvalidDataException( - $"Row fixed value for column '{column.Name}' extends outside the fixed-data region."); - return row.Slice(start, column.Length); - } - - /// The long-value reader, or a refusal naming the column that needed it. Silently returning the - /// descriptor instead is the failure this exists to prevent: it is a byte[] that looks like a - /// value, and it travels — into an index key, into a comparison, into another row — before anything - /// notices. - private LongValueReader Reader(ColumnDef column) => - _longValues ?? throw new InvalidOperationException( - $"Column '{column.Name}' stores its value on long-value pages, so decoding it needs a " - + $"{nameof(LongValueReader)}; this {nameof(RowDecoder)} was constructed without one. " - + $"Use {nameof(LongValueDescriptors)} to read the stored descriptors instead."); - - internal static RowLayout ParseLayout(IReadOnlyList columns, JetFormatBase format, ReadOnlySpan row) - { - if (row.Length < format.RowColumnCountSize) - throw new InvalidDataException("Row is too short to be an inline record."); - return RowLayout.Parse(row, format.RowColumnCountSize, RowLayout.HasVariableSection(row, columns)); - } - - private static bool IsPresent(ReadOnlySpan nullBitmap, int storedColumnCount, int columnId) => - columnId >= 0 && columnId < storedColumnCount - && (nullBitmap[columnId >> 3] & (1 << (columnId & 7))) != 0; -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/RowEncoder.cs b/src/LibRed/LibRed.Core/Storage/RowEncoder.cs deleted file mode 100644 index 004efa5f..00000000 --- a/src/LibRed/LibRed.Core/Storage/RowEncoder.cs +++ /dev/null @@ -1,298 +0,0 @@ -using LibRed.Catalog; -using LibRed.Formats; -using LibRed.Storage.Types; -using System.Buffers.Binary; - -namespace LibRed.Storage; - -/// -/// Encodes a row of CLR values into the Jet 4 / ACE inline row layout — the inverse of -/// : -/// -/// [colCount:2] [fixed data] [var data] [varOffsetTable:(numVar+1)x2] [numVar:2] [nullBitmap] -/// -/// The null bitmap marks present (non-null) columns; a Boolean column has no data and its -/// bit carries the value. Variable columns are laid out in ascending VariableIndex order with -/// an end-first offset table. A memo/OLE column's value is written as an *inline* long-value -/// (12-byte descriptor + payload, §8) when it is small enough; anything larger is stored on LVAL -/// pages by before the row reaches here, so only the descriptor is encoded. -/// A calculated column is the exception, because its value is derived here rather than supplied: an -/// oversized result is spilled through the spillCalculated callback the writer passes in. -/// -public sealed class RowEncoder(IReadOnlyList columns, JetFormatBase format, - int? fixedDataLength = null, int? variableColumnCount = null, - Func? spillCalculated = null, int? columnIdHighWater = null) -{ - private readonly IReadOnlyList _columns = columns; - - // Nothing in the row layout varies by format across the Jet 4 family, so this is unread today. It stays - // because Jet 3 rows differ (1-byte column counts, no variable-length offset widening), and the encoder - // will have to branch on it when that format is implemented. -#pragma warning disable CA1823, IDE0052 // Deliberately unread; see above. - private readonly JetFormatBase _format = format; -#pragma warning restore CA1823, IDE0052 - - // How an oversized calculated result reaches an LVAL page, returning the in-row descriptor. A calculated - // column is the one value the encoder DERIVES rather than receives, so it cannot have been materialised - // before the row arrived here the way a plain memo is; the writer that owns the pages supplies this - // instead. Null for a standalone encode, which has no channel to write to and so must refuse. - private readonly Func? _spillCalculated = spillCalculated; - - // How many variable slots a row carries. The TDEF's 0x2B when the caller has it — a high-water that - // never decrements — else the tight maximum over the live columns, which is the same number until the - // LAST variable column is dropped and is all a standalone encode can know. - private readonly int? _variableColumnCount = variableColumnCount; - - // How many column ids the row's leading count and null bitmap span: the TDEF's 0x29 high-water when the - // caller has it, which is what ACE writes even after the highest-id column has been dropped — else the - // highest live id + 1, all a standalone encode can know. - private readonly int? _columnIdHighWater = columnIdHighWater; - - // Fixed (non-boolean) columns occupy a contiguous region; its length is defined by the - // table definition. Default to the tight max so a standalone encode round-trips; INSERT - // passes the TDEF's actual fixed-row size so the on-disk layout matches Access. - private readonly int _fixedDataLength = fixedDataLength ?? ComputeFixedDataLength(columns); - - public byte[] Encode(object?[] values) => Encode(values, null); - - /// Encodes a row, recomputing its calculated columns. - /// The row's values, one per column in table order. - /// Envelopes to carry over verbatim, keyed by - /// . An UPDATE that touches nothing a calculated column reads must leave - /// the cached value exactly as it was: ACE recomputes only when a referenced column is written, so - /// recomputing unconditionally would write bytes ACE would not have (§3.4a). Null on INSERT, where every - /// calculated column is computed fresh. - /// The row as its columns hold it, for the calculated columns to read, when - /// already carries long values as their on-disk descriptors. A memo's text is - /// what an expression reads, not the descriptor that points at it. Null when the two are the same. - public byte[] Encode(object?[] values, IReadOnlyDictionary? preservedCalculated, - object?[]? logicalValues = null) - { - if (values.Length != _columns.Count) - throw new ArgumentException($"Expected {_columns.Count} values, got {values.Length}.", nameof(values)); - - // The leading count and the null-bitmap width span every column id the table has ever handed out — the - // TDEF's 0x29 high-water — NOT the live column count, and not the highest live id either. The three - // coincide while ids are contiguous (fresh table / ADD COLUMN) and diverge once one is dead: a burned - // type-change id, or a DROP COLUMN gap. Measured vs ACE: after dropping the only other column of a - // two-column table, ACE still writes count 2 (spec §5). AssembleRow derives the bitmap width from this. - int maxColumnId = Math.Max( - (_columnIdHighWater ?? 0) - 1, - _columns.Count == 0 ? -1 : _columns.Max(c => c.ColumnId)); - - // And the variable section is addressed the same way: by VariableIndex, NOT by position among the - // live variable columns. DROP COLUMN leaves a hole in the index space — the TDEF's 0x2B count is a - // high-water mark that never decrements, and a column added later takes the next index above it — so - // packing the chunks densely puts every column after the hole one slot too low. The decoder reads - // VarChunk(column.VariableIndex) and so does ACE, which is what makes it silent: the row is written - // and read back happily by nothing at all. - // The trailer survives the loss of the last variable column, too: ACE keeps writing it, with numVar at - // the 0x2B high-water, once a table has ever had one (measured — a table whose only TEXT column was - // dropped still gets `06 00 06 00 | 01 00`). Only a table that never had one has no trailer at all. - var varCols = _columns.Where(c => !c.IsFixedLength).ToList(); - int numVar = Math.Max( - _variableColumnCount ?? 0, - varCols.Count == 0 ? 0 : varCols.Max(c => c.VariableIndex) + 1); - - // Encode each region's payload first so we can size the row exactly. - var fixedRegion = new byte[_fixedDataLength]; - foreach (ColumnDef column in _columns) - { - if (column.Type == JetDataType.Boolean || !column.IsFixedLength) continue; - object? v = values[column.Index]; - if (v is null) continue; // null fixed value: leave its slot zeroed, clear the bit below - byte[] encoded = JetTypeCodec.Encode(column, v); - if (encoded.Length != column.Length) - throw new InvalidOperationException($"Column '{column.Name}' encoded to {encoded.Length} bytes, expected {column.Length}."); - encoded.CopyTo(fixedRegion.AsSpan(column.FixedOffset)); - } - - var varChunks = new byte[numVar][]; - Array.Fill(varChunks, []); // a dropped column's slot stays present, and empty - foreach (ColumnDef column in varCols) - { - if (column.IsCalculated) - { - varChunks[column.VariableIndex] = EncodeCalculated(column, logicalValues ?? values, preservedCalculated); - continue; - } - object? v = values[column.Index]; - varChunks[column.VariableIndex] = v is null ? [] : JetTypeCodec.Encode(column, v); - } - - return AssembleRow(maxColumnId, fixedRegion, varChunks, _columns, values); - } - - /// The slot for a calculated column: the envelope holding the result, wrapped in a long-value - /// descriptor when the column owns a long-value map (which is how ACE stores a calculated Memo). - private byte[] EncodeCalculated(ColumnDef column, object?[] values, - IReadOnlyDictionary? preservedCalculated) - { - if (preservedCalculated is not null && preservedCalculated.TryGetValue(column.Index, out byte[]? kept)) - return kept; - - byte[] envelope = CalculatedValue.Encode(column, CalculatedValue.Evaluate(column, _columns, values)); - if (!column.HasLongValueMap) return envelope; - - // A memo-backed result inlines while it fits and spills to an LVAL page once it does not — the same - // 64-byte boundary a plain memo uses, and for the same reason: Access reads an inlined long value - // back but refuses one that should have been on a page. What goes on the page is the WHOLE envelope, - // header and padding included, because that is what the in-row slot would otherwise have held. - if (envelope.Length <= LongValueFormat.MaxInlineValue) - return JetTypeCodec.EncodeInlineLongValue(envelope); - - return _spillCalculated is not null - ? _spillCalculated(column, envelope) - : throw new NotSupportedException( - $"Calculated column '{column.Name}' produced a {envelope.Length}-byte value, too large to " - + "store inline. Encoding it needs a writer that can allocate long-value pages, which a " - + "standalone RowEncoder has no channel for — insert or update through RowInserter."); - } - - /// Rejects a variable TEXT/BINARY value longer than its column's declared width, as ACE does - /// (measured in ColumnLengthAccessTests); without it LibRed wrote rows Access will not read back. - /// Memo/OLE are exempt — they encode to a long-value descriptor whose size is unrelated to - /// . Fixed columns are checked by - /// before the codec pads them, since padding to width would otherwise hide an over-long value. - private static void EnsureFitsDeclaredLength(ColumnDef column, byte[] encoded) - { - if (column.Type is not (JetDataType.Text or JetDataType.Binary or JetDataType.BigBinary)) return; - if (column.Length <= 0) return; - - // In the column's own units: TEXT declares characters, BINARY bytes. A TEXT value is counted by - // decoding it, because WITH COMPRESSION stores a Latin-1 value one byte per character — counted in - // bytes, a TEXT(5) would take 8 characters, where ACE refuses the sixth. Neither encoding spends - // fewer than one byte per character, so a value within the declared count of BYTES is within the - // declared count of characters and needs no decoding — which is every ordinary value. - bool text = column.Type == JetDataType.Text; - int declared = text ? column.Length / 2 : column.Length; - if (encoded.Length <= declared) return; - - int actual = text ? JetTypeCodec.DecodeText(encoded).Length : encoded.Length; - if (actual <= declared) return; - throw new InvalidOperationException( - $"The field '{column.Name}' is too small to accept the amount of data you attempted to add: " - + $"{actual} {(text ? "characters" : "bytes")} into a column declared to hold {declared}."); - } - - /// - /// Smallest fixed region ACE writes in a row that has no variable trailer. It is a floor, not an - /// alignment: measured against ACE, a region of 0 bytes (a table of only Booleans, which occupy none) - /// is padded to 2 and 1 byte (a lone BYTE column) to 2, while 3 stays 3 — a three-BYTE table's - /// row is 6 bytes, odd region and all. A row that carries a variable trailer is exempt: ACE leaves a - /// TEXT-only table's fixed region at 0. - /// - /// - /// Matching it is not cosmetic. Without the pad an all-Boolean table of eight columns or fewer encodes to a - /// 3-byte record, and ACE misreads that record — every Boolean in it comes back False, whichever - /// engine created the table. Measured both ways round: ACE's own table filled by LibRed read False, and - /// LibRed's table filled by ACE read True, which is what pins the fault to the record rather than the TDEF. - /// The cliff is at 4 bytes — a 16-Boolean row (2-byte bitmap, so 4 bytes) reads back correctly — but ACE's - /// own writer never emits a record under 5, so the short form is simply a shape its reader has never met. - /// The TDEF's fixed-row length keeps the true, unpadded value: ACE stores 1 for a BYTE table while - /// writing 5-byte rows into it, so this rounding happens at row-write time and nowhere else. - /// - private const int MinFixedRegion = 2; - - /// Assembles the on-disk row bytes from a prepared fixed region and the ordered variable chunks: - /// [count][fixed][var data][var-offset table][numVar] (the variable section is omitted entirely when - /// there are none) then [null bitmap]. The count and bitmap width are maxColumnId + 1; a - /// column's bit is set when present (Boolean = its truthy value), and a dead id's (a gap below the max, from a - /// burned/dropped id) is taken from — the row's bitmap before an ALTER COLUMN - /// re-lay — or left clear for a new row — all verified vs ACE (§5). Shared by Encode and the ALTER - /// COLUMN row re-lay so the two can never drift. - /// - /// The declared-width check runs HERE rather than in Encode. It used to sit above this call, - /// which meant the ALTER COLUMN re-lay — the other caller — never got it, and a narrowing retype could - /// write rows Access refuses. A guard that both paths must pass through belongs on the shared path. - /// - internal static byte[] AssembleRow(int maxColumnId, ReadOnlySpan fixedRegion, - IReadOnlyList varChunks, IReadOnlyList columns, object?[] values, - ReadOnlySpan priorBitmap = default) - { - // A calculated column is exempt from the declared-width check: its length field is a constant ACE - // writes (39 for a value type, 509 for any Text, whatever size was asked for), not a limit — a - // Text(5) calculated column stores a far longer result and ACE reads it back in full (§3.4a). - foreach (ColumnDef column in columns) - if (!column.IsFixedLength && !column.IsCalculated - && column.VariableIndex >= 0 && column.VariableIndex < varChunks.Count) - EnsureFitsDeclaredLength(column, varChunks[column.VariableIndex]); - - const int countSize = 2; - int count = maxColumnId + 1; - int nullBitmapSize = (count + 7) / 8; - int numVar = varChunks.Count; - int varDataLength = 0; - for (int j = 0; j < numVar; j++) varDataLength += varChunks[j].Length; - int varSectionLen = numVar > 0 ? varDataLength + (numVar + 1) * 2 + 2 : 0; - - // ACE pads an all-fixed row's fixed region out to MinFixedRegion; a row with a variable trailer is - // left alone. The pad is zero bytes between the fixed values and the null bitmap. - int fixedLen = numVar > 0 ? fixedRegion.Length : Math.Max(fixedRegion.Length, MinFixedRegion); - - var row = new byte[countSize + fixedLen + varSectionLen + nullBitmapSize]; - BinaryPrimitives.WriteUInt16LittleEndian(row, (ushort)count); - fixedRegion.CopyTo(row.AsSpan(countSize)); - - int bitmapPos; - if (numVar > 0) - { - int varDataStart = countSize + fixedLen; - int pos = varDataStart; - for (int j = 0; j < numVar; j++) { varChunks[j].CopyTo(row.AsSpan(pos)); pos += varChunks[j].Length; } - - // End-first offset table: entry[numVar] = var-data start, entry[numVar-j-1] = end of var col j. - int tableStart = pos; - BinaryPrimitives.WriteUInt16LittleEndian(row.AsSpan(tableStart + numVar * 2, 2), (ushort)varDataStart); - int running = varDataStart; - for (int j = 0; j < numVar; j++) - { - running += varChunks[j].Length; - BinaryPrimitives.WriteUInt16LittleEndian(row.AsSpan(tableStart + (numVar - j - 1) * 2, 2), (ushort)running); - } - int numVarPos = tableStart + (numVar + 1) * 2; - BinaryPrimitives.WriteUInt16LittleEndian(row.AsSpan(numVarPos, 2), (ushort)numVar); - bitmapPos = numVarPos + 2; - } - else bitmapPos = countSize + fixedLen; - - var liveIds = new HashSet(); - foreach (ColumnDef column in columns) - { - liveIds.Add(column.ColumnId); - // A calculated column is always present, even when its expression evaluated to Null — ACE marks - // the bit and stores a zero-length payload, so here the bit means "has an envelope" and says - // nothing about the value (§3.4a). It is also why a calculated Boolean cannot use the bit as its - // value the way a real one does. - bool present = column.IsCalculated - || (column.Type == JetDataType.Boolean ? IsTruthy(values[column.Index]) : values[column.Index] is not null); - if (present) row[bitmapPos + (column.ColumnId >> 3)] |= (byte)(1 << (column.ColumnId & 7)); - } - // A dead id's bit depends on which route wrote the row, and both are measured against ACE: the ALTER - // COLUMN re-lay carries the old row's bit forward — set where the retyped column held a value, clear - // where it was NULL — while a row INSERTED afterwards leaves it clear: the same statement pair gives - // ACE 0x0F for a re-laid row with a value and 0x0D for the next insert. - for (int id = 0; id <= maxColumnId && id < priorBitmap.Length * 8; id++) - if (!liveIds.Contains(id) && (priorBitmap[id >> 3] & (1 << (id & 7))) != 0) - row[bitmapPos + (id >> 3)] |= (byte)(1 << (id & 7)); - return row; - } - - /// Access truthiness for a Boolean (bit) value being stored: a bool is itself, any non-zero - /// number is true, 0 / null is false. The index key uses the same rule, so a key always agrees with its row. - internal static bool IsTruthy(object? value) => value switch - { - null => false, - bool b => b, - _ => Convert.ToBoolean(value, System.Globalization.CultureInfo.InvariantCulture), - }; - - private static int ComputeFixedDataLength(IReadOnlyList columns) - { - int length = 0; - foreach (ColumnDef c in columns) - if (c.IsFixedLength && c.Type != JetDataType.Boolean) - length = Math.Max(length, c.FixedOffset + c.Length); - return length; - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/RowId.cs b/src/LibRed/LibRed.Core/Storage/RowId.cs index 36613a7a..d9e7acea 100644 --- a/src/LibRed/LibRed.Core/Storage/RowId.cs +++ b/src/LibRed/LibRed.Core/Storage/RowId.cs @@ -1,4 +1,12 @@ namespace LibRed.Storage; /// A pointer to a row: the data page it lives on and its slot index within that page. -public readonly record struct RowId(int Page, int Row); \ No newline at end of file +public readonly record struct RowId(int Page, int Row) +{ + /// The row as one integer, page in the upper three bytes and row in the lowest: the value an index + /// entry's trailer carries (big-endian), and a record pointer (little-endian). + public int Packed => (Page << 8) | (Row & 0xFF); + + /// The inverse of . + public static RowId FromPacked(int packed) => new((int)((uint)packed >> 8), packed & 0xFF); +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/RowInserter.cs b/src/LibRed/LibRed.Core/Storage/RowInserter.cs index 117c81e3..5faab4eb 100644 --- a/src/LibRed/LibRed.Core/Storage/RowInserter.cs +++ b/src/LibRed/LibRed.Core/Storage/RowInserter.cs @@ -3,7 +3,6 @@ using LibRed.IO; using LibRed.Pages; using System.Buffers; -using System.Buffers.Binary; using System.Globalization; using System.Text; @@ -14,11 +13,11 @@ namespace LibRed.Storage; /// stores any long values, maintains every index B-tree, and enforces unique keys. The page's slot /// directory grows forward while row data is packed from the page end backward (see ). /// -public sealed class RowInserter(PageChannel channel, TableDef table) +internal sealed class RowInserter(PageChannel channel, TableDefinition table) { private readonly PageChannel _channel = channel; - private readonly UsageMapWriter _usageMaps = new(channel); - private readonly TableDef _table = table; + private readonly UsageMap _usageMaps = new(channel); + private readonly TableDefinition _table = table; private bool HasCalculatedColumns => _table.Columns.Any(c => c.IsCalculated); @@ -57,31 +56,15 @@ public void Insert(object?[] values, bool updateIndexes) // Encode first: the fixed-region length is pinned by any existing row (to match Access), // or derived from the columns for a just-created empty table. - var encoder = new RowEncoder(_table.Columns, format, InferFixedDataLength(format), + var encoder = new RowCodec(_table.Columns, format, InferFixedDataLength(format), _table.VariableColumnCount, SpillCalculated, _table.ColumnIdHighWater); byte[] record = encoder.Encode(storage, null, calculated ? values : null); EnsureRecordFits(format, record); - // Then find an owned page with room for the record plus its 2-byte slot entry. - (int pageNumber, byte[] page) = FindPageWithRoom(format, record.Length + 2); - - int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2)); - int freeSpace = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2)); - - // Rows are packed from the page end backward, with strictly decreasing slot offsets, - // so the new row goes just below the current lowest row start. - int lowestOffset = LowestRowOffset(page, format, rowCount); - int newOffset = lowestOffset - record.Length; - record.CopyTo(page.AsSpan(newOffset)); - - // Append the slot, bump the row count, shrink free space by row + slot-entry bytes. - // The new row's slot index is the old row count, giving its row id on this page. - BinaryPrimitives.WriteUInt16LittleEndian( - page.AsSpan(format.DataRowDirectoryOffset + rowCount * 2, 2), (ushort)(newOffset & RowPointer.OffsetMask)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2), (ushort)(rowCount + 1)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), (ushort)(freeSpace - record.Length - 2)); - + // Then find an owned page with room for the record plus its slot entry. + (int pageNumber, byte[] page) = FindPageWithRoom(format, record.Length + format.DataRowDirectoryEntrySize); + int rowCount = AppendRow(format, pageNumber, page, record, RowSlotFlags.None); _channel.WritePage(pageNumber, page); // Asked before the row's own entries go in, so an index "already has" a key only through another row. @@ -126,6 +109,8 @@ private void UpdateCore(RowId id, object?[] values, IReadOnlySet changedCol { JetFormatBase format = _channel.Format; RejectExplicitCalculatedValues(values, changedColumns); + EnforceNonNullKeys(values, changedColumns); + EnforceUniqueIndexes(values, id.Packed, changedColumns); // Two arrays, as on the insert path: `values` stays the logical row the caller gave, and `storage` is // where the long values become descriptors. The caller keeps the values it passed — an index entry @@ -137,16 +122,16 @@ private void UpdateCore(RowId id, object?[] values, IReadOnlySet changedCol // after the new values are written, as ACE does: the UPDATE that frees them never reuses them. byte[] oldRow = ReadRowBytes(id); // One layout for both: the trailer arithmetic is the same for either selection off this row. - RowLayout oldLayout = RowDecoder.ParseLayout(_table.Columns, format, oldRow); - var oldDescriptors = RowDecoder.LongValueDescriptors(_table.Columns, oldLayout, oldRow); - var oldCalculated = RowDecoder.CalculatedSlots(_table.Columns, oldLayout, oldRow); + RowCodec.Layout oldLayout = RowCodec.ParseLayout(_table.Columns, format, oldRow); + var oldDescriptors = RowCodec.LongValueDescriptors(_table.Columns, oldLayout, oldRow); + var oldCalculated = RowCodec.CalculatedSlots(_table.Columns, oldLayout, oldRow); var replaced = new List<(ColumnDef Column, byte[] Descriptor)>(); foreach (ColumnDef column in _table.Columns) { if (column.Type is not (JetDataType.Memo or JetDataType.Ole)) continue; if (!oldDescriptors.TryGetValue(column.Index, out byte[]? oldDescriptor)) continue; // old value was null if (changedColumns.Contains(column.Index)) replaced.Add((column, oldDescriptor)); - else storage[column.Index] = new LongValueDescriptor(oldDescriptor); + else storage[column.Index] = new LongValueStore.DescriptorValue(oldDescriptor); } // A calculated column is recomputed only when the UPDATE writes a column its expression READS — @@ -172,7 +157,7 @@ private void UpdateCore(RowId id, object?[] values, IReadOnlySet changedCol if (!column.IsCalculated || !column.HasLongValueMap) continue; if (preservedCalculated?.ContainsKey(column.Index) == true) continue; if (oldCalculated.TryGetValue(column.Index, out byte[]? stale) - && stale.Length >= LongValueFormat.DescriptorSize) + && stale.Length >= format.LongValueDescriptorSize) FreeLongValue(column, stale, releaseAtClose: false); } @@ -184,7 +169,7 @@ private void UpdateCore(RowId id, object?[] values, IReadOnlySet changedCol // Use the same guarded inference as Insert (Math.Max with the column-derived length): the raw per-row // parse returns a negative length for an all-fixed-column table (no variable columns — e.g. Northwind // Order Details), which without the guard would overflow `new byte[len]`. - var encoder = new RowEncoder(_table.Columns, format, InferFixedDataLength(format), + var encoder = new RowCodec(_table.Columns, format, InferFixedDataLength(format), _table.VariableColumnCount, SpillCalculated, _table.ColumnIdHighWater); byte[] record = encoder.Encode(storage, preservedCalculated, HasCalculatedColumns ? values : null); @@ -194,14 +179,13 @@ private void UpdateCore(RowId id, object?[] values, IReadOnlySet changedCol // unreadable row an INSERT was stopped from producing. EnsureRecordFits(format, record); - int raw = BinaryPrimitives.ReadUInt16LittleEndian(srcPage.AsSpan(format.DataRowDirectoryOffset + id.Row * 2, 2)); - if ((raw & RowPointer.OverflowFlag) != 0) + if (DataPage.ReadSlot(srcPage, format, id.Row).Flags.HasFlag(RowSlotFlags.Overflow)) { - // This slot is a 4-byte forward pointer to the real (relocated) row; rewrite it on its target page + // This slot is a forward record pointer to the real (relocated) row; rewrite it on its target page // (which keeps its hidden "deleted" flag). DataPage.TryReadRow(new PageBuffer(srcPage, id.Page), format, id.Row, - out RowSlot sourceSlot, out ReadOnlySpan sourceBytes); - RelocatedRow target = RowRelocationReader.Resolve( + out DataPage.RowSlot sourceSlot, out ReadOnlySpan sourceBytes); + DataPage.RelocatedRow target = DataPage.ResolveRelocation( _channel, _table.DefinitionPage, sourceSlot, sourceBytes); if (TryRewriteRowInPlace(target.Buffer.PageNumber, target.RowNumber, record)) return; @@ -209,7 +193,7 @@ private void UpdateCore(RowId id, object?[] values, IReadOnlySet changedCol // reclaimed first — through the same path a delete uses, and while the source slot still points at // it — and the pointer is then re-aimed, exactly as the first move wrote it. Nothing else changes: // the row keeps its id, so every index entry still names it. - ReclaimRelocationTarget(format, srcPage, id.Row); + ReclaimRelocationTarget(format, target); Relocate(format, id, record); return; } @@ -224,20 +208,20 @@ private void UpdateCore(RowId id, object?[] values, IReadOnlySet changedCol } /// Writes as a hidden row on a page with room and turns the slot at - /// into the 4-byte forward pointer that names it. Four bytes always fit the slot the + /// into the forward record pointer that names it. A pointer always fits the slot the /// row is vacating, so this cannot fail for want of space. private void Relocate(JetFormatBase format, RowId id, byte[] record) { (int targetPage, int targetRow) = WriteHiddenRow(format, record); - var pointerBytes = new byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(pointerBytes, (targetPage << 8) | targetRow); - TryRewriteRowInPlace(id.Page, id.Row, pointerBytes, addFlags: RowPointer.OverflowFlag); + var pointerBytes = new byte[PageBuffer.RecordPointerSize]; + PageBuffer.WriteRecordPointer(pointerBytes, 0, targetRow, targetPage); + TryRewriteRowInPlace(id.Page, id.Row, pointerBytes, addFlags: RowSlotFlags.Overflow); } /// Rewrites the row at (page, slot) in place, repacking the page from the end in slot order so /// every row id is preserved and each slot keeps its flags (plus on the /// target). Returns false without writing if the row no longer fits the page. - private bool TryRewriteRowInPlace(int pageNumber, int slot, byte[] record, int addFlags = 0) + private bool TryRewriteRowInPlace(int pageNumber, int slot, byte[] record, RowSlotFlags addFlags = RowSlotFlags.None) { JetFormatBase format = _channel.Format; // Pool the transient working page: read into a rented buffer, repack in place, write back, return it — @@ -246,17 +230,15 @@ private bool TryRewriteRowInPlace(int pageNumber, int slot, byte[] record, int a try { _channel.ReadPage(pageNumber, page); - int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2)); + int rowCount = DataPage.ReadRowCount(page, format); var rows = new byte[rowCount][]; - var rawDir = new int[rowCount]; - int directoryEnd = format.DataRowDirectoryOffset + rowCount * 2; + var flags = new RowSlotFlags[rowCount]; + int directoryEnd = DataPage.DirectoryEnd(format, rowCount); int prevEnd = format.PageSize; for (int i = 0; i < rowCount; i++) { - int raw = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + i * 2, 2)); - rawDir[i] = raw; - int offset = raw & RowPointer.OffsetMask; + (int offset, flags[i]) = DataPage.ReadSlot(page, format, i); // Same invariant DataPage enforces on the read side — this repacker rewrites the page, so a // bad directory entry must be reported before the first CopyTo rather than mid-repack. DataPage.ValidateSlot(pageNumber, format.PageSize, i, offset, prevEnd, directoryEnd); @@ -264,21 +246,9 @@ private bool TryRewriteRowInPlace(int pageNumber, int slot, byte[] record, int a prevEnd = offset; } rows[slot] = record; - rawDir[slot] |= addFlags; + flags[slot] |= addFlags; - int total = rows.Sum(r => r.Length); - if (total > format.PageSize - format.DataRowDirectoryOffset - rowCount * 2) return false; - - int off = format.PageSize; - for (int i = 0; i < rowCount; i++) - { - off -= rows[i].Length; - rows[i].CopyTo(page.AsSpan(off)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + i * 2, 2), - (ushort)((rawDir[i] & ~RowPointer.OffsetMask) | (off & RowPointer.OffsetMask))); - } - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), - (ushort)(off - format.DataRowDirectoryOffset - rowCount * 2)); + if (!DataPage.LayRows(page.AsSpan(0, format.PageSize), format, rows, flags)) return false; _channel.WritePage(pageNumber, page.AsSpan(0, format.PageSize)); return true; } @@ -289,20 +259,20 @@ private bool TryRewriteRowInPlace(int pageNumber, int slot, byte[] record, int a /// "deleted" so scans skip it there — it's only reached via the forward pointer). Returns its location. private (int Page, int Row) WriteHiddenRow(JetFormatBase format, byte[] record) { - (int pageNumber, byte[] page) = FindPageWithRoom(format, record.Length + 2); - int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2)); - int freeSpace = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2)); - int newOffset = LowestRowOffset(page, format, rowCount) - record.Length; - record.CopyTo(page.AsSpan(newOffset)); - - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + rowCount * 2, 2), - (ushort)((newOffset & RowPointer.OffsetMask) | RowPointer.DeletedFlag)); // hidden target - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2), (ushort)(rowCount + 1)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), (ushort)(freeSpace - record.Length - 2)); + (int pageNumber, byte[] page) = FindPageWithRoom(format, record.Length + format.DataRowDirectoryEntrySize); + int rowCount = AppendRow(format, pageNumber, page, record, RowSlotFlags.Deleted); // hidden target _channel.WritePage(pageNumber, page); return (pageNumber, rowCount); } + /// Appends to a page chose, returning its row. + /// That page declared the room, so a page whose rows leave none is corrupt. + private static int AppendRow(JetFormatBase format, int pageNumber, byte[] page, byte[] record, RowSlotFlags flags) => + DataPage.TryAppendRow(page, format, record, flags, out int row) + ? row + : throw new InvalidDataException( + $"Data page {pageNumber} declares room for a {record.Length}-byte row, but its rows leave none."); + /// /// Deletes the row at and reclaims its space, then decrements the TDEF row count /// (0x10). The caller removes the row's index entries first. A relocated row's hidden target is reclaimed @@ -316,7 +286,7 @@ public void Delete(RowId id) // Free the deleted row's chained long-value pages — held until close, as ACE holds them. byte[] rowBytes = ReadRowBytes(id); - var oldDescriptors = RowDecoder.LongValueDescriptors(_table.Columns, format, rowBytes); + var oldDescriptors = RowCodec.LongValueDescriptors(_table.Columns, format, rowBytes); // Which real indexes this row was the LAST holder of a key for. Asked before the row goes, and with // the row itself excluded, so the answer is "does another row still carry this key" either way — @@ -331,8 +301,7 @@ public void Delete(RowId id) try { _channel.ReadPage(id.Page, page); - int dir = format.DataRowDirectoryOffset + id.Row * 2; - ushort entry = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(dir, 2)); + RowSlotFlags entry = DataPage.ReadSlot(page, format, id.Row).Flags; // A LIVE slot carrying the overflow flag is a 4-byte forward pointer to a relocated row rather // than the row itself, so the row it forwards to has to go first — otherwise deleting a relocated @@ -343,10 +312,15 @@ public void Delete(RowId id) // Deleted must be excluded, not just overflow: ReclaimRow's own tombstone is deleted+overflow // (0xC000) and zero-length, so testing the overflow bit alone treats a tombstone as a relocation // source and reads four bytes of the NEIGHBOURING row as a page/row pointer. - if ((entry & (RowPointer.DeletedFlag | RowPointer.OverflowFlag)) == RowPointer.OverflowFlag) - ReclaimRelocationTarget(format, page, id.Row); + if ((entry & (RowSlotFlags.Deleted | RowSlotFlags.Overflow)) == RowSlotFlags.Overflow) + { + DataPage.TryReadRow(new PageBuffer(page.AsMemory(0, format.PageSize), id.Page), format, id.Row, + out DataPage.RowSlot sourceSlot, out ReadOnlySpan sourceBytes); + ReclaimRelocationTarget(format, DataPage.ResolveRelocation( + _channel, _table.DefinitionPage, sourceSlot, sourceBytes)); + } - ReclaimRow(format, page, id.Row); + DataPage.ReclaimRow(page.AsSpan(0, format.PageSize), format, id.Row); released = WriteReclaimedPage(format, page, id.Page); } finally { ArrayPool.Shared.Return(page); } @@ -362,8 +336,7 @@ public void Delete(RowId id) try { _channel.ReadPage(_table.DefinitionPage, tdef); - int rowCount = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(format.TdefRowCountOffset, 4)); - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(format.TdefRowCountOffset, 4), rowCount - 1); + TableDefinition.WriteRowCount(tdef, format, TableDefinition.ReadRowCount(tdef, format) - 1); // ACE's index entry counts on delete, measured against a DAO delete of the same row // (ComplexDeleteByteParityProbeTest): the TOTAL count (+0) drops by one on every real index, and @@ -378,14 +351,10 @@ public void Delete(RowId id) // ACE is not maintaining, and it stops touching the pair. Without this the totals go negative. foreach (IndexDef index in _table.Indexes.GroupBy(i => i.RealIndexOrdinal).Select(g => g.First())) { - int at = format.TdefRealIndexBlockOffset + index.RealIndexOrdinal * format.RealIndexEntrySize; - int total = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(at, 4)); + (int total, int unique) = TableDefinition.ReadIndexCounts(tdef, format, index.RealIndexOrdinal); if (total == 0) continue; - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(at, 4), total - 1); - - if (!lastKeyLost.Contains(index.RealIndexOrdinal)) continue; - int unique = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(at + 4, 4)); - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(at + 4, 4), unique - 1); + TableDefinition.WriteIndexCounts(tdef, format, index.RealIndexOrdinal, total - 1, + lastKeyLost.Contains(index.RealIndexOrdinal) ? unique - 1 : unique); } _channel.WritePage(_table.DefinitionPage, tdef.AsSpan(0, format.PageSize)); @@ -404,12 +373,9 @@ public void CountKeyMoved(IndexDef index) try { _channel.ReadPage(_table.DefinitionPage, tdef); - int at = format.TdefRealIndexBlockOffset + index.RealIndexOrdinal * format.RealIndexEntrySize; - int total = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(at, 4)); + (int total, int unique) = TableDefinition.ReadIndexCounts(tdef, format, index.RealIndexOrdinal); if (total == 0) return; - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(at, 4), total - 1); - int unique = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(at + 4, 4)); - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(at + 4, 4), Math.Min(unique, total - 1)); + TableDefinition.WriteIndexCounts(tdef, format, index.RealIndexOrdinal, total - 1, Math.Min(unique, total - 1)); _channel.WritePage(_table.DefinitionPage, tdef.AsSpan(0, format.PageSize)); } finally { ArrayPool.Shared.Return(tdef); } @@ -429,71 +395,40 @@ private HashSet IndexesLosingTheirLastKey(JetFormatBase format, byte[] rowB // indexable, and decoded without the reader it comes back as its 12-byte on-disk descriptor, which the // encoder's text path casts to string and dies on. A decode that feeds a key needs the value the // column holds, not the pointer to it. - object?[] values = new RowDecoder(_table.Columns, format, new LongValueReader(_channel)).Decode(rowBytes); - var writer = new IndexWriter(_channel, _table); + object?[] values = new RowCodec(_table.Columns, format, longValues: new LongValueStore(_channel)).Decode(rowBytes); + var writer = new IndexTree(_channel, _table); foreach (IndexDef index in _table.Indexes.GroupBy(i => i.RealIndexOrdinal).Select(g => g.First())) { if (index.RootPage <= 0) continue; if (index.IgnoreNulls && HasNullKey(index, values)) continue; - if (!writer.KeyExists(index, values, (id.Page << 8) | id.Row)) + if (!writer.KeyExists(index, values, id.Packed)) lost.Add(index.RealIndexOrdinal); } return lost; } - /// Reclaims the row a relocation pointer forwards to, on whatever page it lives. The pointer - /// is the first four bytes of the source row: page = pointer >> 8, row = pointer & 0xFF. - /// The target is flagged deleted so scans skip it, which is why it is read straight off the directory - /// rather than through — but it carries that reader's checks, because - /// this one WRITES: an unvalidated pointer runs ReclaimRow over a live row of an unrelated page, sliding - /// its neighbours and stamping a tombstone. + /// Reclaims the row a relocation pointer forwards to, on whatever page it lives. The caller resolves + /// through , whose checks matter more here than on a + /// read, because this WRITES: an unvalidated pointer runs ReclaimRow over a live row of an unrelated page, + /// sliding its neighbours and stamping a tombstone. /// A failed check is corruption, and says so. It used to return quietly so that a delete /// still removed the row the caller asked about — but the caller only reaches here because the slot is a /// live relocation pointer, so a pointer that does not resolve means the file already disagrees with /// itself, and carrying on strands the moved row silently. That is also how a bug in this engine's own /// relocation writer would look: nothing raised, a page quietly leaked. - /// The forward pointer does not resolve to a hidden row of this - /// table. - private void ReclaimRelocationTarget(JetFormatBase format, byte[] sourcePage, int row) + private void ReclaimRelocationTarget(JetFormatBase format, DataPage.RelocatedRow target) { - int offset = BinaryPrimitives.ReadUInt16LittleEndian( - sourcePage.AsSpan(format.DataRowDirectoryOffset + row * 2, 2)) & RowPointer.OffsetMask; - int end = row == 0 - ? format.PageSize - : BinaryPrimitives.ReadUInt16LittleEndian( - sourcePage.AsSpan(format.DataRowDirectoryOffset + (row - 1) * 2, 2)) & RowPointer.OffsetMask; - if (offset < format.DataRowDirectoryOffset || end > format.PageSize || end - offset < 4) - throw Unresolvable(row, $"its slot spans [{offset}, {end}), which cannot hold a 4-byte pointer"); - - int pointer = BinaryPrimitives.ReadInt32LittleEndian(sourcePage.AsSpan(offset, 4)); - int targetPage = pointer >> 8, targetRow = pointer & 0xFF; - if (targetPage <= 0 || targetPage >= _channel.PageCount) - throw Unresolvable(row, $"it names page {targetPage}, which is outside the file"); - byte[] page = ArrayPool.Shared.Rent(format.PageSize); try { - _channel.ReadPage(targetPage, page); - uint owner = BinaryPrimitives.ReadUInt32LittleEndian(page.AsSpan(format.DataOwnerOffset, 4)); - if (owner != (uint)_table.DefinitionPage) - throw Unresolvable(row, $"page {targetPage} is owned by {owner}, not '{_table.Name}'"); - int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2)); - if (targetRow >= rowCount) - throw Unresolvable(row, $"page {targetPage} has {rowCount} slots, so row {targetRow} is not one"); - - // The target is a hidden inline row: deleted, not itself a relocation source, and nonempty. - ushort slot = BinaryPrimitives.ReadUInt16LittleEndian( - page.AsSpan(format.DataRowDirectoryOffset + targetRow * 2, 2)); - if ((slot & (RowPointer.DeletedFlag | RowPointer.OverflowFlag)) != RowPointer.DeletedFlag) - throw Unresolvable(row, $"page {targetPage} row {targetRow} is not a hidden relocated row"); - - ReclaimRow(format, page, targetRow); + target.Buffer.Span.CopyTo(page); + DataPage.ReclaimRow(page.AsSpan(0, format.PageSize), format, target.RowNumber); // Written back, never released, even when that was the page's last row — ACE keeps a page emptied // this way as an ordinary data page (measured: deleting a relocated row leaves the page it had // moved to at type 0x01 with a bare 4080 free and its one slot tombstoned, still in the table's // maps). The release belongs to deleting a LIVE row (see WriteReclaimedPage); the row reclaimed // here was already hidden, so emptying its page is a space operation rather than a deletion. - _channel.WritePage(targetPage, page.AsSpan(0, format.PageSize)); + _channel.WritePage(target.Buffer.PageNumber, page.AsSpan(0, format.PageSize)); } finally { ArrayPool.Shared.Return(page); } } @@ -523,13 +458,13 @@ private void ReclaimRelocationTarget(JetFormatBase format, byte[] sourcePage, in private bool WriteReclaimedPage(JetFormatBase format, byte[] page, int pageNumber) { bool release = IsEmptied(format, page) && !IsFirstDataPage(pageNumber); - if (release) PageHeader.WriteType(page, PageType.ReleasedDataPage); + if (release) DataPage.MarkReleased(page); _channel.WritePage(pageNumber, page.AsSpan(0, format.PageSize)); if (!release) return false; UpdateUsageBit(format.TdefOwnedPagesOffset, pageNumber, set: false); UpdateUsageBit(format.TdefFreePagesOffset, pageNumber, set: false); - new PageAllocator(_channel).Release(pageNumber); + _channel.Allocator.Release(pageNumber); return true; } @@ -549,70 +484,17 @@ private bool IsFirstDataPage(int pageNumber) => /// (the page size standing in for the first). private static bool IsEmptied(JetFormatBase format, byte[] page) { - int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2)); + int rowCount = DataPage.ReadRowCount(page, format); int previous = format.PageSize; for (int i = 0; i < rowCount; i++) { - ushort entry = BinaryPrimitives.ReadUInt16LittleEndian( - page.AsSpan(format.DataRowDirectoryOffset + i * 2, 2)); - int offset = entry & RowPointer.OffsetMask; - if ((entry & RowPointer.DeletedFlag) == 0 || offset != previous) return false; + (int offset, RowSlotFlags flags) = DataPage.ReadSlot(page, format, i); + if (!flags.HasFlag(RowSlotFlags.Deleted) || offset != previous) return false; previous = offset; } return true; } - private InvalidDataException Unresolvable(int row, string why) => - new($"The relocation pointer in '{_table.Name}' row {row} does not resolve: {why}."); - - /// - /// Takes a row's bytes off its page the way ACE does: the rows stored below it slide up to close the - /// gap, their slot offsets follow, and the emptied slot becomes a zero-length tombstone whose offset is - /// the row's FORMER END, flagged deleted + overflow. The freed bytes go back to the page's free-space - /// count, so a delete-heavy table stops growing where ACE's would not. - /// - /// Slot indices never move, which is what keeps index entries and row ids valid — only offsets - /// change. Pointing the tombstone at the former end rather than at the page end is what keeps the - /// directory non-increasing, which relies on to derive each row's length - /// from the previous slot. Verified against ACE for a first, middle and last row: deleting the first of - /// three 19-byte rows gives D000 0FED 0FDA, the middle 0FED CFED 0FDA, the last - /// 0FED 0FDA CFDA, with free space rising by 19 in each case. - /// - /// - internal static void ReclaimRow(JetFormatBase format, byte[] page, int row) - { - int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2)); - int Offset(int i) => BinaryPrimitives.ReadUInt16LittleEndian( - page.AsSpan(format.DataRowDirectoryOffset + i * 2, 2)) & RowPointer.OffsetMask; - - // Slot offsets are non-increasing with slot index, so row i occupies [offset(i), offset(i-1)) and - // every row stored below this one is simply a LATER slot. Working by slot index rather than by - // comparing offsets is what keeps zero-length tombstones correct: one sitting at exactly this row's - // offset has to move up with the rows after it, and an offset comparison leaves it behind — where it - // then absorbs this row's length and starves the next live row down to zero. - int start = Offset(row); - int end = row == 0 ? format.PageSize : Offset(row - 1); - int length = end - start; - - int lowest = rowCount > 0 ? Offset(rowCount - 1) : format.PageSize; - if (length > 0 && start > lowest) - page.AsSpan(lowest, start - lowest).CopyTo(page.AsSpan(lowest + length)); - - for (int i = row + 1; i < rowCount; i++) - { - int at = format.DataRowDirectoryOffset + i * 2; - ushort entry = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(at, 2)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(at, 2), - (ushort)((entry & ~RowPointer.OffsetMask) | ((entry & RowPointer.OffsetMask) + length))); - } - - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + row * 2, 2), - (ushort)(end | RowPointer.DeletedFlag | RowPointer.OverflowFlag)); - - int free = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), (ushort)(free + length)); - } - /// The full inline bytes of the row at (following an overflow pointer). public byte[] ReadRow(RowId id) => ReadRowBytes(id); @@ -626,22 +508,21 @@ public void RewriteRowRaw(RowId id, byte[] record) // The third entry point that writes a row, and the one most likely to cross the cap: its caller is // the in-place ALTER re-lay, which by design makes rows LONGER — the retyped column is appended and // its old slot left as dead space. The record arrives pre-built (BuildRelaidRecord → AssembleRow), - // so it never passes through RowEncoder.Encode or either guarded entry point. + // so it never passes through RowCodec.Encode or either guarded entry point. EnsureRecordFits(format, record); byte[] srcPage = _channel.ReadPageShared(id.Page).Span.ToArray(); - int raw = BinaryPrimitives.ReadUInt16LittleEndian(srcPage.AsSpan(format.DataRowDirectoryOffset + id.Row * 2, 2)); - if ((raw & RowPointer.OverflowFlag) != 0) + if (DataPage.ReadSlot(srcPage, format, id.Row).Flags.HasFlag(RowSlotFlags.Overflow)) { DataPage.TryReadRow(new PageBuffer(srcPage, id.Page), format, id.Row, - out RowSlot sourceSlot, out ReadOnlySpan sourceBytes); - RelocatedRow target = RowRelocationReader.Resolve( + out DataPage.RowSlot sourceSlot, out ReadOnlySpan sourceBytes); + DataPage.RelocatedRow target = DataPage.ResolveRelocation( _channel, _table.DefinitionPage, sourceSlot, sourceBytes); if (TryRewriteRowInPlace(target.Buffer.PageNumber, target.RowNumber, record)) return; // Outgrown its new page as well, so it moves again — the old hidden row reclaimed while the // pointer still names it, then the pointer re-aimed. See Update, which does the same. - ReclaimRelocationTarget(format, srcPage, id.Row); + ReclaimRelocationTarget(format, target); Relocate(format, id, record); return; } @@ -655,11 +536,11 @@ private byte[] ReadRowBytes(RowId id) { JetFormatBase format = _channel.Format; PageBuffer page = _channel.ReadPageShared(id.Page); - if (!DataPage.TryReadRow(page, format, id.Row, out RowSlot slot, out ReadOnlySpan bytes)) + if (!DataPage.TryReadRow(page, format, id.Row, out DataPage.RowSlot slot, out ReadOnlySpan bytes)) throw new InvalidDataException($"Row {id.Page}:{id.Row} does not exist."); if (!slot.HasOverflow) return bytes.ToArray(); - return RowRelocationReader.Resolve(_channel, _table.DefinitionPage, slot, bytes).Bytes.ToArray(); + return DataPage.ResolveRelocation(_channel, _table.DefinitionPage, slot, bytes).Bytes.ToArray(); } /// @@ -675,38 +556,30 @@ private byte[] ReadRowBytes(RowId id) /// private void FreeLongValue(ColumnDef column, byte[] descriptor, bool releaseAtClose) { - // The descriptor comes off the row's variable chunk, so its width is whatever the offset table said. - // LongValueReader requires the full 12 bytes before reading any field; reclaiming has to agree, or a - // short chunk indexes past the end and escapes Delete/Update as IndexOutOfRangeException. - if (descriptor.Length < LongValueFormat.DescriptorSize) - throw new InvalidDataException( - $"Long-value descriptor has {descriptor.Length} bytes; expected at least {LongValueFormat.DescriptorSize}."); - - // Byte 3 carries the length's top byte AND the flags, so it must be masked before comparison — the - // length runs to 0x3FFFFFFF, and a 16 MB value puts 0x01 there. Masked exactly as LongValueReader does. - byte flags = (byte)(descriptor[3] & LongValueFormat.FlagMask); - if (flags is not (LongValueFormat.FlagInline or LongValueFormat.FlagSinglePage or LongValueFormat.FlagChained)) - throw new InvalidDataException($"Long-value descriptor has unknown flags 0x{flags:X2}."); - // Inline (0x80) keeps its payload in the row, so there is nothing to give back. - if (flags == LongValueFormat.FlagInline) return; - - TableDefinitionPage definition = LongValueMaps; + // The descriptor comes off the row's variable chunk, so its width is whatever the offset table said, and + // it is read — and checked — exactly as LongValueStore reads it: a short chunk would otherwise index past + // the end and escape Delete/Update as IndexOutOfRangeException. + (_, LongValueStore.StorageKind storage, int valueRow, int valuePage, _) = LongValueStore.Read(descriptor, _channel.Format); + // An inline value keeps its payload in the row, so there is nothing to give back. + if (storage == LongValueStore.StorageKind.Inline) return; + + TableDefinition definition = LongValueMaps; definition.LongValueOwnedMaps.TryGetValue(column.ColumnId, out (int Row, int Page) owned); definition.LongValueFreeMaps.TryGetValue(column.ColumnId, out (int Row, int Page) free); - // A single-page (0x40) value shares its page with other values, so the page goes back only once the - // last of them is gone — until then just its own row is retired. - if (flags == LongValueFormat.FlagSinglePage) + // A single-page value shares its page with other values, so the page goes back only once the last of + // them is gone — until then just its own row is retired. + if (storage == LongValueStore.StorageKind.SinglePage) { - ReleasePackedValue(column, descriptor[5] | (descriptor[6] << 8) | (descriptor[7] << 16), - descriptor[4], owned, free, releaseAtClose); + ReleasePackedValue(column, valuePage, valueRow, owned, free, releaseAtClose); return; } - var allocator = new PageAllocator(_channel); - var reader = new LongValueReader(_channel); + var allocator = _channel.Allocator; + var reader = new LongValueStore(_channel); _ = reader.ResolveWithPages(descriptor, out IReadOnlyList pages); - HashSet ownedPages = MapPages(owned.Row, owned.Page).ToHashSet(); - if (free.Page != 0) _ = MapPages(free.Row, free.Page); // validate both mutation targets before the first free + var maps = new UsageMap(_channel, _table); + HashSet ownedPages = maps.PagesInMap(owned.Row, owned.Page).ToHashSet(); + if (free.Page != 0) _ = maps.PagesInMap(free.Row, free.Page); // validate both mutation targets before the first free foreach (int page in pages) if (!ownedPages.Contains(page)) throw new InvalidDataException( @@ -760,30 +633,20 @@ private void ReleasePackedValue(ColumnDef column, int pageNumber, int row, $"Long-value row pointer {pageNumber}:{row} is outside the page's 0..{holder.RowCount - 1} range."); if (holder.Rows[row].IsDeleted) return; // already retired; freeing twice must not double-count - int dir = format.DataRowDirectoryOffset; + // Every dead row — this one, and any already retired — becomes a 0-length deleted + overflow tombstone, as + // ACE writes it. var records = new byte[holder.RowCount][]; - var flags = new ushort[holder.RowCount]; + var flags = new RowSlotFlags[holder.RowCount]; for (int i = 0; i < holder.RowCount; i++) { bool dead = i == row || holder.Rows[i].IsDeleted; records[i] = dead ? [] : page.AsSpan(holder.Rows[i].Offset, holder.Rows[i].Length).ToArray(); - flags[i] = (ushort)((dead ? RowPointer.DeletedFlag : 0) - | (dead || holder.Rows[i].HasOverflow ? RowPointer.OverflowFlag : 0)); + flags[i] = dead ? RowSlotFlags.Deleted | RowSlotFlags.Overflow : DataPage.Flags(holder.Rows[i]); } - - int offset = format.PageSize; - for (int i = 0; i < holder.RowCount; i++) - { - offset -= records[i].Length; // a 0-length tombstone lands on the page end, as ACE writes it - records[i].CopyTo(page.AsSpan(offset)); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(dir + i * 2, 2), - (ushort)(flags[i] | (offset & RowPointer.OffsetMask))); - } - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), - (ushort)(offset - (dir + holder.RowCount * 2))); + DataPage.LayRows(page, format, records, flags); // shrinking, so it always fits bool emptied = records.All(r => r.Length == 0); - if (emptied) PageHeader.WriteType(page, PageType.ReleasedDataPage); + if (emptied) DataPage.MarkReleased(page); _channel.WritePage(pageNumber, page); // A page that survives has room again, so it goes back into the column's free-pages map — the same @@ -796,34 +659,38 @@ private void ReleasePackedValue(ColumnDef column, int pageNumber, int row, } _usageMaps.SetBit(owned.Row, owned.Page, pageNumber, set: false); if (free.Page != 0) _usageMaps.SetBit(free.Row, free.Page, pageNumber, set: false); - if (releaseAtClose) new PageAllocator(_channel).Release(pageNumber); - else new PageAllocator(_channel).Free(pageNumber); + if (releaseAtClose) _channel.Allocator.Release(pageNumber); + else _channel.Allocator.Free(pageNumber); } /// Rejects the insert if a primary-key or WITH DISALLOW NULL index would get a Null in any of its /// columns — ACE refuses it on every write, not only when the index is built (verified). - private void EnforceNonNullKeys(object?[] values) + private void EnforceNonNullKeys(object?[] values, IReadOnlySet? changedColumns = null) { foreach (IndexDef index in _table.Indexes.Where(i => (i.IsPrimaryKey || i.Required) && i.RootPage > 0)) + { + if (changedColumns is not null && !index.Columns.Any(c => changedColumns.Contains(c.Column.Index))) continue; if (index.Columns.FirstOrDefault(c => values[c.Column.Index] is null or DBNull).Column is { } nullColumn) throw new InvalidOperationException( $"Index or primary key cannot contain a Null value: column '{nullColumn.Name}' of " + $"'{_table.Name}' is Null, and index '{index.Name}' does not allow it."); + } } /// Rejects the insert if a UNIQUE or PRIMARY index would gain a duplicate key. A row with a /// null in any of a unique index's columns is skipped — Jet treats nulls as distinct, so a unique index /// allows multiple nulls (verified vs ACE). Runs before the row is written so nothing is half-inserted. - private void EnforceUniqueIndexes(object?[] values) + private void EnforceUniqueIndexes(object?[] values, int? excludePointer = null, IReadOnlySet? changedColumns = null) { - IndexWriter? writer = null; + IndexTree? writer = null; foreach (IndexDef index in _table.RealIndexes.Where(i => i.IsUnique)) { + if (changedColumns is not null && !index.Columns.Any(c => changedColumns.Contains(c.Column.Index))) continue; if (HasNullKey(index, values)) continue; // nulls are distinct — multiple allowed - writer ??= new IndexWriter(_channel, _table); - if (writer.KeyExists(index, values)) + writer ??= new IndexTree(_channel, _table); + if (writer.KeyExists(index, values, excludePointer)) throw new ConstraintViolationException( - $"Cannot insert into '{_table.Name}': a row with the same {(index.IsPrimaryKey ? "primary key" : "unique key")} " + + $"Cannot {(excludePointer is null ? "insert into" : "update")} '{_table.Name}': a row with the same {(index.IsPrimaryKey ? "primary key" : "unique key")} " + $"already exists (index '{index.Name}').", index.Name, index.IsPrimaryKey); @@ -834,7 +701,7 @@ private void EnforceUniqueIndexes(object?[] values) /// indexes share a real index's data) so indexed lookups — and Access — find it. private void UpdateIndexes(object?[] values, RowId rowId) { - var writer = new IndexWriter(_channel, _table); + var writer = new IndexTree(_channel, _table); foreach (IndexDef index in _table.RealIndexes) { // WITH IGNORE NULL: a row with a null in any indexed column is not added to this index. @@ -852,11 +719,11 @@ private void UpdateIndexes(object?[] values, RowId rowId) private HashSet IndexesGainingANewKey(object?[] values) { var gaining = new HashSet(); - IndexWriter? writer = null; + IndexTree? writer = null; foreach (IndexDef index in _table.RealIndexes.Where(i => !i.IsUnique)) { if (index.IgnoreNulls && HasNullKey(index, values)) continue; - writer ??= new IndexWriter(_channel, _table); + writer ??= new IndexTree(_channel, _table); if (!writer.KeyExists(index, values)) gaining.Add(index.RootPage); } return gaining; @@ -890,12 +757,12 @@ private static void EnsureRecordFits(JetFormatBase format, byte[] record) foreach (int pageNumber in new UsageMap(_channel, _table).FreeDataPages()) { byte[] page = _channel.ReadPageShared(pageNumber).Span.ToArray(); - int freeSpace = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2)); + int freeSpace = DataPage.ReadFreeSpace(page, format); // Space is not the only limit: an index addresses a row by a one-byte slot number, so a page that - // already holds RowPointer.MaxRowsPerPage rows has no addressable slot left however much room it - // has. Narrow rows hit this long before they fill the page. - int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2)); - if (freeSpace >= needed && rowCount < RowPointer.MaxRowsPerPage) + // already holds MaxRowsPerPage rows has no addressable slot left however much room it has. Narrow rows + // hit this long before they fill the page. + int rowCount = DataPage.ReadRowCount(page, format); + if (freeSpace >= needed && rowCount < format.MaxRowsPerPage) return (pageNumber, page); } @@ -917,14 +784,9 @@ private static void EnsureRecordFits(JetFormatBase format, byte[] record) // the last of six equally-full pages stays in the free map.) int previousTail = new UsageMap(_channel, _table).MaxDataPage(); - int pageNumber = new PageAllocator(_channel).Allocate(); + int pageNumber = _channel.Allocator.Allocate(); - var page = new byte[format.PageSize]; - PageHeader.WriteType(page, PageType.DataPage); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.DataOwnerOffset, 4), _table.DefinitionPage); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2), 0); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.DataFreeSpaceOffset, 2), - (ushort)(format.PageSize - format.DataRowDirectoryOffset)); + byte[] page = DataPage.NewPage(format, (uint)_table.DefinitionPage); _channel.WritePage(pageNumber, page); if (previousTail >= 0) @@ -939,8 +801,8 @@ private static void EnsureRecordFits(JetFormatBase format, byte[] record) /// referenced by the TDEF pointer at (row byte + 3-byte page). private void UpdateUsageBit(int tdefPointerOffset, int targetPage, bool set) { - PageBuffer tdef = _channel.ReadPageShared(_table.DefinitionPage); - _usageMaps.SetBit(tdef.ReadByte(tdefPointerOffset), tdef.ReadInt24(tdefPointerOffset + 1), targetPage, set, + (int row, int page) = _channel.ReadPageShared(_table.DefinitionPage).ReadRecordPointer(tdefPointerOffset); + _usageMaps.SetBit(row, page, targetPage, set, movableWindow: tdefPointerOffset == _channel.Format.TdefFreePagesOffset); } @@ -971,18 +833,7 @@ private void UpdateUsageBit(int tdefPointerOffset, int targetPage, bool set) if (InferFixedDataLength(page, format) is { } pinned) return Math.Max(pinned, derived); // ADD COLUMN of a fixed column grows the region past old rows } - return null; // empty table → RowEncoder derives the same length from the columns - } - - private static int LowestRowOffset(byte[] page, JetFormatBase format, int rowCount) - { - int lowest = format.PageSize; - for (int i = 0; i < rowCount; i++) - { - int raw = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + i * 2, 2)); - lowest = Math.Min(lowest, raw & RowPointer.OffsetMask); - } - return lowest; + return null; // empty table → RowCodec derives the same length from the columns } /// @@ -992,7 +843,7 @@ private static int LowestRowOffset(byte[] page, JetFormatBase format, int rowCou /// private int? InferFixedDataLength(byte[] page, JetFormatBase format) { - int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowCountOffset, 2)); + int rowCount = DataPage.ReadRowCount(page, format); // Take the largest sane fixed length across the page's inline rows rather than trusting the first one: // some rows decode to a nonsensical (e.g. zero) variable-data offset, which would yield a negative @@ -1001,16 +852,15 @@ private static int LowestRowOffset(byte[] page, JetFormatBase format, int rowCou int prevEnd = format.PageSize; for (int i = 0; i < rowCount; i++) { - int raw = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + i * 2, 2)); - int offset = raw & RowPointer.OffsetMask; + (int offset, RowSlotFlags flags) = DataPage.ReadSlot(page, format, i); int length = prevEnd - offset; prevEnd = offset; - if ((raw & (RowPointer.DeletedFlag | RowPointer.OverflowFlag)) != 0) continue; - if (length < format.RowColumnCountSize + 2) continue; + if ((flags & (RowSlotFlags.Deleted | RowSlotFlags.Overflow)) != RowSlotFlags.None) continue; + if (length < format.RowColumnCountSize) continue; ReadOnlySpan row = page.AsSpan(offset, length); - bool rowHasVar = RowLayout.HasVariableSection(row, _table.Columns); + bool rowHasVar = RowCodec.Layout.HasVariableSection(row, _table.Columns, format); // A row written while a since-dropped variable column existed still carries its trailer, but the // live columns no longer say so, and reading it as all-fixed measures the trailer as fixed data — @@ -1018,7 +868,10 @@ private static int LowestRowOffset(byte[] page, JetFormatBase format, int rowCou // 0x2B high-water is the tell: it outlives the drop, so a row that reads as all-fixed in a table // that has ever had a variable column cannot be trusted to pin the length. The schema does it. if (!rowHasVar && _table.VariableColumnCount > 0) continue; - RowLayout layout = RowLayout.Parse(row, format.RowColumnCountSize, rowHasVar); + // Too short for even an empty row of its own column count: not an inline record, so it cannot pin + // the length. + if (length < RowCodec.Layout.MinimumLength(format, RowCodec.Layout.ReadColumnCount(row, format), rowHasVar)) continue; + RowCodec.Layout layout = RowCodec.Layout.Parse(row, format, rowHasVar); int varDataStart = layout.FixedRegionLength + format.RowColumnCountSize; // The variable-data start is the end of the fixed region; it must lie after the column-count field @@ -1062,62 +915,64 @@ private void MaterializeLongValues(object?[] values) { // ACE writes a row's chained values first and its single-page values after, each in column order // (measured), so a single-page value in an earlier column takes the page after the chains, not before. - LongValueWriter? writer = null; + LongValueStore? writer = null; + int maxSinglePage = _channel.Format.LongValueMaxSinglePage; foreach (bool chained in new[] { true, false }) foreach (ColumnDef column in _table.Columns) if (column.Type is JetDataType.Memo or JetDataType.Ole && Chains(values[column.Index]) == chained) values[column.Index] = MaterializeLongValue(column, values[column.Index], ref writer); // The storage form follows the uncompressed length, as MaterializeLongValue decides it. - static bool Chains(object? value) => value switch + bool Chains(object? value) => value switch { - string s => Encoding.Unicode.GetByteCount(s) > LongValueFormat.MaxSinglePageValue, - byte[] b => b.Length > LongValueFormat.MaxSinglePageValue, + string s => Encoding.Unicode.GetByteCount(s) > maxSinglePage, + byte[] b => b.Length > maxSinglePage, _ => false, }; } /// The storage form of one Memo/OLE value: a value over 64 bytes is written to an LVAL page and - /// becomes its ; anything else — a short value, null, a descriptor already + /// becomes its ; anything else — a short value, null, a descriptor already /// built — comes back as it was, for the row codec to inline. For a caller re-laying a single column. internal object? MaterializeLongValue(ColumnDef column, object? value) { - LongValueWriter? writer = null; + LongValueStore? writer = null; return MaterializeLongValue(column, value, ref writer); } - private object? MaterializeLongValue(ColumnDef column, object? value, ref LongValueWriter? writer) + private object? MaterializeLongValue(ColumnDef column, object? value, ref LongValueStore? writer) { byte[]? payload = value switch { string s => Encoding.Unicode.GetBytes(s), // memo: UTF-16LE byte[] b => b, // OLE: raw bytes - _ => null, // null, or an already-built LongValueDescriptor + _ => null, // null, or an already-built LongValueStore.DescriptorValue }; - if (payload is null || payload.Length <= LongValueFormat.MaxInlineValue) return value; + JetFormatBase format = _channel.Format; + if (payload is null || payload.Length <= format.LongValueMaxInline) return value; // Compress before storing, but AFTER the inline test above and using the uncompressed length for // the single-page test below: ACE decides the storage form on the uncompressed size and applies // compression to whatever form results, never to a chained value (LongTextStorageAccessTests). - // ACE places a compressed value by its uncompressed bytes and leaves them behind (LongValueWriter.TryAppend). + // ACE places a compressed value by its uncompressed bytes and leaves them behind (LongValueStore.TryAppend). byte[]? uncompressed = null; - if (payload.Length <= LongValueFormat.MaxSinglePageValue + if (payload.Length <= format.LongValueMaxSinglePage && value is string text && Types.JetTypeCodec.TryCompressText(column, text) is { } compressed) (uncompressed, payload) = (payload, compressed); - writer ??= new LongValueWriter(_channel); - TableDefinitionPage definition = LongValueMaps; + writer ??= new LongValueStore(_channel); + TableDefinition definition = LongValueMaps; definition.LongValueOwnedMaps.TryGetValue(column.ColumnId, out (int Row, int Page) owned); definition.LongValueFreeMaps.TryGetValue(column.ColumnId, out (int Row, int Page) free); - return new LongValueDescriptor(StoreLongValue(writer, payload, owned, free, uncompressed)); + return new LongValueStore.DescriptorValue(StoreLongValue(writer, payload, owned, free, uncompressed)); } - // A page is dropped from the free-pages map once it cannot hold a 256-byte value and its 2-byte row-directory - // entry (verified vs ACE, memo and OLE alike: a page left with 257 bytes free leaves the map, one with 258 - // stays). Not the smallest long value a page could still take — anything over 64 bytes is one — but ACE's + // A page is dropped from the free-pages map once it cannot hold a 256-byte value and its row-directory entry + // (verified vs ACE, memo and OLE alike: a page left with 257 bytes free leaves the map, one with 258 stays). + // Not the smallest long value a page could still take — anything over the inline limit is one — but ACE's // own cut-off, below which it stops offering the page. - private const int MinLvalRow = 256 + 2; + private const int MinLvalValue = 256; /// Rejects a caller-supplied value for a calculated column, as ACE does — it refuses both an /// INSERT naming one and an UPDATE setting one, with "Cannot update 'x'; field not updateable." @@ -1142,24 +997,26 @@ private void RejectExplicitCalculatedValues(object?[] values, IReadOnlySet? } /// Writes an oversized calculated result to an LVAL page and returns its in-row descriptor. - /// Handed to , which derives the value and so is the only place that knows how + /// Handed to , which derives the value and so is the only place that knows how /// big it turned out; the pages and their usage maps stay this class's business. private byte[] SpillCalculated(ColumnDef column, byte[] envelope) - => StorePackedLongValue(column.ColumnId, envelope); + => StorePackedLongValue(column.ColumnId, envelope).Bytes; /// Stores for long-value column and returns /// the in-row descriptor: inline up to 64 bytes, as any long value is, else on an LVAL page — packing onto a /// free page as usual. The MSysObjects LvProp property blob takes the same rule (verified: ACE inlines - /// a 63-byte blob and puts a 65-byte one on a page). Call before so the - /// row carries the returned descriptor as a . - public byte[] StorePackedLongValue(int columnId, byte[] payload) + /// a 63-byte blob and puts a 65-byte one on a page). Call before and put + /// the descriptor in the row's slot for the column. + public LongValueStore.DescriptorValue StorePackedLongValue(int columnId, byte[] payload) { - if (payload.Length <= LongValueFormat.MaxInlineValue) return Types.JetTypeCodec.EncodeInlineLongValue(payload); + JetFormatBase format = _channel.Format; + if (payload.Length <= format.LongValueMaxInline) + return new LongValueStore.DescriptorValue(Types.JetTypeCodec.EncodeInlineLongValue(payload, format)); - TableDefinitionPage definition = ReadDefinition(); + TableDefinition definition = ReadDefinition(); definition.LongValueOwnedMaps.TryGetValue(columnId, out (int Row, int Page) owned); definition.LongValueFreeMaps.TryGetValue(columnId, out (int Row, int Page) free); - return StoreLongValue(new LongValueWriter(_channel), payload, owned, free); + return new LongValueStore.DescriptorValue(StoreLongValue(new LongValueStore(_channel), payload, owned, free)); } /// @@ -1173,17 +1030,19 @@ public byte[] StorePackedLongValue(int columnId, byte[] payload) /// and MSysAccessXML.LValue that way, and packs nothing into them — measured across every such file, /// one LVAL page per stored value. /// - private byte[] StoreLongValue(LongValueWriter writer, byte[] payload, (int Row, int Page) owned, (int Row, int Page) free, + private byte[] StoreLongValue(LongValueStore writer, byte[] payload, (int Row, int Page) owned, (int Row, int Page) free, byte[]? uncompressed = null) { if (owned.Page == 0) throw new InvalidDataException("Long-value column has no owned-pages usage-map pointer."); - _ = MapPages(owned.Row, owned.Page); // validate both map targets before allocating or writing LVAL pages - IReadOnlyList freePages = free.Page == 0 ? [] : MapPages(free.Row, free.Page); + var maps = new UsageMap(_channel, _table); + _ = maps.PagesInMap(owned.Row, owned.Page); // validate both map targets before allocating or writing LVAL pages + IReadOnlyList freePages = free.Page == 0 ? [] : [.. maps.PagesInMap(free.Row, free.Page)]; - if (payload.Length > LongValueFormat.MaxSinglePageValue) + JetFormatBase format = _channel.Format; + if (payload.Length > format.LongValueMaxSinglePage) { - LongValueResult chained = writer.Write(payload); + (byte[] Descriptor, IReadOnlyList OwnedPages, int FreePage) chained = writer.Write(payload); foreach (int page in chained.OwnedPages) _usageMaps.SetBit(owned.Row, owned.Page, page, set: true); if (chained.FreePage != 0 && free.Page != 0) _usageMaps.SetBit(free.Row, free.Page, chained.FreePage, set: true, movableWindow: true); @@ -1195,8 +1054,9 @@ private byte[] StoreLongValue(LongValueWriter writer, byte[] payload, (int Row, foreach (int page in freePages) if (writer.TryAppend(page, payload, uncompressed) is (int row, int remaining)) { - if (remaining < MinLvalRow) _usageMaps.SetBit(free.Row, free.Page, page, set: false); // now full - return LongValueWriter.SinglePageDescriptor(payload.Length, page, row); + if (remaining < MinLvalValue + format.DataRowDirectoryEntrySize) + _usageMaps.SetBit(free.Row, free.Page, page, set: false); // now full + return LongValueStore.Descriptor(format, payload.Length, LongValueStore.StorageKind.SinglePage, page, row); } // No free page had room: a fresh page (owned, and free — it still has spare room). @@ -1204,53 +1064,7 @@ private byte[] StoreLongValue(LongValueWriter writer, byte[] payload, (int Row, _usageMaps.SetBit(owned.Row, owned.Page, newPage, set: true); if (free.Page != 0) _usageMaps.SetBit(free.Row, free.Page, newPage, set: true, movableWindow: true); - return LongValueWriter.SinglePageDescriptor(payload.Length, newPage, 0); - } - - /// Reads every page marked in a validated inline or reference usage map. - private List MapPages(int mapRow, int mapPage) - { - if (mapPage <= 1 || mapPage >= _channel.PageCount) - throw new InvalidDataException($"Long-value usage-map page {mapPage} is outside the physical file."); - var holder = new DataPage(); - holder.Read(_channel.ReadPageShared(mapPage), _channel.Format); - if (holder.IsLongValuePage || holder.OwningTablePage != 0) - throw new InvalidDataException( - $"Long-value usage-map pointer {mapPage}:{mapRow} does not target an owner-zero usage-map data page."); - if (mapRow < 0 || mapRow >= holder.RowCount) - throw new InvalidDataException($"Long-value usage-map row {mapPage}:{mapRow} does not exist."); - RowSlot slot = holder.Rows[mapRow]; - if (slot.IsDeleted || slot.HasOverflow || slot.Length == 0) - throw new InvalidDataException($"Long-value usage-map row {mapPage}:{mapRow} is deleted, overflowed, or empty."); - ReadOnlySpan map = holder.GetRow(mapRow); - var result = new List(); - - if (map[0] == 0x00) - { - if (map.Length < 5) - throw new InvalidDataException("Inline long-value usage map is shorter than its 5-byte header."); - int startPage = BinaryPrimitives.ReadInt32LittleEndian(map.Slice(1, 4)); - UsageMapBits.Append(result, map[5..], startPage, _channel.PageCount, "Long-value usage map"); - return result; - } - - if (map[0] != 0x01 || map.Length != 69) - throw new InvalidDataException( - $"Long-value usage map has invalid type/length 0x{map[0]:X2}/{map.Length}."); - int pagesPerBitmap = (_channel.PageSize - 4) * 8; - var bitmapPages = new HashSet(); - for (int i = 0; i < 17; i++) - { - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(map.Slice(1 + i * 4, 4)); - if (bitmapPage == 0) continue; - if (bitmapPage <= 1 || bitmapPage >= _channel.PageCount || !bitmapPages.Add(bitmapPage)) - throw new InvalidDataException($"Long-value usage map has invalid bitmap-page pointer {bitmapPage}."); - ReadOnlySpan bitmap = _channel.ReadPageShared(bitmapPage).Span; - if (PageHeader.ReadType(bitmap) != PageType.PageUsageBitmap || bitmap[2] != 0 || bitmap[3] != 0) - throw new InvalidDataException($"Long-value usage-map pointer {bitmapPage} is not a bitmap page."); - UsageMapBits.Append(result, bitmap[4..], i * pagesPerBitmap, _channel.PageCount, "Long-value usage map"); - } - return result; + return LongValueStore.Descriptor(format, payload.Length, LongValueStore.StorageKind.SinglePage, newPage); } /// @@ -1259,18 +1073,18 @@ private List MapPages(int mapRow, int mapPage) /// /// /// Freeing or storing a long value writes the map holder page and the global map; - /// never writes the definition page, and an inline-to-reference conversion + /// never writes the definition page, and an inline-to-reference conversion /// rewrites the record inside the same map row. So the §3.3.2 pointers are fixed for the life of this /// inserter, where the row count and AutoNumber high-water on the same page are not — read those from the /// page, never from here. /// - private TableDefinitionPage LongValueMaps => _longValueMaps ??= ReadDefinition(); + private TableDefinition LongValueMaps => _longValueMaps ??= ReadDefinition(); - private TableDefinitionPage? _longValueMaps; + private TableDefinition? _longValueMaps; - private TableDefinitionPage ReadDefinition() + private TableDefinition ReadDefinition() { - var definition = new TableDefinitionPage(); + var definition = new TableDefinition(); definition.Read(_channel, _table.DefinitionPage); return definition; } @@ -1292,7 +1106,7 @@ private TableDefinitionPage ReadDefinition() if (!needed) return null; ReadOnlySpan tdef = _channel.ReadPageShared(_table.DefinitionPage).Span; - int highWater = BinaryPrimitives.ReadInt32LittleEndian(tdef.Slice(format.TdefLastAutoNumberOffset, 4)); + int highWater = TableDefinition.ReadLastAutoNumber(tdef, format); // A complex (multi-value / attachment) column is an AutoNumber too, but it draws on its own counter // at 0x1C and there is ONE id per row shared by every complex column of the table — the counter is a @@ -1337,8 +1151,7 @@ private void AssignComplexId(JetFormatBase format, object?[] values, ReadOnlySpa .ToList(); if (complex.Count == 0) return; - int next = unchecked(BinaryPrimitives.ReadInt32LittleEndian( - tdef.Slice(format.TdefComplexAutoNumberOffset, 4)) + 1); + int next = unchecked(TableDefinition.ReadComplexAutoNumber(tdef, format) + 1); foreach (ColumnDef column in complex) values[column.Index] = next; } @@ -1365,7 +1178,7 @@ private static int RandomAutoNumber() /// (the row brings a key it does not hold yet). Access advances it on insert only, /// once per real index, and never decrements it. The sibling **total-entry count** /// (`+0`) is deliberately left untouched **on insert**: Access does not raise it there — it is written - /// when the index is built (to its entries, in TableCreator's back-fill) or the file compacted + /// when the index is built (to its entries, in SchemaEditor's back-fill) or the file compacted /// (verified: an index created on an empty table reads total `0` through any number of inserts). /// Delete is not the mirror of this — ACE drops the total by one on every real index when a row /// goes, so the figure drifts downward across insert/delete cycles in ACE as much as here. Measured, and @@ -1376,8 +1189,7 @@ private void UpdateTdefCounters(JetFormatBase format, object?[] values, bool[]? { byte[] tdef = _channel.ReadPageShared(_table.DefinitionPage).Span.ToArray(); - int count = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(format.TdefRowCountOffset, 4)); - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(format.TdefRowCountOffset, 4), count + 1); + TableDefinition.WriteRowCount(tdef, format, TableDefinition.ReadRowCount(tdef, format) + 1); // The record's complex id rides its own counter at 0x1C, and every complex column of the table holds // the one id, so the high-water moves once. Deleting a row never rolls it back (verified vs ACE — @@ -1386,11 +1198,8 @@ private void UpdateTdefCounters(JetFormatBase format, object?[] values, bool[]? && values[complexColumn.Index] is { } complexValue) { int assignedId = Convert.ToInt32(complexValue, CultureInfo.InvariantCulture); - int complexHighWater = BinaryPrimitives.ReadInt32LittleEndian( - tdef.AsSpan(format.TdefComplexAutoNumberOffset, 4)); - if (assignedId > complexHighWater) - BinaryPrimitives.WriteInt32LittleEndian( - tdef.AsSpan(format.TdefComplexAutoNumberOffset, 4), assignedId); + if (assignedId > TableDefinition.ReadComplexAutoNumber(tdef, format)) + TableDefinition.WriteComplexAutoNumber(tdef, format, assignedId); } foreach (ColumnDef column in _table.Columns) @@ -1404,7 +1213,7 @@ private void UpdateTdefCounters(JetFormatBase format, object?[] values, bool[]? // don't form a monotone sequence) and diverge from Access's on-disk state. if (column.IsRandomAutoNumber) continue; int assigned = Convert.ToInt32(value, CultureInfo.InvariantCulture); - int highWater = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(format.TdefLastAutoNumberOffset, 4)); + int highWater = TableDefinition.ReadLastAutoNumber(tdef, format); // 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 @@ -1424,7 +1233,7 @@ private void UpdateTdefCounters(JetFormatBase format, object?[] values, bool[]? || (column.Increment >= 0 ? assigned > highWater : assigned < highWater); int newHighWater = advances ? assigned : highWater; if (advances) - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(format.TdefLastAutoNumberOffset, 4), assigned); + TableDefinition.WriteLastAutoNumber(tdef, format, assigned); // Keep the cached catalog seed in sync with the on-disk 0x14. The insert path itself reads the // high-water from disk (AssignAutoNumbers), so this isn't needed for assigning ids — but anything that // reconstructs the counter from the cached ColumnDef.Seed would, left stale, reset it to its @@ -1438,9 +1247,8 @@ private void UpdateTdefCounters(JetFormatBase format, object?[] values, bool[]? { if (!index.IsUnique && !newKeys.Contains(index.RootPage)) continue; if (index.IgnoreNulls && HasNullKey(index, values)) continue; // row was excluded from the index - int statsUnique = format.TdefRealIndexBlockOffset + index.RealIndexOrdinal * format.RealIndexEntrySize + 4; - int unique = BinaryPrimitives.ReadInt32LittleEndian(tdef.AsSpan(statsUnique, 4)); - BinaryPrimitives.WriteInt32LittleEndian(tdef.AsSpan(statsUnique, 4), unique + 1); + (int total, int unique) = TableDefinition.ReadIndexCounts(tdef, format, index.RealIndexOrdinal); + TableDefinition.WriteIndexCounts(tdef, format, index.RealIndexOrdinal, total, unique + 1); } _channel.WritePage(_table.DefinitionPage, tdef); diff --git a/src/LibRed/LibRed.Core/Storage/RowLayout.cs b/src/LibRed/LibRed.Core/Storage/RowLayout.cs deleted file mode 100644 index 419855ea..00000000 --- a/src/LibRed/LibRed.Core/Storage/RowLayout.cs +++ /dev/null @@ -1,112 +0,0 @@ -using LibRed.Catalog; -using System.Buffers.Binary; - -namespace LibRed.Storage; - -/// -/// Parses the structural trailer of an inline row record once (spec §5), so the several call sites that -/// need to locate a row's regions don't each re-derive the offset arithmetic. Layout: -/// -/// [count:2] [fixed data] [var data] [varOffsetTable:(numVar+1)x2] [numVar:2] [nullBitmap] -/// -/// The leading count is maxColumnId + 1 and drives the null-bitmap width. A table with NO variable -/// columns omits the whole variable section (offset table + numVar) — such a row can't self-describe that, -/// so the caller passes hasVar from the schema. -/// -internal readonly ref struct RowLayout -{ - private readonly ReadOnlySpan _row; - - /// The leading column count (= max column id + 1). - public int ColumnCount { get; } - /// Null-bitmap width in bytes, from the leading count. - public int NullBitmapSize { get; } - /// Number of variable columns stored (0 when the table has no variable section). - public int NumVar { get; } - /// Offset of the variable-offset table, or -1 when there is no variable section. - public int VarTableStart { get; } - /// Length of the fixed-data region (bytes between the leading count and the variable data). - public int FixedRegionLength { get; } - - private RowLayout(ReadOnlySpan row, int countSize, bool hasVar) - { - if (countSize != 2 || row.Length < countSize) - throw new InvalidDataException("Row is too short to contain its 2-byte column count."); - - _row = row; - ColumnCount = BinaryPrimitives.ReadUInt16LittleEndian(row[..countSize]); - NullBitmapSize = (ColumnCount + 7) / 8; - if (row.Length < countSize + NullBitmapSize) - throw new InvalidDataException( - $"Row is {row.Length} bytes, too short for its {NullBitmapSize}-byte null bitmap."); - - if (!hasVar) - { - NumVar = 0; - VarTableStart = -1; - FixedRegionLength = row.Length - countSize - NullBitmapSize; - return; - } - - int numVarPos = row.Length - NullBitmapSize - 2; - if (numVarPos < countSize + 2) - throw new InvalidDataException("Row is too short to contain a variable-column trailer."); - NumVar = BinaryPrimitives.ReadUInt16LittleEndian(row.Slice(numVarPos, 2)); - long tableStart = (long)numVarPos - ((long)NumVar + 1) * 2; - if (tableStart < countSize || tableStart > numVarPos) - throw new InvalidDataException( - $"Row declares {NumVar} variable slots, placing its offset table outside the row."); - VarTableStart = (int)tableStart; - - int previous = VarOffset(0); - if (previous < countSize || previous > VarTableStart) - throw new InvalidDataException( - $"Row variable-data end {previous} is outside the data region ending at {VarTableStart}."); - for (int entry = 1; entry <= NumVar; entry++) - { - int current = VarOffset(entry); - if (current < countSize || current > previous) - throw new InvalidDataException( - $"Row variable offset {entry} ({current}) is outside or above its preceding boundary {previous}."); - previous = current; - } - // The last offset-table entry is the variable-data start (= count field + fixed region). - FixedRegionLength = previous - countSize; - } - - /// Parses ; is whether the schema has any variable column. - public static RowLayout Parse(ReadOnlySpan row, int countSize, bool hasVar) => new(row, countSize, hasVar); - - /// Whether this row carries a variable section — the argument every - /// caller needs, derived once here rather than at each call site. - /// - /// It is not "does the schema have a variable column": a row written before the table's first variable - /// ADD COLUMN has no trailer even though the current schema does, because ADD COLUMN is metadata-only. - /// The row's own stored count is what dates it — a column id at or above the count did not exist when - /// the row was written. Get this wrong and the parse reads the last bytes of FIXED data as numVar and an - /// offset table, which the bounds checks above usually catch, but not always. - /// - public static bool HasVariableSection(ReadOnlySpan row, IReadOnlyList columns) - { - if (row.Length < 2) - throw new InvalidDataException("Row is too short to contain its 2-byte column count."); - int storedCount = BinaryPrimitives.ReadUInt16LittleEndian(row[..2]); - foreach (ColumnDef column in columns) - if (!column.IsFixedLength && column.ColumnId < storedCount) return true; - return false; - } - - /// The raw bytes of variable column (end-first offset table). - public ReadOnlySpan VarChunk(int variableIndex) - { - if (variableIndex < 0 || variableIndex >= NumVar) - throw new InvalidDataException( - $"Row has {NumVar} variable slots but column metadata requests slot {variableIndex}."); - int start = VarOffset(NumVar - variableIndex); - int end = VarOffset(NumVar - variableIndex - 1); - return _row[start..end]; - } - - private int VarOffset(int entry) => - BinaryPrimitives.ReadUInt16LittleEndian(_row.Slice(VarTableStart + entry * 2, 2)); -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/RowRelocationReader.cs b/src/LibRed/LibRed.Core/Storage/RowRelocationReader.cs deleted file mode 100644 index 6ba62986..00000000 --- a/src/LibRed/LibRed.Core/Storage/RowRelocationReader.cs +++ /dev/null @@ -1,66 +0,0 @@ -using LibRed.IO; -using LibRed.Pages; -using System.Buffers.Binary; - -namespace LibRed.Storage; - -/// A validated, page-backed relocated-row target. The bytes remain zero-copy for index seeks. -internal readonly record struct RelocatedRow(PageBuffer Buffer, RowSlot Slot, int RowNumber) -{ - public ReadOnlySpan Bytes => Buffer.Slice(Slot.Offset, Slot.Length); -} - -/// -/// 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, - RowSlot sourceSlot, ReadOnlySpan sourceBytes) - { - if (sourceSlot.IsDeleted || !sourceSlot.HasOverflow) - throw new InvalidDataException("A relocation source must be a live overflow row slot."); - if (sourceBytes.Length < 4) - throw new InvalidDataException( - $"A relocation source must begin with a 4-byte pointer; found {sourceBytes.Length} bytes."); - - int pointer = BinaryPrimitives.ReadInt32LittleEndian(sourceBytes[..4]); - int pageNumber = pointer >> 8; - int rowNumber = pointer & 0xFF; - if (pageNumber <= 0 || pageNumber >= channel.PageCount) - throw new InvalidDataException( - $"Relocation pointer targets page {pageNumber}, outside the file's 1..{channel.PageCount - 1} range."); - - PageBuffer targetBuffer = channel.ReadPageShared(pageNumber); - uint owner = targetBuffer.ReadUInt32(channel.Format.DataOwnerOffset); - if (owner != (uint)owningTablePage) - throw new InvalidDataException( - $"Relocation target page {pageNumber} belongs to TDEF {owner}, not TDEF {owningTablePage}."); - if (!DataPage.TryReadRow(targetBuffer, channel.Format, rowNumber, out RowSlot targetSlot, out _)) - throw new InvalidDataException( - $"Relocation pointer targets missing row {rowNumber} on page {pageNumber}."); - if (!targetSlot.IsDeleted || targetSlot.HasOverflow || targetSlot.Length == 0) - throw new InvalidDataException( - $"Relocation target {pageNumber}:{rowNumber} is not a nonempty hidden inline row."); - - return new RelocatedRow(targetBuffer, targetSlot, rowNumber); - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/Table.cs b/src/LibRed/LibRed.Core/Storage/Table.cs index a0be2540..81ef4817 100644 --- a/src/LibRed/LibRed.Core/Storage/Table.cs +++ b/src/LibRed/LibRed.Core/Storage/Table.cs @@ -1,30 +1,33 @@ using LibRed.Catalog; using LibRed.IO; +using LibRed.Pages; namespace LibRed.Storage; /// -/// An opened table: pairs a with the means to read its rows. +/// An opened table: pairs a with the means to read its rows. /// The primary entry point for scanning data out of the storage layer. /// public sealed class Table { - public Table(PageChannel channel, TableDef definition) + internal Table(PageChannel channel, TableDefinition definition, JetCatalog catalog) { Channel = channel; + _catalog = catalog; Definition = definition; UsageMap = new UsageMap(channel, definition); } - public PageChannel Channel { get; } - public TableDef Definition { get; } - public UsageMap UsageMap { get; } + internal PageChannel Channel { get; } + private readonly JetCatalog _catalog; + public TableDefinition Definition { get; } + internal UsageMap UsageMap { get; } public string Name => Definition.Name; /// Returns a forward-only cursor over all rows in the table. /// Which columns to decode, by , or null for all; a column - /// left out reads as null. For a reader that never looks at it — see . + /// left out reads as null. For a reader that never looks at it — see . public TableCursor Rows(bool[]? decode = null) => new(this, decode); /// A decode mask for and the seeks that reads only @@ -71,9 +74,9 @@ public bool[] DecodeOnly(IEnumerable columns) } /// A row decoder over this table's columns — reuse one across a seek/scan rather than allocating - /// per row (each carries a shared ). - private RowDecoder NewDecoder(bool[]? decode = null) => - new(Definition.Columns, Channel.Format, new LongValueReader(Channel), decode); + /// per row (each carries a shared ). + private RowCodec NewDecoder(bool[]? decode = null) => + new(Definition.Columns, Channel.Format, longValues: new LongValueStore(Channel), decode: decode); /// The decoder a seek reads its rows with: the last one made, while it was made for the same column /// mask (the same array — a caller works one out and passes it to every seek), else a new one. @@ -81,29 +84,29 @@ private RowDecoder NewDecoder(bool[]? decode = null) => /// array copied, a long-value reader made — was a sixth of what such a join allocated. A decoder holds nothing /// that changes as it decodes, so one serves every seek, interleaved or not. The mask and its decoder are held /// as one object, so no reader can pair one with the other's partner. - private RowDecoder SeekDecoder(bool[]? decode) + private RowCodec SeekDecoder(bool[]? decode) { if (_seekDecoder is { } held && ReferenceEquals(held.Mask, decode)) return held.Decoder; - RowDecoder decoder = NewDecoder(decode); + RowCodec decoder = NewDecoder(decode); _seekDecoder = new MaskedDecoder(decode, decoder); return decoder; } - private sealed record MaskedDecoder(bool[]? Mask, RowDecoder Decoder); + private sealed record MaskedDecoder(bool[]? Mask, RowCodec Decoder); private MaskedDecoder? _seekDecoder; /// The index reader every seek goes through, made once: seeking changes nothing about it. - private IndexWriter IndexReader => _indexReader ??= new IndexWriter(Channel, Definition); + private IndexTree IndexReader => _indexReader ??= new IndexTree(Channel, Definition); - private IndexWriter? _indexReader; + private IndexTree? _indexReader; /// Decodes the row at (following an overflow forward-pointer to a /// relocated row), or if the slot is empty/deleted. Used by an index seek, which /// yields row ids. public object?[]? GetRow(RowId id) => GetRow(id, NewDecoder()); - private object?[]? GetRow(RowId id, RowDecoder decoder) + private object?[]? GetRow(RowId id, RowCodec decoder) { if (id.Page <= 0 || id.Page >= Channel.PageCount) throw new InvalidDataException( @@ -112,13 +115,18 @@ private sealed record MaskedDecoder(bool[]? Mask, RowDecoder Decoder); // 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. - if (!Pages.DataPage.TryReadRow(Channel.ReadPageShared(id.Page), Channel.Format, id.Row, out Pages.RowSlot slot, out ReadOnlySpan bytes)) + PageBuffer page = Channel.ReadPageShared(id.Page); + if (!Pages.DataPage.TryReadRow(page, Channel.Format, id.Row, out DataPage.RowSlot slot, out ReadOnlySpan bytes)) return null; + uint owner = DataPage.ReadOwner(page.Span, Channel.Format); + if (owner != (uint)Definition.DefinitionPage) + throw new InvalidDataException($"Row page {id.Page} belongs to TDEF {owner}, not TDEF {Definition.DefinitionPage}."); + if (slot.IsDeleted) return null; if (slot.HasOverflow) { - RelocatedRow target = RowRelocationReader.Resolve( + DataPage.RelocatedRow target = DataPage.ResolveRelocation( Channel, Definition.DefinitionPage, slot, bytes); return decoder.Decode(target.Bytes); } @@ -130,7 +138,7 @@ private sealed record MaskedDecoder(bool[]? Mask, RowDecoder Decoder); /// the predicate. is as for . public IEnumerable SeekRows(IndexDef index, object?[] values, bool[]? decode = null) { - RowDecoder decoder = SeekDecoder(decode); + RowCodec decoder = SeekDecoder(decode); foreach (RowId id in IndexReader.Seek(index, values)) if (GetRow(id, decoder) is { } row) yield return row; @@ -142,7 +150,7 @@ private sealed record MaskedDecoder(bool[]? Mask, RowDecoder Decoder); /// in full. public IEnumerable<(RowId Id, object?[] Values)> SeekRowsWithIds(IndexDef index, object?[] values, bool[]? decode = null) { - RowDecoder decoder = SeekDecoder(decode); + RowCodec decoder = SeekDecoder(decode); foreach (RowId id in IndexReader.Seek(index, values)) if (GetRow(id, decoder) is { } row) yield return (id, row); @@ -153,7 +161,7 @@ private sealed record MaskedDecoder(bool[]? Mask, RowDecoder Decoder); /// boundaries; the caller re-checks the predicate. is as for . public IEnumerable SeekRangeRows(IndexDef index, object?[]? low, object?[]? high, bool[]? decode = null) { - RowDecoder decoder = SeekDecoder(decode); + RowCodec decoder = SeekDecoder(decode); foreach (RowId id in IndexReader.SeekRange(index, low, high)) if (GetRow(id, decoder) is { } row) yield return row; @@ -165,8 +173,23 @@ private sealed record MaskedDecoder(bool[]? Mask, RowDecoder Decoder); /// Rewrites the row at in place with new values (row id preserved). /// are the columns that actually changed — an unchanged memo/OLE column /// keeps its stored descriptor (no re-materialise), a changed one has its old LVAL pages reclaimed. - public void Update(RowId id, object?[] values, IReadOnlySet changedColumns) => - new RowInserter(Channel, Definition).Update(id, values, changedColumns); + public void Update(RowId id, object?[] values, IReadOnlySet changedColumns) + { + Write(() => + { + object?[] original = GetRow(id) ?? throw new InvalidOperationException($"Row '{id}' does not exist."); + var changed = new HashSet(changedColumns); + for (int i = 0; i < values.Length; i++) + if (original[i] is byte[] oldBytes && values[i] is byte[] newBytes + ? !oldBytes.AsSpan().SequenceEqual(newBytes) : !Equals(original[i], values[i])) + changed.Add(i); + + new RowInserter(Channel, Definition).Update(id, values, changed); + foreach (IndexDef index in Definition.RealIndexes + .Where(i => i.Columns.Any(c => changed.Contains(c.Column.Index)))) + MoveIndexEntry(index, original, values, id); + }); + } /// Rewrites the row treating every column as changed (materialises all long values). public void Update(RowId id, object?[] values) => @@ -175,24 +198,61 @@ public void Update(RowId id, object?[] values) => /// Moves a row's entry in one index when its key changes (remove old key, add new; row id /// unchanged), and counts the move in the index's statistics as ACE does. Used by UPDATE of an indexed /// column. - public void MoveIndexEntry(IndexDef index, object?[] oldValues, object?[] newValues, RowId id) + internal void MoveIndexEntry(IndexDef index, object?[] oldValues, object?[] newValues, RowId id) { - new IndexWriter(Channel, Definition).MoveEntry(index, oldValues, newValues, id); + new IndexTree(Channel, Definition).MoveEntry(index, oldValues, newValues, id); // An IGNORE NULL index the row was absent from never counted it, so has nothing to count out. - if (!(index.IgnoreNulls && IndexWriter.HasNullKey(index, oldValues))) + if (!(index.IgnoreNulls && IndexTree.HasNullKey(index, oldValues))) new RowInserter(Channel, Definition).CountKeyMoved(index); } /// Whether ' key already exists in for a row /// other than — used to enforce a UNIQUE/PRIMARY index on UPDATE. public bool HasDuplicateKey(IndexDef index, object?[] values, RowId excludeRow) => - new IndexWriter(Channel, Definition).KeyExists(index, values, (excludeRow.Page << 8) | excludeRow.Row); + new IndexTree(Channel, Definition).KeyExists(index, values, excludeRow.Packed); /// Soft-deletes the row at (row bytes kept, slot flagged; TDEF row count - /// decremented). The caller removes its index entries first via . - public void Delete(RowId id) => new RowInserter(Channel, Definition).Delete(id); + /// decremented), removing every index entry before reclaiming the row. + public void Delete(RowId id) + { + Write(() => + { + object?[] values = GetRow(id) ?? throw new InvalidOperationException($"Row '{id}' does not exist."); + foreach (ComplexColumn complex in _catalog.ComplexColumns) + { + if (complex.OwnerTable.DefinitionPage != Definition.DefinitionPage) continue; + if (values[complex.OwnerTable.RequireColumn(complex.ColumnName).Index] is not { } raw) continue; + int recordId = Convert.ToInt32(raw, System.Globalization.CultureInfo.InvariantCulture); + var flat = new Table(Channel, complex.FlatTable, _catalog); + int linkColumn = complex.OwnerLink.Index; + foreach ((RowId flatId, _) in flat.RowsWithKey([linkColumn], [recordId], row => + row[linkColumn] is { } link + && Convert.ToInt32(link, System.Globalization.CultureInfo.InvariantCulture) == recordId).ToList()) + flat.Delete(flatId); + } + foreach (IndexDef index in Definition.RealIndexes) + RemoveIndexEntry(index, values, id); + new RowInserter(Channel, Definition).Delete(id); + }); + } + + private void Write(Action action) + { + bool ownTransaction = !Channel.InTransaction; + if (ownTransaction) Channel.BeginTransaction(); + try + { + action(); + if (ownTransaction) Channel.CommitTransaction(flush: false); + } + catch + { + if (ownTransaction) Channel.RollbackTransaction(); + throw; + } + } /// Removes a deleted row's entry from one index. - public void RemoveIndexEntry(IndexDef index, object?[] values, RowId id) => - new IndexWriter(Channel, Definition).DeleteEntry(index, values, id); + internal void RemoveIndexEntry(IndexDef index, object?[] values, RowId id) => + new IndexTree(Channel, Definition).DeleteEntry(index, values, id); } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/TableCursor.cs b/src/LibRed/LibRed.Core/Storage/TableCursor.cs index c994c28c..bdbc8fd1 100644 --- a/src/LibRed/LibRed.Core/Storage/TableCursor.cs +++ b/src/LibRed/LibRed.Core/Storage/TableCursor.cs @@ -8,7 +8,7 @@ namespace LibRed.Storage; /// decodes each inline row, and yields one value array per row. /// /// The table to read. -/// Which columns to decode, or null for all; see . +/// Which columns to decode, or null for all; see . public sealed class TableCursor(Table table, bool[]? decode = null) : IEnumerable { private readonly Table _table = table; @@ -24,11 +24,11 @@ public sealed class TableCursor(Table table, bool[]? decode = null) : IEnumerabl /// reference the row (e.g. back-filling an index over existing data). public IEnumerable<(RowId Id, object?[] Values)> WithIds() { - var decoder = new RowDecoder( + var decoder = new RowCodec( _table.Definition.Columns, _table.Channel.Format, - new LongValueReader(_table.Channel), - _decode); + longValues: new LongValueStore(_table.Channel), + decode: _decode); foreach (int pageNumber in _table.UsageMap.DataPages()) { @@ -41,7 +41,7 @@ public sealed class TableCursor(Table table, bool[]? decode = null) : IEnumerabl for (int i = 0; i < page.RowCount; i++) { - RowSlot slot = page.Rows[i]; + DataPage.RowSlot slot = page.Rows[i]; if (slot.IsDeleted) continue; // deleted, or a relocated row's hidden target (reached via its pointer) // A relocated row: the slot holds a 4-byte pointer (page<<8 | row) to the real row, which lives @@ -49,7 +49,7 @@ public sealed class TableCursor(Table table, bool[]? decode = null) : IEnumerabl // index entries keep pointing here. Follow the pointer and decode the target's bytes. if (slot.HasOverflow) { - RelocatedRow target = RowRelocationReader.Resolve( + DataPage.RelocatedRow target = DataPage.ResolveRelocation( _table.Channel, _table.Definition.DefinitionPage, slot, page.GetRow(i)); yield return (new RowId(pageNumber, i), decoder.Decode(target.Bytes)); continue; diff --git a/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs b/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs index c1b05b4e..a9a32b00 100644 --- a/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs +++ b/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs @@ -11,7 +11,7 @@ namespace LibRed.Storage.Types; /// the per-type quirks: the 1899-12-30 OLE date epoch, Jet CURRENCY (scaled int64), /// GUID byte order, and UTF-16LE text. A memo/OLE value that lives on LVAL pages is handled as its /// descriptor here — resolving the pages needs page access this codec deliberately does not have, and is -/// RowDecoder's job through its LongValueReader. +/// RowCodec's job through its LongValueStore. /// public static class JetTypeCodec { @@ -32,7 +32,7 @@ public static class JetTypeCodec JetDataType.Int32 or JetDataType.Single or JetDataType.Complex => 4, JetDataType.Int64 or JetDataType.Double or JetDataType.DateTime or JetDataType.Currency => 8, JetDataType.Guid => 16, - JetDataType.FixedPoint => 17, + JetDataType.FixedPoint => NumericLength, JetDataType.DateTimeExtended => ExtendedDateTimeLength, _ => -1, }; @@ -62,7 +62,10 @@ public static class JetTypeCodec case JetDataType.Double: return BinaryPrimitives.ReadDoubleLittleEndian(value); case JetDataType.DateTime: - return DateTime.FromOADate(BinaryPrimitives.ReadDoubleLittleEndian(value)); + double serial = BinaryPrimitives.ReadDoubleLittleEndian(value); + return TryFromOaDate(serial, out DateTime date) + ? date + : throw new InvalidDataException($"Column '{column.Name}' holds {serial}, which is not a date."); case JetDataType.DateTimeExtended: // ACE 17 DATETIME2 return DecodeExtendedDateTime(value); case JetDataType.Currency: @@ -78,7 +81,7 @@ public static class JetTypeCodec return DecodeNumeric(value, column.Scale); // Long values live on LVAL pages, so the inline bytes are only a descriptor. Resolving them needs - // page access this codec deliberately does not have: RowDecoder holds the LongValueReader and + // page access this codec deliberately does not have: RowCodec holds the LongValueStore and // substitutes the real value, and hands the raw bytes here only when it has none. case JetDataType.Memo: case JetDataType.Ole: @@ -130,7 +133,7 @@ private static DateTime DecodeExtendedDateTime(ReadOnlySpan value) /// /// 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 + /// verbatim (see IndexKeyCodec), 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 @@ -155,18 +158,25 @@ internal static byte[] EncodeExtendedDateTime(DateTime value) /// The fixed on-disk width of a DATETIME2 (Date/Time Extended) value. internal const int ExtendedDateTimeLength = 42; + // A Decimal/Numeric value's fixed layout: a sign byte, then a 128-bit magnitude as four 32-bit little-endian + // words in big-endian word order — top, high, middle, low. + private const int NumericLength = 17; + private const byte NumericNegative = 0x80; // in the sign byte, at 0 + private const int NumericTopWord = 1, NumericHighWord = 5, NumericMidWord = 9, NumericLowWord = 13; + /// - /// 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 - /// order (the low word last). The value is the magnitude divided by 10^scale. + /// Decodes a Jet Decimal/Numeric value ( bytes): a sign byte + /// ( = negative) followed by a 128-bit magnitude stored as four 32-bit + /// little-endian words in big-endian word order (the low word last). The value is the magnitude divided by + /// 10^scale. /// private static decimal DecodeNumeric(ReadOnlySpan value, byte scale) { - bool negative = (value[0] & 0x80) != 0; - uint lo = BinaryPrimitives.ReadUInt32LittleEndian(value.Slice(13, 4)); - uint mid = BinaryPrimitives.ReadUInt32LittleEndian(value.Slice(9, 4)); - uint hi = BinaryPrimitives.ReadUInt32LittleEndian(value.Slice(5, 4)); - uint top = BinaryPrimitives.ReadUInt32LittleEndian(value.Slice(1, 4)); + bool negative = (value[0] & NumericNegative) != 0; + uint lo = BinaryPrimitives.ReadUInt32LittleEndian(value.Slice(NumericLowWord, sizeof(uint))); + uint mid = BinaryPrimitives.ReadUInt32LittleEndian(value.Slice(NumericMidWord, sizeof(uint))); + uint hi = BinaryPrimitives.ReadUInt32LittleEndian(value.Slice(NumericHighWord, sizeof(uint))); + uint top = BinaryPrimitives.ReadUInt32LittleEndian(value.Slice(NumericTopWord, sizeof(uint))); if (top != 0) throw new OverflowException("Numeric value exceeds the range of System.Decimal."); @@ -237,7 +247,7 @@ public static string DecodeText(ReadOnlySpan value) /// and all. internal static decimal CurrencyFromScaled(long raw) { - byte scale = 4; + byte scale = CurrencyScale; while (scale > 0 && raw % 10 == 0) { raw /= 10; @@ -249,6 +259,15 @@ internal static decimal CurrencyFromScaled(long raw) return new decimal((int)(uint)magnitude, (int)(uint)(magnitude >> 32), 0, raw < 0, scale); } + /// A value as a CURRENCY stores it — the inverse of : the decimal + /// (through , never the runtime's conversion) × 10,000, rounded. + internal static long CurrencyToScaled(object value, IFormatProvider c) => + (long)decimal.Round(JetDecimalConverter.ToDecimal(value, c) * CurrencyFactor); + + /// A CURRENCY's fixed decimal places — OLE Automation's CY, an int64 scaled by 10,000. + private const byte CurrencyScale = 4; + private const decimal CurrencyFactor = 10000m; + /// Plain UTF-16LE text, as decodes it. /// Without a surrogate code unit, and at an even length, UTF-16LE bytes ARE the string's chars, so /// they are copied rather than decoded — the decoder's validation pass, counting and then converting, was @@ -275,19 +294,19 @@ private static string DecodeUtf16(ReadOnlySpan value) /// one reaches here as the pre-built descriptor of a value /// has already put on LVAL pages. /// - public static byte[] Encode(ColumnDef column, object value) => Encode(column, column.Type, value); + public static byte[] Encode(ColumnDef column, object value, JetFormatBase format) => Encode(column, column.Type, value, format); /// Encodes as rather than the column's declared /// type — the write-side counterpart of the /// overload, and needed for the same reason: a calculated column's payload is encoded in its /// ResultType, not in the promoted type its descriptor carries. - internal static byte[] Encode(ColumnDef column, JetDataType type, object value) + internal static byte[] Encode(ColumnDef column, JetDataType type, object value, JetFormatBase format) { var c = System.Globalization.CultureInfo.InvariantCulture; // A long value already written to an LVAL page arrives as its pre-built 12-byte reference // descriptor, which is written verbatim (memo/OLE columns only). - if (value is LibRed.Storage.LongValueDescriptor descriptor) + if (value is LibRed.Storage.LongValueStore.DescriptorValue descriptor) return descriptor.Bytes; // Jet represents a boolean as -1 (true) / 0 (false). EF maps a CLR bool onto a numeric @@ -319,10 +338,10 @@ internal static byte[] Encode(ColumnDef column, JetDataType type, object value) 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(JetDecimalConverter.ToDecimal(value, c) * 10000m))); + return Bytes(8, b => BinaryPrimitives.WriteInt64LittleEndian(b, CurrencyToScaled(value, c))); case JetDataType.Guid: // Coerced, not cast: every other type here accepts what the caller has (AsText, AsBinary, ToOaDate, - // Convert.To*), and TableCreator.ConvertValue already parses a string GUID on the ALTER path. A hard + // Convert.To*), and SchemaEditor.ConvertValue already parses a string GUID on the ALTER path. A hard // cast turned a string reaching a GUID column into an InvalidCastException with no column named. return (value switch { @@ -341,9 +360,9 @@ string s when TryParseGuid(s, out Guid parsed) => parsed, return EncodeNumeric(column, JetDecimalConverter.ToDecimal(value, c)); // Long values (memo/OLE): store the payload inline after the 12-byte descriptor (memo - // text as UTF-16LE, OLE as raw bytes). LongValueReader reads this back via the inline flag. Only a + // text as UTF-16LE, OLE as raw bytes). LongValueStore reads this back via the inline flag. Only a // value the caller has already decided to inline reaches here; anything larger is written to LVAL - // pages, single or chained, by LongValueWriter before the row is encoded. + // pages, single or chained, by LongValueStore before the row is encoded. case JetDataType.Memo: { // An inline memo compresses whether or not the column was declared WITH COMPRESSION — the @@ -351,10 +370,10 @@ string s when TryParseGuid(s, out Guid parsed) => parsed, // the caller has already decided to inline reach here, so no storage-form test is needed. string memo = AsText(value, c); return EncodeInlineLongValue( - TryCompressText(column, memo, requireCapableFlag: false) ?? Encoding.Unicode.GetBytes(memo)); + TryCompressText(column, memo, requireCapableFlag: false) ?? Encoding.Unicode.GetBytes(memo), format); } case JetDataType.Ole: - return EncodeInlineLongValue(AsBinary(column, value)); + return EncodeInlineLongValue(AsBinary(column, value), format); default: throw new NotSupportedException($"Encoding {column.Type} is not supported yet."); @@ -450,7 +469,7 @@ private static byte[] EncodeText(ColumnDef column, string value) /// The padding exists for the short case — ACE stores fixed text space-padded to the full width — but it /// truncates the long case just as silently, so CHAR(3) accepted 'abcdef' and stored 'abc' while the /// variable column of the same width raised. Same message and units as the variable-width check in - /// RowEncoder, because to a caller it is the same mistake. + /// RowCodec, because to a caller it is the same mistake. internal static void EnsureFitsFixedWidth(ColumnDef column, int encodedLength, int declaredUnits) { if (encodedLength <= column.Length) return; @@ -514,8 +533,9 @@ private static byte[] Bytes(int length, Action> write) }; /// 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); + /// storing a / as a date offset — on the way in here, on the way + /// out of a reader, and for a parameter bound to one. + public static readonly DateTime OleEpoch = new(1899, 12, 30); /// /// Converts a date/time-ish CLR value to the OLE-automation double stored in a Jet DateTime @@ -547,25 +567,30 @@ internal static double ToOaDate(ColumnDef column, object value, IFormatProvider /// The earliest date ACE stores: 0100-01-01, serial -657434. private static readonly DateTime MinOaDate = new(100, 1, 1); - /// - /// Builds an inline long-value (memo/OLE) in-row value: a 12-byte descriptor - /// (length combined with the 0x80 inline flag, then 8 unused bytes) followed by the payload. - /// This is the exact shape reads back for an inline - /// value. - /// - internal static byte[] EncodeInlineLongValue(ReadOnlySpan payload) + /// The date at an OLE Automation serial read from a file, or false where + /// would refuse it — not finite, or outside its range — so a caller reports a bad value as it chooses rather than + /// as an argument error from inside the conversion. + internal static bool TryFromOaDate(double serial, out DateTime date) { - LongValueFormat.ValidateLength(payload.Length); - var result = new byte[12 + payload.Length]; - BinaryPrimitives.WriteUInt32LittleEndian(result, (uint)payload.Length | 0x80000000u); - payload.CopyTo(result.AsSpan(12)); - return result; + // FromOADate's own bounds, both exclusive: 0100-01-01 through 9999-12-31. + const double MinSerial = -657435.0, MaxSerial = 2958466.0; + bool valid = double.IsFinite(serial) && serial > MinSerial && serial < MaxSerial; + date = valid ? DateTime.FromOADate(serial) : default; + return valid; } - /// Inverse of : 17 bytes, sign + 128-bit magnitude (top word 0). + /// + /// Builds an inline long-value (memo/OLE) in-row value: the descriptor + /// (, no pointer, no stamp) followed by the payload. This is the exact + /// shape reads back for an inline value. + /// + internal static byte[] EncodeInlineLongValue(ReadOnlySpan payload, JetFormatBase format) => + [.. LongValueStore.Descriptor(format, payload.Length, LongValueStore.StorageKind.Inline), .. payload]; + /// The largest precision Jet/ACE accepts on a NUMERIC/DECIMAL column. internal const byte MaxNumericPrecision = 28; + /// Inverse of . private static byte[] EncodeNumeric(ColumnDef column, decimal value) { byte scale = column.Scale; @@ -587,17 +612,17 @@ private static byte[] EncodeNumeric(ColumnDef column, decimal value) // Truncate toward zero: ACE coerces excess scale rather than refusing it, and truncation matches it in // every measured case (1.23456 → 1.2345, 1.99999 → 1.9999, -1.23455 → -1.2345). Was decimal.Round(…, 0) // — ToEven — which differed silently, each engine reading its own answer back happily. - // IndexKeyEncoder.EncodeFixedPoint must quantise identically or keys stop matching their rows. + // IndexKeyCodec.EncodeFixedPoint must quantise identically or keys stop matching their rows. decimal magnitude = decimal.Truncate(Math.Abs(value) * factor); int[] bits = decimal.GetBits(magnitude); // [lo, mid, hi, flags]; magnitude has scale 0 - var result = new byte[17]; + var result = new byte[NumericLength]; // IsNegative, not < 0: a negative zero read back from ACE's row (-0.0000m) has to be written back as one. - result[0] = (byte)(decimal.IsNegative(value) ? 0x80 : 0x00); - // bytes[1..5) top word = 0; hi at 5, mid at 9, lo at 13 (see DecodeNumeric). - BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(5, 4), (uint)bits[2]); - BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(9, 4), (uint)bits[1]); - BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(13, 4), (uint)bits[0]); + result[0] = decimal.IsNegative(value) ? NumericNegative : (byte)0x00; + // The top word stays 0: a System.Decimal is 96 bits. + BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(NumericHighWord, sizeof(uint)), (uint)bits[2]); + BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(NumericMidWord, sizeof(uint)), (uint)bits[1]); + BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(NumericLowWord, sizeof(uint)), (uint)bits[0]); return result; } diff --git a/src/LibRed/LibRed.Core/Storage/UsageMap.cs b/src/LibRed/LibRed.Core/Storage/UsageMap.cs index 31beca07..30584810 100644 --- a/src/LibRed/LibRed.Core/Storage/UsageMap.cs +++ b/src/LibRed/LibRed.Core/Storage/UsageMap.cs @@ -11,23 +11,34 @@ namespace LibRed.Storage; /// /// /// The TDEF holds a pointer (row + page) to the usage-map record. An inline map -/// (type 0x00) stores a start page and a bitmap where bit i marks page (startPage + i) -/// as owned. A reference map (type 0x01, for very large tables) instead stores a list of -/// pointers to dedicated bitmap pages (page type 0x0105); pointer k's bitmap covers the page -/// range starting at k * (pageSize - 4) * 8. +/// () stores a start page and a bitmap where bit i marks page (startPage + i) +/// as owned. A reference map (, for very large tables) instead stores a list +/// of pointers to dedicated bitmap pages (page type 0x0105); pointer k's bitmap covers the page range starting +/// at k * . /// -public sealed class UsageMap(PageChannel channel, TableDef table) +internal sealed class UsageMap(PageChannel channel, TableDefinition? table = null) { - private const byte MapTypeInline = 0x00; - private const byte MapTypeReference = 0x01; - private const int ReferenceMapSlots = 17; - private const int ReferenceMapRecordSize = 1 + ReferenceMapSlots * 4; - - /// Bytes preceding the bitmap on a dedicated usage-bitmap page (type 0x0105). - private const int BitmapPageHeaderSize = 4; + /// Builds the global map page: two full-width inline maps. Row 0 is the free map — pages < + /// are used (bit 0); pages from there to the map's reach are marked free + /// (bit 1), pre-declaring space beyond the file end so an allocator (LibRed's or Access's) grabs a "free" page + /// and grows the file. Row 1 is the released map, empty, as real files carry. + internal static byte[] BuildGlobalMapPage(JetFormatBase format, int usedPages) + { + // The holder's owner field reads 1 on the global map page (observed), where a table's usage-map page has 0. + byte[] page = UsageMap.NewMapPage(format, mapCount: 2, owner: 1); + + Span freeMap = UsageMap.InlineBits( + page.AsSpan(DataPage.ReadSlot(page, format, 0).Offset, format.UsageMapInlineRecordSize), format); // row 0's + for (int p = usedPages; p < format.UsageMapInlineBitmapSize * 8; p++) + BitmapBits.Set(freeMap, p, true); + return page; + } private readonly PageChannel _channel = channel; - private readonly TableDef _table = table; + private readonly TableDefinition? _table = table; + + private int DefinitionPage => _table?.DefinitionPage + ?? throw new InvalidOperationException("A table definition is required to enumerate its data pages."); /// Yields the page numbers of every data page owned by the table, in ascending order. public IEnumerable DataPages() => PagesAt(_channel.Format.TdefOwnedPagesOffset); @@ -37,38 +48,6 @@ public sealed class UsageMap(PageChannel channel, TableDef table) /// being appended to, so it is the map to consult when looking for somewhere to put a new row. public IEnumerable FreeDataPages() => PagesAt(_channel.Format.TdefFreePagesOffset); - /// The pages recorded by the usage map at an explicit : pointer, rather than one of the TDEF's two fixed-offset maps. A long-value column's - /// owned and free maps are reached this way — their pointers sit in the TDEF keyed by column id, so the - /// pages holding a table's Memo/OLE content are invisible to . - public IEnumerable PagesInMap(int mapRow, int mapPage) => ReadMapAt(mapRow, mapPage); - - /// The dedicated bitmap pages (type 0x0105) a reference-form map record at the pointer names, each - /// validated; none for an inline record. - public IReadOnlyList BitmapPagesOf(int mapRow, int mapPage) - { - if (mapPage <= 1 || mapPage >= _channel.PageCount) - throw new InvalidDataException( - $"Usage-map pointer names page {mapPage}, outside the file's 2..{_channel.PageCount - 1} range."); - var holder = new DataPage(); - holder.Read(_channel.ReadPage(mapPage), _channel.Format); - if (mapRow < 0 || mapRow >= holder.RowCount) - throw new InvalidDataException($"Usage-map row {mapPage}:{mapRow} does not exist."); - ReadOnlySpan map = holder.GetRow(mapRow); - if (map.Length == 0 || map[0] != MapTypeReference) return []; - - ValidateReferenceRecord(map); - var pages = new List(); - for (int e = 0; e < ReferenceMapSlots; e++) - { - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(map.Slice(1 + e * 4, 4)); - if (bitmapPage == 0) continue; - _ = ReadBitmapPage(bitmapPage); - pages.Add(bitmapPage); - } - return pages; - } - /// The highest-numbered data page the table owns, or -1 when it owns none. public int MaxDataPage() => EdgeDataPage(fromEnd: true); @@ -84,152 +63,489 @@ public IReadOnlyList BitmapPagesOf(int mapRow, int mapPage) /// private int EdgeDataPage(bool fromEnd) { - byte[] record = ReadMapRecord(_channel.Format.TdefOwnedPagesOffset); - - if (record.Length == 0) - throw new InvalidDataException("A usage-map record cannot be empty."); + JetFormatBase format = _channel.Format; + PageBuffer tdef = _channel.ReadPage(DefinitionPage); + (int row, int page) = tdef.ReadRecordPointer(format.TdefOwnedPagesOffset); + ReadOnlySpan record = ReadRecordAt(row, page); - if (record[0] == MapTypeInline) + if (RecordType(record) == UsageMapType.Inline) { - if (record.Length < 5) - throw new InvalidDataException("An inline usage-map record must contain its 5-byte header."); - int startPage = BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(1, 4)); - int bit = EdgeSetBit(record.AsSpan(5), fromEnd); - return bit < 0 ? -1 : startPage + bit; + int bit = EdgeSetBit(record[format.UsageMapInlineHeaderSize..], fromEnd); + return bit < 0 ? -1 : StartPage(record, format) + bit; } - if (record[0] != MapTypeReference) - throw new NotSupportedException($"Unknown usage map type 0x{record[0]:X2}."); - - ValidateReferenceRecord(record); - - int pagesPerBitmap = (_channel.PageSize - BitmapPageHeaderSize) * 8; - for (int k = 0; k < ReferenceMapSlots; k++) + for (int k = 0; k < format.UsageMapReferenceSlots; k++) { - int e = fromEnd ? ReferenceMapSlots - 1 - k : k; - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(1 + e * 4, 4)); + int e = fromEnd ? format.UsageMapReferenceSlots - 1 - k : k; + int bitmapPage = ReferencePointer(record, e, format); if (bitmapPage == 0) continue; - int bit = EdgeSetBit(ReadBitmapPage(bitmapPage), fromEnd); - if (bit >= 0) return e * pagesPerBitmap + bit; + int found = EdgeSetBit(BitmapPageBits(ReadBitmapPage(_channel, bitmapPage).Span, format), fromEnd); + if (found >= 0) return e * format.UsageMapPagesPerBitmapPage + found; } - return -1; } - /// Index of the highest () or lowest set bit in - /// , or -1 if it is all zeros. - private static int EdgeSetBit(ReadOnlySpan bitmap, bool fromEnd) + private static int EdgeSetBit(ReadOnlySpan bitmap, bool fromEnd) => + fromEnd ? BitmapBits.LastSetBit(bitmap) : BitmapBits.NextSetBit(bitmap, 0); + + /// Reads the usage map whose (row, page) pointer sits at in + /// the TDEF. Both maps share the same pointer shape and record format. + private IEnumerable PagesAt(int pointerOffset) { - for (int n = 0; n < bitmap.Length; n++) + (int row, int page) = _channel.ReadPage(DefinitionPage).ReadRecordPointer(pointerOffset); + return PagesInMap(row, page); + } + + /// The pages recorded by the usage map at a : + /// pointer. The TDEF's own two maps come through here (, ), + /// and so does every other map reached by an explicit pointer — a long-value column's owned and free maps, + /// whose pointers sit in the TDEF keyed by column id, and an index's own map. + public IEnumerable PagesInMap(int mapRow, int mapPage) => + PagesInRecord(_channel, ReadRecordAt(mapRow, mapPage), _channel.PageCount, "A usage map"); + + /// The pages a map marks, inline or reference form. Pointer k's bitmap covers + /// the page range starting at k * UsageMapPagesPerBitmapPage; a zero pointer means the range has no pages in the + /// map, and a bitmap page belongs to one range only. A page at or past is + /// corruption; null keeps every bit, for a map that may name pages past the file's end (see + /// ). names the map in the messages. + internal static List PagesInRecord(PageChannel channel, ReadOnlySpan record, int? rejectBeyond, string what) + { + JetFormatBase format = channel.Format; + var pages = new List(); + if (RecordType(record) == UsageMapType.Inline) { - int i = fromEnd ? bitmap.Length - 1 - n : n; - if (bitmap[i] == 0) continue; - for (int m = 0; m < 8; m++) - { - int bit = fromEnd ? 7 - m : m; - if ((bitmap[i] & (1 << bit)) != 0) return i * 8 + bit; - } + BitmapBits.AppendPages(pages, record[format.UsageMapInlineHeaderSize..], StartPage(record, format), + rejectBeyond, what); + return pages; } - return -1; + + var bitmapPages = new HashSet(); + for (int e = 0; e < format.UsageMapReferenceSlots; e++) + { + int bitmapPage = ReferencePointer(record, e, format); + if (bitmapPage == 0) continue; + if (!bitmapPages.Add(bitmapPage)) + throw new InvalidDataException($"{what} repeats bitmap page {bitmapPage}."); + BitmapBits.AppendPages(pages, BitmapPageBits(ReadBitmapPage(channel, bitmapPage).Span, format), + e * format.UsageMapPagesPerBitmapPage, rejectBeyond, what); + } + return pages; } - /// The raw usage-map record whose (row, page) pointer sits at - /// in the TDEF. - private byte[] ReadMapRecord(int pointerOffset) + /// The usage-map record a (row, page) pointer out of a TDEF names: , and on top + /// of it an owner-zero holder — a table's, an index's and a long-value column's maps all live on usage-map + /// pages that belong to no table. + /// Both halves of the pointer come out of the TDEF, so both are corruption when wrong. Unchecked, + /// the page number reached the channel as an out-of-range read and the row number reached GetRow as an + /// index. + internal ReadOnlySpan ReadRecordAt(int mapRow, int mapPage) { - JetFormatBase format = _channel.Format; - PageBuffer tdef = _channel.ReadPage(_table.DefinitionPage); + (PageBuffer page, _, DataPage.RowSlot slot) = ReadRecord(_channel, mapRow, mapPage, "Usage-map pointer"); + if (DataPage.ReadOwner(page.Span, _channel.Format) != 0) + throw new InvalidDataException( + $"Usage-map pointer {mapPage}:{mapRow} does not target an owner-zero usage-map data page."); + return page.Slice(slot.Offset, slot.Length); + } + /// + /// The usage-map record a (row, page) pointer names, after proving the pointer names one: a data page inside the + /// file, a live, non-empty record on it, and a record of a known type and a valid length. The one place a map + /// record is located — the TDEF's maps here, page 0's global maps in , and every map + /// writes; names the pointer in the messages. The holder + /// comes back parsed as well, for a writer that repacks it. + /// + internal static (PageBuffer Page, DataPage Holder, DataPage.RowSlot Slot) ReadRecord( + PageChannel channel, int mapRow, int mapPage, string what) + { + JetFormatBase format = channel.Format; + if (mapPage <= 0 || mapPage >= channel.PageCount) + throw new InvalidDataException( + $"{what} names page {mapPage}, outside the file's 1..{channel.PageCount - 1} range."); + + PageBuffer page = channel.ReadPageShared(mapPage); + if (PageHeader.ReadType(page.Span) != PageType.DataPage) + throw new InvalidDataException($"{what} names page {mapPage}, which is not a data page."); var holder = new DataPage(); - holder.Read(_channel.ReadPage(tdef.ReadInt24(pointerOffset + 1)), format); - return holder.GetRow(tdef.ReadByte(pointerOffset)).ToArray(); + holder.Read(page, format); + if (mapRow < 0 || mapRow >= holder.RowCount) + throw new InvalidDataException($"{what} names row {mapPage}:{mapRow}, which does not exist."); + DataPage.RowSlot slot = holder.Rows[mapRow]; + if (slot.IsDeleted || slot.HasOverflow || slot.Length == 0) + throw new InvalidDataException($"{what} names row {mapPage}:{mapRow}, which is deleted, overflowed, or empty."); + + ReadOnlySpan record = page.Slice(slot.Offset, slot.Length); + switch (RecordType(record)) + { + case UsageMapType.Inline: + if (record.Length < format.UsageMapInlineHeaderSize) + throw new InvalidDataException( + $"An inline usage-map record must contain its {format.UsageMapInlineHeaderSize}-byte header."); + break; + case UsageMapType.Reference: + if (record.Length != format.UsageMapReferenceRecordSize) + throw new InvalidDataException( + $"A reference usage-map record must be exactly {format.UsageMapReferenceRecordSize} bytes; got {record.Length}."); + break; + default: + throw new InvalidDataException($"{what} names a usage map of unknown type 0x{record[0]:X2}."); + } + return (page, holder, slot); } - /// Reads the usage map whose (row, page) pointer sits at in - /// the TDEF. Both maps share the same pointer shape and record format. - private List PagesAt(int pointerOffset) + /// A record's , its first byte. + internal static UsageMapType RecordType(ReadOnlySpan record) => (UsageMapType)record[0]; + + /// A new inline record starting at with bytes + /// of bitmap, every bit clear. + internal static byte[] NewInlineRecord(JetFormatBase format, int startPage, int bitmapBytes) + { + var record = new byte[format.UsageMapInlineHeaderSize + bitmapBytes]; + record[0] = (byte)UsageMapType.Inline; + BinaryPrimitives.WriteInt32LittleEndian(record.AsSpan(format.UsageMapStartPageOffset, sizeof(int)), startPage); + return record; + } + + /// A new reference record with no bitmap pages. + internal static byte[] NewReferenceRecord(JetFormatBase format) { - PageBuffer tdef = _channel.ReadPage(_table.DefinitionPage); - return ReadMapAt(tdef.ReadByte(pointerOffset), tdef.ReadInt24(pointerOffset + 1)); + var record = new byte[format.UsageMapReferenceRecordSize]; + record[0] = (byte)UsageMapType.Reference; + return record; } - /// Reads the usage-map record at a (row, page) pointer. Shared by the TDEF's own two maps and by - /// the per-column long-value maps, which differ only in where the pointer is stored. - private List ReadMapAt(int mapRow, int mapPage) + /// An inline record's bitmap — what follows its header. + internal static Span InlineBits(Span record, JetFormatBase format) => + record[format.UsageMapInlineHeaderSize..]; + + /// An inline record's start page. + internal static int StartPage(ReadOnlySpan record, JetFormatBase format) => + BinaryPrimitives.ReadInt32LittleEndian(record.Slice(format.UsageMapStartPageOffset, sizeof(int))); + + /// A reference record's pointer for range ; 0 when the range has no bitmap page. + internal static int ReferencePointer(ReadOnlySpan record, int slot, JetFormatBase format) => + BinaryPrimitives.ReadInt32LittleEndian(record.Slice(ReferencePointerOffset(slot, format), sizeof(int))); + + /// Writes a reference record's pointer for range . + internal static void WriteReferencePointer(Span record, int slot, JetFormatBase format, int bitmapPage) => + BinaryPrimitives.WriteInt32LittleEndian(record.Slice(ReferencePointerOffset(slot, format), sizeof(int)), bitmapPage); + + private static int ReferencePointerOffset(int slot, JetFormatBase format) => + format.UsageMapReferencePointersOffset + slot * sizeof(int); + + /// Whether carries a dedicated bitmap page's complete + /// [05 01 00 00] header — the check every reader and writer makes before trusting a pointer to one. + private static bool IsBitmapPage(ReadOnlySpan page, JetFormatBase format) => + PageHeader.ReadType(page) == PageType.PageUsageBitmap + && !page[sizeof(ushort)..format.UsageMapBitmapPageHeaderSize].ContainsAnyExcept((byte)0); // past the type: zero + + /// + /// The dedicated bitmap page (type 0x0105) a reference record's pointer names, after proving it is one: inside the + /// file and carrying the complete bitmap-page header. The pointer comes out of the file, so this is the one check + /// every reader and writer makes before trusting it — the spec states it as mandatory "before any bitmap is + /// expanded into page numbers", and a writer that skipped it OR'd a bit into an ordinary data, TDEF or index page. + /// The bytes are the channel's shared copy; a writer copies them before changing any. + /// + internal static PageBuffer ReadBitmapPage(PageChannel channel, int pageNumber) { - // Both halves of the pointer come out of the TDEF, so both are corruption when wrong. Unchecked, the - // page number reached the channel as an out-of-range read and the row number reached GetRow as an - // index; the long-value map's equivalent pointer is validated the same way in RowInserter.MapPages. - if (mapPage <= 1 || mapPage >= _channel.PageCount) + if (pageNumber <= 0 || pageNumber >= channel.PageCount) throw new InvalidDataException( - $"Usage-map pointer names page {mapPage}, outside the file's 2..{_channel.PageCount - 1} range."); + $"Usage map names bitmap page {pageNumber}, outside the file's 1..{channel.PageCount - 1} range."); + PageBuffer page = channel.ReadPageShared(pageNumber); + if (!IsBitmapPage(page.Span, channel.Format)) + throw new InvalidDataException( + $"Page {pageNumber} is named as a usage bitmap but does not carry the [05 01 00 00] header."); + return page; + } - var holder = new DataPage(); - holder.Read(_channel.ReadPage(mapPage), _channel.Format); - if (mapRow < 0 || mapRow >= holder.RowCount) - throw new InvalidDataException($"Usage-map row {mapPage}:{mapRow} does not exist."); - ReadOnlySpan map = holder.GetRow(mapRow); + /// A bitmap page's bits — what follows its header. + internal static ReadOnlySpan BitmapPageBits(ReadOnlySpan page, JetFormatBase format) => + page[format.UsageMapBitmapPageHeaderSize..]; - if (map.Length == 0) - throw new InvalidDataException("A usage-map record cannot be empty."); + /// A bitmap page's bits, writable — over a page copy the caller will write back. + internal static Span BitmapPageBits(byte[] page, JetFormatBase format) => + page.AsSpan(format.UsageMapBitmapPageHeaderSize); - return map[0] switch - { - MapTypeInline => ReadInlineMap(map), - MapTypeReference => ReadReferenceMap(map), - byte t => throw new NotSupportedException($"Unknown usage map type 0x{t:X2}."), - }; + /// A fresh, empty dedicated bitmap page (type 0x0105). + internal static byte[] NewBitmapPage(JetFormatBase format) + { + var bitmap = new byte[format.PageSize]; + PageHeader.WriteType(bitmap, PageType.PageUsageBitmap); + return bitmap; } - private List ReadInlineMap(ReadOnlySpan map) + /// A usage-map data page holding empty full-width inline records — + /// what Access writes for a fresh table that has no data page yet. Each record is [0x00][startPage = 0] + /// [all-zero bitmap], row 0 nearest the page end. A table's map page belongs to no table (owner 0); the + /// global maps' holder carries 1. + internal static byte[] NewMapPage(JetFormatBase format, int mapCount, uint owner = 0) { - if (map.Length < 5) - throw new InvalidDataException("An inline usage-map record must contain its 5-byte header."); - int startPage = BinaryPrimitives.ReadInt32LittleEndian(map.Slice(1, 4)); - var pages = new List(); - UsageMapBits.Append(pages, map[5..], startPage, _channel.PageCount, "A usage map"); - return pages; + byte[] page = DataPage.NewPage(format, owner); + // An all-zero record is an empty inline map: inline type, start page 0, zero bitmap. + var records = Enumerable.Range(0, mapCount).Select(_ => new byte[format.UsageMapInlineRecordSize]).ToArray(); + DataPage.LayRows(page, format, records, new RowSlotFlags[mapCount]); + return page; } - private List ReadReferenceMap(ReadOnlySpan map) + /// How many empty full-width inline records, each with its directory slot, fit in + /// of a usage-map page. + internal static int RecordsFitting(JetFormatBase format, int freeBytes) => + freeBytes / (format.UsageMapInlineRecordSize + format.DataRowDirectoryEntrySize); + + /// Sets or clears the bit for in the usage map at record + /// on . + /// + /// Handles both map types. An inline map is grown in place while its record still fits the page; once it + /// cannot, the map is converted to a reference map — exactly Access's own threshold. + /// + /// marks a map whose set bits stay clustered near the append tail — a + /// free-pages map. Rather than growing a bitmap from startPage = 0 all the way out to the tail, + /// such a map slides a fixed full-width window, as Access does, so its record stays full-width forever. + /// An owned-pages map cannot do this: it must retain every page it has ever taken. + /// + /// + public void SetBit(int mapRow, int mapPage, int targetPage, bool set, bool movableWindow = false) { - ValidateReferenceRecord(map); - int pagesPerBitmap = (_channel.PageSize - BitmapPageHeaderSize) * 8; - var pages = new List(); + JetFormatBase format = _channel.Format; + // The pointer comes out of the file, so the record is located as every reader locates it: a live, non-empty + // record of a known type and a valid length, or corruption said as such. This writer used to index the slot + // list bare and fall through on an unknown type byte, mangling the record in place. + (PageBuffer shared, DataPage holder, DataPage.RowSlot slot) = ReadRecord(_channel, mapRow, mapPage, "Usage-map pointer"); + byte[] page = shared.Span.ToArray(); + int mapOffset = slot.Offset; + + if (RecordType(page.AsSpan(mapOffset)) == UsageMapType.Reference) + { + SetReferenceBit(page, mapPage, mapOffset, targetPage, set); + return; + } - // The record is a list of 4-byte pointers to bitmap pages; pointer k's bitmap - // covers the page range starting at k * pagesPerBitmap. A zero pointer means the - // range has no owned pages. - for (int e = 0; e < ReferenceMapSlots; e++) + int headerSize = format.UsageMapInlineHeaderSize; + int startPage = StartPage(page.AsSpan(mapOffset), format); + int bitmapBits = (slot.Length - headerSize) * 8; + int bitIndex = targetPage - startPage; + + // The inline bitmap covers pages [startPage, startPage + bitmapBits). Clearing a bit outside that + // window is a no-op — it is already 0. + if (bitIndex < 0 || bitIndex >= bitmapBits) { - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(map.Slice(1 + e * 4, 4)); - if (bitmapPage == 0) continue; + if (!set) return; + + // A free-pages map slides its window onto the target instead of growing (and can therefore also + // move *backwards*, which a grown map could never do). + if (movableWindow && TryRepositionWindow(page, holder, format, mapPage, mapRow, targetPage)) + return; + + if (bitIndex < 0) + throw new NotSupportedException( + $"Page {targetPage} is below the usage map's start page {startPage}; this map's window cannot move."); + } + + // When Access needs to mark a page beyond the window it grows the bitmap record in place (still + // inline, same startPage), extending it in UsageMapInlineGrowthSize steps. + if (bitIndex >= bitmapBits) + { + int neededBitmapBytes = InlineBitmapBytes(format, bitIndex + 1); + byte[] grownRecord = new byte[headerSize + neededBitmapBytes]; // extra bitmap bytes stay zero + page.AsSpan(mapOffset, slot.Length).CopyTo(grownRecord); + + byte[]? grown = ReplaceMapRecord(page, holder, format, mapRow, grownRecord, out mapOffset); + if (grown is null) + { + // The record can no longer grow within its page: switch to a reference map and retry there. + ConvertInlineToReference(mapPage, mapRow); + (PageBuffer converted, _, DataPage.RowSlot reference) = ReadRecord(_channel, mapRow, mapPage, "Usage-map pointer"); + SetReferenceBit(converted.Span.ToArray(), mapPage, reference.Offset, targetPage, set); + return; + } + + page = grown; + } + + BitmapBits.Set(InlineBits(page.AsSpan(mapOffset), format), bitIndex, set); + _channel.WritePage(mapPage, page); + } + + /// + /// Slides an inline map's window onto : a full-width bitmap starting at the + /// window boundary below the target, carrying over every bit already set and adding the target's. + /// Returns — leaving the map untouched — when some page already marked would + /// fall outside the new window, since moving would silently forget it; the caller then grows instead. + /// + private bool TryRepositionWindow(byte[] page, DataPage holder, JetFormatBase format, int mapPage, int mapRow, int targetPage) + { + DataPage.RowSlot slot = holder.Rows[mapRow]; + int headerSize = format.UsageMapInlineHeaderSize; + int startPage = StartPage(page.AsSpan(slot.Offset), format); + ReadOnlySpan bitmap = page.AsSpan(slot.Offset + headerSize, slot.Length - headerSize); + + int windowPages = format.UsageMapInlineBitmapSize * 8; + int newStart = targetPage / windowPages * windowPages; + int newEnd = newStart + windowPages; + + var marked = new List(); + BitmapBits.AppendPages(marked, bitmap, startPage, rejectBeyond: null, "A free-pages map"); + bool fitsWindow = marked.TrueForAll(p => p >= newStart && p < newEnd); + + // A window that has already moved above the target cannot slide back down without dropping the pages + // it still advertises, so it widens instead: the start drops to the lowest page it must cover (rounded + // down to a byte, as the released map's move does) and the record is sized to reach the highest. That + // keeps it inline and keeps every bit. Marking a page a table's free map cannot represent is not a + // corruption — the bit only advertises room — but silently dropping it is what leaves reusable space + // invisible, and throwing outright failed an ordinary DROP TABLE on a real file (complex1.accdb, whose + // MSysObjects free map sits at page 2288 while its catalog rows live at page 17). + int bitmapBytes = format.UsageMapInlineBitmapSize; + if (!fitsWindow) + { + int lowest = Math.Min(targetPage, marked.Count == 0 ? targetPage : marked.Min()); + int highest = Math.Max(targetPage, marked.Count == 0 ? targetPage : marked.Max()); + newStart = lowest / 8 * 8; + bitmapBytes = InlineBitmapBytes(format, highest - newStart + 1); + } + + byte[] record = NewInlineRecord(format, newStart, bitmapBytes); + marked.Add(targetPage); + foreach (int markedPage in marked) + BitmapBits.Set(InlineBits(record, format), markedPage - newStart, true); - int rangeBase = e * pagesPerBitmap; - ReadOnlySpan bitmap = ReadBitmapPage(bitmapPage); - UsageMapBits.Append(pages, bitmap, rangeBase, _channel.PageCount, "A usage map"); + byte[]? rewritten = ReplaceMapRecord(page, holder, format, mapRow, record, out _); + if (rewritten is null) return false; // shrinking or same size, so effectively unreachable + + _channel.WritePage(mapPage, rewritten); + return true; + } + + /// Sets or clears 's bit in a reference map: pointer slot + /// targetPage / UsageMapPagesPerBitmapPage names the bitmap page holding it. A slot's bitmap page is + /// allocated lazily, only when a bit in its range is first set. + private void SetReferenceBit(byte[] page, int mapPage, int mapOffset, int targetPage, bool set) + { + JetFormatBase format = _channel.Format; + int pagesPerBitmap = format.UsageMapPagesPerBitmapPage; + int slot = targetPage / pagesPerBitmap; + if (slot >= format.UsageMapReferenceSlots) + throw new NotSupportedException( + $"Page {targetPage} lies past the {format.UsageMapReferenceSlots} bitmap slots a usage map can address (Jet's 2 GB file limit)."); + + Span record = page.AsSpan(mapOffset); + int bitmapPage = ReferencePointer(record, slot, format); + if (bitmapPage == 0) + { + if (!set) return; // the bit is already clear — no need to materialize the bitmap page + bitmapPage = AllocateBitmapPage(); + WriteReferencePointer(record, slot, format, bitmapPage); + _channel.WritePage(mapPage, page); } + // The pointer comes out of the file, so the page is proven to be a bitmap page before a bit goes into it. + byte[] bitmap = ReadBitmapPage(_channel, bitmapPage).Span.ToArray(); + BitmapBits.Set(BitmapPageBits(bitmap, format), targetPage % pagesPerBitmap, set); + _channel.WritePage(bitmapPage, bitmap); + } + + /// Zeroes the bitmap of every dedicated bitmap page a reference-form names, + /// leaving each page's header, and returns them; nothing for an inline record. ACE does this both when it + /// clears the global released-pages map at close and when it retires a dropped object's map — even the table's + /// own owned map, whose bits an inline record keeps. + internal List ClearBitmapPages(ReadOnlySpan record) + { + JetFormatBase format = _channel.Format; + var pages = new List(); + if (RecordType(record) != UsageMapType.Reference) return pages; + for (int slot = 0; slot < format.UsageMapReferenceSlots; slot++) + { + int bitmapPage = ReferencePointer(record, slot, format); + if (bitmapPage == 0) continue; + byte[] bitmap = ReadBitmapPage(_channel, bitmapPage).Span.ToArray(); + BitmapPageBits(bitmap, format).Clear(); + _channel.WritePage(bitmapPage, bitmap); + pages.Add(bitmapPage); + } return pages; } - private static void ValidateReferenceRecord(ReadOnlySpan map) + /// Allocates and initialises an empty dedicated usage-bitmap page (type 0x0105). + private int AllocateBitmapPage() { - if (map.Length != ReferenceMapRecordSize) - throw new InvalidDataException( - $"A reference usage-map record must be exactly {ReferenceMapRecordSize} bytes; got {map.Length}."); + int pageNumber = _channel.Allocator.Allocate(); + _channel.WritePage(pageNumber, NewBitmapPage(_channel.Format)); + return pageNumber; + } + + /// + /// Rewrites an inline usage map as a reference map: every page the inline bitmap marked is re-marked in a + /// dedicated bitmap page, and the record shrinks to the fixed-size pointer table. Bits are grouped by slot + /// so each bitmap page is written once rather than once per page. + /// + private void ConvertInlineToReference(int mapPage, int mapRow) + { + JetFormatBase format = _channel.Format; + (PageBuffer shared, _, DataPage.RowSlot slot) = ReadRecord(_channel, mapRow, mapPage, "Usage-map pointer"); + byte[] page = shared.Span.ToArray(); + + int headerSize = format.UsageMapInlineHeaderSize; + int startPage = StartPage(page.AsSpan(slot.Offset), format); + ReadOnlySpan bitmap = page.AsSpan(slot.Offset + headerSize, slot.Length - headerSize); + + // Every bit kept, as above: this is re-expressing a map the file already holds, not reading one. + var marked = new List(); + BitmapBits.AppendPages(marked, bitmap, startPage, rejectBeyond: null, "An inline usage map"); + + byte[] record = NewReferenceRecord(format); + + int pagesPerBitmap = format.UsageMapPagesPerBitmapPage; + foreach (IGrouping group in marked.GroupBy(p => p / pagesPerBitmap)) + { + if (group.Key >= format.UsageMapReferenceSlots) + throw new NotSupportedException( + $"Page {group.First()} lies past the {format.UsageMapReferenceSlots} bitmap slots a usage map can address (Jet's 2 GB file limit)."); + + int bitmapPage = AllocateBitmapPage(); + byte[] bits = _channel.ReadPage(bitmapPage).Span.ToArray(); + foreach (int ownedPage in group) + BitmapBits.Set(BitmapPageBits(bits, format), ownedPage % pagesPerBitmap, true); + _channel.WritePage(bitmapPage, bits); + WriteReferencePointer(record, group.Key, format, bitmapPage); + } + + // Re-read: allocating bitmap pages above may have grown the file, though not this page. + (PageBuffer fresh, DataPage current, _) = ReadRecord(_channel, mapRow, mapPage, "Usage-map pointer"); + byte[] rewritten = ReplaceMapRecord(fresh.Span.ToArray(), current, format, mapRow, record, out _) + ?? throw new InvalidOperationException("The reference-map record does not fit its usage-map page."); + _channel.WritePage(mapPage, rewritten); } - private ReadOnlySpan ReadBitmapPage(int pageNumber) + /// The bitmap bytes an inline record needs to cover pages from its start page: + /// whole bytes, rounded up to steps. + internal static int InlineBitmapBytes(JetFormatBase format, int pages) { - if (pageNumber <= 0 || pageNumber >= _channel.PageCount) - throw new InvalidDataException($"Usage-map bitmap page {pageNumber} is outside the database."); + int unit = format.UsageMapInlineGrowthSize; + return (BitmapBits.ByteCount(pages) + unit - 1) / unit * unit; + } - ReadOnlySpan page = _channel.ReadPage(pageNumber).Span; - if (PageHeader.ReadType(page) != PageType.PageUsageBitmap || page[2] != 0 || page[3] != 0) - throw new InvalidDataException($"Usage-map pointer {pageNumber} does not reference a valid bitmap page."); - return page[BitmapPageHeaderSize..]; + /// Replaces the usage-map record at with , + /// repacking every record on the page from the end backward — the way Access enlarges a table's owned/free + /// bitmap once it spans past the current window. Records keep their directory order (row 0 nearest the + /// page end), and are laid over the page as it stands: bytes a moved record vacates are not cleared, as ACE + /// leaves them. Returns the rewritten page and the record's new offset, or if the + /// record no longer fits the page. + internal static byte[]? ReplaceMapRecord(byte[] page, DataPage holder, JetFormatBase format, int mapRow, byte[] newRecord, out int newOffset) + { + int rowCount = holder.Rows.Count; + var records = new byte[rowCount][]; + for (int i = 0; i < rowCount; i++) + records[i] = page.AsSpan(holder.Rows[i].Offset, holder.Rows[i].Length).ToArray(); + records[mapRow] = newRecord; + + // Carry each slot's flags across, not just its offset. A usage-map page can hold the deleted + overflow + // tombstone that the index-rebuild recycle deliberately leaves behind (RecycleOwnedMapRow, reproduced + // byte-for-byte from ACE); rebuilding the entry from the offset alone cleared those two bits and turned + // that tombstone back into a live zero-length record. + newOffset = -1; + byte[] result = [.. page]; + if (!DataPage.LayRows(result, format, records, [.. holder.Rows.Select(DataPage.Flags)])) return null; + newOffset = DataPage.ReadSlot(result, format, mapRow).Offset; + return result; } } \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/UsageMapBits.cs b/src/LibRed/LibRed.Core/Storage/UsageMapBits.cs deleted file mode 100644 index 8d91e3db..00000000 --- a/src/LibRed/LibRed.Core/Storage/UsageMapBits.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace LibRed.Storage; - -/// -/// The walk from a usage-map bitmap to the page numbers it marks — bit i of byte n meaning page -/// basePage + n×8 + i (page-05 §9). Every map form reduces to this: an inline record's single bitmap, -/// each bitmap page of a reference record, and a long-value column's maps of either form. -/// -internal static class UsageMapBits -{ - /// - /// Appends the pages marks to . - /// - /// Collects the page numbers, in ascending order. - /// The map's bits, without whatever header carried them. - /// The page bit 0 stands for. - /// - /// The file's page count, to reject a bit naming a page outside it — or null to keep every bit. - /// Which of those is right is a property of the caller, not an oversight. A map read feeds - /// its numbers straight into page reads, so one outside the file is corruption and says so. A map being - /// rewritten must keep bits it cannot currently represent: a free map's window slides with the - /// append tail and can sit above a page it still has to record, so the record widens rather than dropping - /// them (page-05 §9). Rejecting there is not theoretical — it failed an ordinary DROP TABLE on - /// complex1.accdb, whose MSysObjects free map starts at page 2288 while its catalog rows - /// live at page 17. - /// - /// Names the map in the rejection message. - public static void Append( - List pages, ReadOnlySpan bitmap, int basePage, int? rejectBeyond, string what) - { - // The bound is read once by the caller and passed in: this loop runs per set bit on every insert, and - // PageChannel.PageCount used to be a file-length syscall, which made a per-bit test cost a - // non-transactional insert ~1.9x. Nothing here writes, so the count cannot move under it. - for (int i = 0; i < bitmap.Length; i++) - { - byte b = bitmap[i]; - if (b == 0) continue; - for (int bit = 0; bit < 8; bit++) - { - if ((b & (1 << bit)) == 0) continue; - long page = (long)basePage + i * 8 + bit; - if (rejectBeyond is { } pageCount && (page <= 1 || page >= pageCount)) - throw new InvalidDataException( - $"{what} names page {page}, outside the file's 2..{pageCount - 1} range."); - pages.Add((int)page); - } - } - } -} diff --git a/src/LibRed/LibRed.Core/Storage/UsageMapWriter.cs b/src/LibRed/LibRed.Core/Storage/UsageMapWriter.cs deleted file mode 100644 index 45b3b988..00000000 --- a/src/LibRed/LibRed.Core/Storage/UsageMapWriter.cs +++ /dev/null @@ -1,346 +0,0 @@ -using LibRed.Formats; -using LibRed.IO; -using LibRed.Pages; -using System.Buffers.Binary; - -namespace LibRed.Storage; - -/// -/// Writes a usage map — the bitmap recording which pages belong to a table, an index, or a long-value -/// column. The counterpart to , which reads them. -/// -/// -/// A map is a single record on a usage-map page, addressed by a (row, page) pointer in the TDEF. It starts -/// as an inline map (type 0x00: a start page plus a bitmap) and, once its record can no longer -/// grow within its page, is rewritten as a reference map (type 0x01: pointers to dedicated -/// bitmap pages). See §9 of the format spec. -/// -public sealed class UsageMapWriter(PageChannel channel) -{ - // Access grows an inline usage-map bitmap in 32-bit (4-byte) steps. Verified against owned-map record - // lengths on a 255-column ACE table whose data pages start at 353: 8,000 rows → 1053, 12,000 → 1553, - // 30,000 → 3801, i.e. 5 + roundUp(ceil((353 + rows) / 8), 4) exactly. (A 32-byte chunk would give 1056 / - // 1568 / 3808.) Getting this right matters beyond tidiness: overshooting the record length spends the - // usage-map page's remaining room and converts the map to reference type earlier than Access would. - private const int UsageMapChunkBytes = 4; - - private const byte InlineMapType = 0x00; - private const byte ReferenceMapType = 0x01; - private const int InlineMapHeaderSize = 5; // type byte + 4-byte start page - private const int BitmapPageHeaderSize = 4; // type + flags + 2 unused, then the bitmap - - // A reference map's record is a type byte followed by 17 four-byte bitmap-page pointers (69 bytes). - // Seventeen is not arbitrary: each bitmap page covers (pageSize - 4) * 8 = 32,736 pages, so 17 slots - // span ~2.28 GB — just past Jet's 2 GB file ceiling. Verified against an ACE-built 134 MB table. - private const int ReferenceMapSlots = 17; - private const int ReferenceMapRecordSize = 1 + ReferenceMapSlots * 4; - - // A movable inline window is exactly 512 pages (a 64-byte bitmap) aligned to a 512-page boundary. - // Verified against ACE free-pages maps: the sole set bit at page 852/1227/1852/2852 sat in a 64-byte - // record whose startPage was 512/1024/1536/2560 — i.e. floor(page / 512) * 512. - private const int InlineWindowBitmapBytes = 64; - private const int InlineWindowPages = InlineWindowBitmapBytes * 8; - - private readonly PageChannel _channel = channel; - - /// Sets or clears the bit for in the usage map at record - /// on . - /// - /// Handles both map types. An inline map (0x00) is grown in place while its record still fits the page; - /// once it cannot, the map is converted to a reference map (0x01) — exactly Access's own threshold. - /// - /// marks a map whose set bits stay clustered near the append tail — a - /// free-pages map. Rather than growing a bitmap from startPage = 0 all the way out to the tail, - /// such a map slides a fixed 512-page window, as Access does, so its record stays 69 bytes forever. - /// An owned-pages map cannot do this: it must retain every page it has ever taken. - /// - /// - public void SetBit(int mapRow, int mapPage, int targetPage, bool set, bool movableWindow = false) - { - JetFormatBase format = _channel.Format; - byte[] page = _channel.ReadPage(mapPage).Span.ToArray(); - var holder = new DataPage(); - holder.Read(_channel.ReadPage(mapPage), format); - - // mapRow comes from a TDEF pointer, i.e. out of the file. The readers of this same pointer all - // bounds-check it and report corruption; this writer indexed the slot list bare, so a malformed - // pointer escaped as ArgumentOutOfRangeException from a write path instead. - if (mapRow < 0 || mapRow >= holder.RowCount) - throw new InvalidDataException($"Usage-map row {mapPage}:{mapRow} does not exist."); - int mapOffset = holder.Rows[mapRow].Offset; - - if (page[mapOffset] == ReferenceMapType) - { - SetReferenceBit(page, mapPage, mapOffset, targetPage, set); - return; - } - - // Anything that is neither type byte is corruption, not an inline map. Falling through treated it as - // inline and read the next four bytes as a start page — mangling the record in place, so the eventual - // complaint came from a reader at a different call site. Both readers reject an unknown type. - if (page[mapOffset] != InlineMapType) - throw new InvalidDataException( - $"Usage map at {mapPage}:{mapRow} has unknown type 0x{page[mapOffset]:X2}."); - if (holder.Rows[mapRow].Length < InlineMapHeaderSize) - throw new InvalidDataException( - $"Inline usage map at {mapPage}:{mapRow} is {holder.Rows[mapRow].Length} bytes, too short for its header."); - - int startPage = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(mapOffset + 1, 4)); - int bitmapBits = (holder.Rows[mapRow].Length - InlineMapHeaderSize) * 8; - int bitIndex = targetPage - startPage; - - // The inline bitmap covers pages [startPage, startPage + bitmapBits). Clearing a bit outside that - // window is a no-op — it is already 0. - if (bitIndex < 0 || bitIndex >= bitmapBits) - { - if (!set) return; - - // A free-pages map slides its window onto the target instead of growing (and can therefore also - // move *backwards*, which a grown map could never do). - if (movableWindow && TryRepositionWindow(page, holder, format, mapPage, mapRow, targetPage)) - return; - - if (bitIndex < 0) - throw new NotSupportedException( - $"Page {targetPage} is below the usage map's start page {startPage}; this map's window cannot move."); - } - - // When Access needs to mark a page beyond the window it grows the bitmap record in place (still - // type 0x00, same startPage), extending it in 4-byte steps. - if (bitIndex >= bitmapBits) - { - int neededBitmapBytes = RoundUpTo(bitIndex / 8 + 1, UsageMapChunkBytes); - byte[] grownRecord = new byte[InlineMapHeaderSize + neededBitmapBytes]; // extra bitmap bytes stay zero - page.AsSpan(mapOffset, holder.Rows[mapRow].Length).CopyTo(grownRecord); - - byte[]? grown = ReplaceMapRecord(page, holder, format, mapRow, grownRecord, out mapOffset); - if (grown is null) - { - // The record can no longer grow within its page: switch to a reference map and retry there. - ConvertInlineToReference(mapPage, mapRow); - page = _channel.ReadPage(mapPage).Span.ToArray(); - var converted = new DataPage(); - converted.Read(_channel.ReadPage(mapPage), format); - SetReferenceBit(page, mapPage, converted.Rows[mapRow].Offset, targetPage, set); - return; - } - - page = grown; - } - - int byteIndex = mapOffset + InlineMapHeaderSize + bitIndex / 8; - byte mask = (byte)(1 << (bitIndex % 8)); - if (set) page[byteIndex] |= mask; - else page[byteIndex] &= (byte)~mask; - _channel.WritePage(mapPage, page); - } - - /// - /// Slides an inline map's window onto : a 64-byte bitmap starting at - /// floor(targetPage / 512) * 512, carrying over every bit already set and adding the target's. - /// Returns — leaving the map untouched — when some page already marked would - /// fall outside the new window, since moving would silently forget it; the caller then grows instead. - /// - private bool TryRepositionWindow(byte[] page, DataPage holder, JetFormatBase format, int mapPage, int mapRow, int targetPage) - { - RowSlot slot = holder.Rows[mapRow]; - int startPage = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(slot.Offset + 1, 4)); - ReadOnlySpan bitmap = page.AsSpan(slot.Offset + InlineMapHeaderSize, slot.Length - InlineMapHeaderSize); - - int newStart = targetPage / InlineWindowPages * InlineWindowPages; - int newEnd = newStart + InlineWindowPages; - - var marked = new List(); - UsageMapBits.Append(marked, bitmap, startPage, rejectBeyond: null, "A free-pages map"); - bool fitsWindow = marked.TrueForAll(p => p >= newStart && p < newEnd); - - // A window that has already moved above the target cannot slide back down without dropping the pages - // it still advertises, so it widens instead: the start drops to the lowest page it must cover (rounded - // down to a byte, as the released map's move does) and the record is sized to reach the highest. That - // keeps it inline and keeps every bit. Marking a page a table's free map cannot represent is not a - // corruption — the bit only advertises room — but silently dropping it is what leaves reusable space - // invisible, and throwing outright failed an ordinary DROP TABLE on a real file (complex1.accdb, whose - // MSysObjects free map sits at page 2288 while its catalog rows live at page 17). - int bitmapBytes = InlineWindowBitmapBytes; - if (!fitsWindow) - { - int lowest = Math.Min(targetPage, marked.Count == 0 ? targetPage : marked.Min()); - int highest = Math.Max(targetPage, marked.Count == 0 ? targetPage : marked.Max()); - newStart = lowest / 8 * 8; - bitmapBytes = RoundUpTo((highest - newStart) / 8 + 1, UsageMapChunkBytes); - } - - var record = new byte[InlineMapHeaderSize + bitmapBytes]; - record[0] = InlineMapType; - BinaryPrimitives.WriteInt32LittleEndian(record.AsSpan(1, 4), newStart); - marked.Add(targetPage); - foreach (int markedPage in marked) - { - int bit = markedPage - newStart; - record[InlineMapHeaderSize + bit / 8] |= (byte)(1 << (bit % 8)); - } - - byte[]? rewritten = ReplaceMapRecord(page, holder, format, mapRow, record, out _); - if (rewritten is null) return false; // shrinking or same size, so effectively unreachable - - _channel.WritePage(mapPage, rewritten); - return true; - } - - /// Number of pages one dedicated bitmap page (type 0x0105) covers. - private int PagesPerBitmapPage => (_channel.Format.PageSize - BitmapPageHeaderSize) * 8; - - /// Sets or clears 's bit in a reference map: pointer slot - /// targetPage / PagesPerBitmapPage names the bitmap page holding it. A slot's bitmap page is - /// allocated lazily, only when a bit in its range is first set. - private void SetReferenceBit(byte[] page, int mapPage, int mapOffset, int targetPage, bool set) - { - int slot = targetPage / PagesPerBitmapPage; - if (slot >= ReferenceMapSlots) - throw new NotSupportedException( - $"Page {targetPage} lies past the {ReferenceMapSlots} bitmap slots a usage map can address (Jet's 2 GB file limit)."); - - int pointerOffset = mapOffset + 1 + slot * 4; - int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(pointerOffset, 4)); - if (bitmapPage == 0) - { - if (!set) return; // the bit is already clear — no need to materialize the bitmap page - bitmapPage = AllocateBitmapPage(); - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(pointerOffset, 4), bitmapPage); - _channel.WritePage(mapPage, page); - } - - SetBitmapPageBit(bitmapPage, targetPage % PagesPerBitmapPage, set); - } - - /// Flips one bit on a dedicated bitmap page, after proving the page IS one. - /// - /// The pointer comes out of the map record, i.e. out of the file. All three readers of this same pointer - /// verify the page is in range and carries the complete [05 01 00 00] bitmap header before trusting - /// it — the spec states that validation as mandatory "before any bitmap is expanded into page numbers". - /// The writer skipped both, so a stale or corrupt in-range pointer had it OR a bit into an ordinary data, - /// TDEF or index page and write it back. The readers then reject that same pointer, meaning the damage - /// landed on a different page from the one eventually diagnosed. - /// - private void SetBitmapPageBit(int bitmapPage, int bit, bool set) - { - if (bitmapPage <= 1 || bitmapPage >= _channel.PageCount) - throw new InvalidDataException( - $"Usage map names bitmap page {bitmapPage}, outside the file's 2..{_channel.PageCount - 1} range."); - - byte[] bitmap = _channel.ReadPage(bitmapPage).Span.ToArray(); - if (PageHeader.ReadType(bitmap) != PageType.PageUsageBitmap || bitmap[2] != 0 || bitmap[3] != 0) - throw new InvalidDataException( - $"Page {bitmapPage} is named as a usage bitmap but does not carry the [05 01 00 00] header."); - - int byteIndex = BitmapPageHeaderSize + bit / 8; - byte mask = (byte)(1 << (bit % 8)); - if (set) bitmap[byteIndex] |= mask; - else bitmap[byteIndex] &= (byte)~mask; - _channel.WritePage(bitmapPage, bitmap); - } - - /// Allocates and initialises an empty dedicated usage-bitmap page (type 0x0105). - private int AllocateBitmapPage() - { - int pageNumber = new PageAllocator(_channel).Allocate(); - var bitmap = new byte[_channel.Format.PageSize]; - PageHeader.WriteType(bitmap, PageType.PageUsageBitmap); - _channel.WritePage(pageNumber, bitmap); - return pageNumber; - } - - /// - /// Rewrites an inline (0x00) usage map as a reference (0x01) map: every page the inline bitmap marked is - /// re-marked in a dedicated bitmap page, and the record shrinks to the fixed 69-byte pointer table. Bits - /// are grouped by slot so each bitmap page is written once rather than once per page. - /// - private void ConvertInlineToReference(int mapPage, int mapRow) - { - JetFormatBase format = _channel.Format; - byte[] page = _channel.ReadPage(mapPage).Span.ToArray(); - var holder = new DataPage(); - holder.Read(_channel.ReadPage(mapPage), format); - RowSlot slot = holder.Rows[mapRow]; - - int startPage = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(slot.Offset + 1, 4)); - ReadOnlySpan bitmap = page.AsSpan(slot.Offset + InlineMapHeaderSize, slot.Length - InlineMapHeaderSize); - - // Every bit kept, as above: this is re-expressing a map the file already holds, not reading one. - var marked = new List(); - UsageMapBits.Append(marked, bitmap, startPage, rejectBeyond: null, "An inline usage map"); - - var record = new byte[ReferenceMapRecordSize]; - record[0] = ReferenceMapType; - - foreach (IGrouping group in marked.GroupBy(p => p / PagesPerBitmapPage)) - { - if (group.Key >= ReferenceMapSlots) - throw new NotSupportedException( - $"Page {group.First()} lies past the {ReferenceMapSlots} bitmap slots a usage map can address (Jet's 2 GB file limit)."); - - int bitmapPage = AllocateBitmapPage(); - byte[] bits = _channel.ReadPage(bitmapPage).Span.ToArray(); - foreach (int ownedPage in group) - { - int bit = ownedPage % PagesPerBitmapPage; - bits[BitmapPageHeaderSize + bit / 8] |= (byte)(1 << (bit % 8)); - } - _channel.WritePage(bitmapPage, bits); - BinaryPrimitives.WriteInt32LittleEndian(record.AsSpan(1 + group.Key * 4, 4), bitmapPage); - } - - // Re-read: allocating bitmap pages above may have grown the file, though not this page. - byte[] fresh = _channel.ReadPage(mapPage).Span.ToArray(); - var current = new DataPage(); - current.Read(_channel.ReadPage(mapPage), format); - byte[] rewritten = ReplaceMapRecord(fresh, current, format, mapRow, record, out _) - ?? throw new InvalidOperationException("The reference-map record does not fit its usage-map page."); - _channel.WritePage(mapPage, rewritten); - } - - private static int RoundUpTo(int value, int unit) => (value + unit - 1) / unit * unit; - - /// Replaces the usage-map record at with , - /// repacking every record on the page from the end backward — the way Access enlarges a table's owned/free - /// bitmap once it spans past the current window. Records keep their directory order (row 0 nearest the - /// page end). Returns the rewritten page and the record's new offset, or if the - /// record no longer fits the page. - internal static byte[]? ReplaceMapRecord(byte[] page, DataPage holder, JetFormatBase format, int mapRow, byte[] newRecord, out int newOffset) - { - int rowCount = holder.Rows.Count; - var records = new byte[rowCount][]; - for (int i = 0; i < rowCount; i++) - records[i] = page.AsSpan(holder.Rows[i].Offset, holder.Rows[i].Length).ToArray(); - records[mapRow] = newRecord; - - // Check the fit *before* laying anything out: the records are packed from the page end backward, so - // an oversized record would otherwise run past offset 0 mid-copy rather than reporting "doesn't fit". - newOffset = -1; - int directoryEnd = format.DataRowDirectoryOffset + rowCount * 2; - if (format.PageSize - records.Sum(r => r.Length) < directoryEnd) return null; - - var result = new byte[format.PageSize]; - Array.Copy(page, result, format.DataRowDirectoryOffset); // preserve type/flags/owner/header - int offset = format.PageSize; - for (int i = 0; i < rowCount; i++) - { - offset -= records[i].Length; - Array.Copy(records[i], 0, result, offset, records[i].Length); - // Carry each slot's flags across, not just its offset. A usage-map page can hold the deleted + - // overflow tombstone that the index-rebuild recycle deliberately leaves behind (RecycleOwnedMapRow, - // reproduced byte-for-byte from ACE); rebuilding the entry from the offset alone cleared those two - // bits and turned that tombstone back into a live zero-length record. The data-page repacker in - // RowInserter preserves them explicitly for the same reason. - ushort slotFlags = (ushort)((holder.Rows[i].IsDeleted ? RowPointer.DeletedFlag : 0) - | (holder.Rows[i].HasOverflow ? RowPointer.OverflowFlag : 0)); - BinaryPrimitives.WriteUInt16LittleEndian(result.AsSpan(format.DataRowDirectoryOffset + i * 2, 2), - (ushort)(slotFlags | (offset & RowPointer.OffsetMask))); - if (i == mapRow) newOffset = offset; - } - - BinaryPrimitives.WriteUInt16LittleEndian(result.AsSpan(format.DataRowCountOffset, 2), (ushort)rowCount); - BinaryPrimitives.WriteUInt16LittleEndian(result.AsSpan(format.DataFreeSpaceOffset, 2), (ushort)(offset - directoryEnd)); - return result; - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs deleted file mode 100644 index 2a8f3b13..00000000 --- a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs +++ /dev/null @@ -1,280 +0,0 @@ -using LibRed.Catalog; -using LibRed.Formats; -using LibRed.IO; -using System.Buffers.Binary; - -namespace LibRed.Storage; - -/// -/// Creates a view (a stored SELECT query) the way Access does — an MSysObjects row of type 5 with -/// a negative synthetic id, plus the query decomposed into MSysQueries rows (one per column / table -/// / join / where, bracketed by a type row and an end row). Verified byte-faithful against ACE for the -/// "simple SELECT" views a view is allowed to contain. -/// -public sealed class ViewCreator(PageChannel channel, JetCatalog catalog) -{ - // MSysObjects.Flags for a stored query: 0x10000000 plus the kind, and the kind byte is DAO's own - // QueryDef.Type value (verified vs ACE for all six: crosstab 16, delete 32, update 48, append 64, - // make-table 80, data-definition 96) — not the Flag the MSysQueries action row carries, which numbers - // the kinds differently. - private const int ViewFlags = 0x10000000; // a SELECT query / view (DAO type 0) - private const int DeleteFlags = 0x10000020; - private const int UpdateFlags = 0x10000030; - private const int AppendFlags = 0x10000040; // an INSERT (append) query - private const int MakeTableFlags = 0x10000050; - private const int DataDefinitionFlags = 0x10000060; // a CREATE/DROP TABLE (data-definition) query - - private readonly PageChannel _channel = channel; - private readonly JetCatalog _catalog = catalog; - - public void Create(string name, ViewSpec spec) - { - int objectId = AllocateQueryObject(name, ViewFlags); - AddQueryRows(objectId, spec); - } - - /// Persists a stored action query (a non-SELECT CREATE PROCEDURE body) byte-faithfully. - public void CreateAction(string name, ActionQuerySpec spec) - { - int flags = spec.Kind switch - { - ActionQueryKind.DataDefinition => DataDefinitionFlags, - ActionQueryKind.Append => AppendFlags, - ActionQueryKind.Update => UpdateFlags, - ActionQueryKind.Delete => DeleteFlags, - ActionQueryKind.MakeTable => MakeTableFlags, - _ => throw new NotSupportedException($"Action query kind {spec.Kind} is not stored yet."), - }; - int objectId = AllocateQueryObject(name, flags); - AddActionRows(objectId, spec); - } - - /// - /// Records a relationship as ACE does (verified): a type-8 MSysObjects object in the Relationships - /// container, named after it, flags 0, with the next high-bit id — the sequence queries draw from, so the two - /// interleave, and a dropped one's id is taken again — and its two MSysACEs rows. Refuses a name - /// another relationship has, as ACE does; a table or query may share it. - /// - public void CreateRelationshipObject(string name) => - AllocateObject(name, CatalogFormat.ObjectTypeRelationship, CatalogFormat.RelationshipContainerParentId, flags: 0); - - private int AllocateQueryObject(string name, int flags) - { - JetName.Validate(name, "query name"); - return AllocateObject(name, StoredQueryFormat.ObjectTypeQuery, CatalogFormat.ObjectContainerParentId, flags); - } - - /// Reserves the next free high-bit object id, checks the name is free, and writes - /// the MSysObjects row and the MSysACEs rows its container grants it. For a query the - /// distinguish view / append / data-definition. - private int AllocateObject(string name, short type, int parentId, int flags) - { - TableDef msysObjects = _catalog.RequireTable("MSysObjects"); - int idIndex = msysObjects.RequireColumn("Id").Index; - int nameIndex = msysObjects.RequireColumn("Name").Index; - int parentIndex = msysObjects.RequireColumn("ParentId").Index; - - // A name must be free within the object's own container — the rule MSysObjects' unique (ParentId, Name) - // index states, and the one ACE applies (measured): a query collides with a table, a query or a linked - // table, all in the Tables container, and not with a form, report, macro, module, relationship, database - // document or container; a relationship only with another relationship. Find the next free negative id - // (they increment from 0x80000000) in the same scan. - bool relationship = parentId == CatalogFormat.RelationshipContainerParentId; - int nextId = unchecked((int)0x80000000); - var objects = new Table(_channel, msysObjects); - foreach (object?[] row in objects.Rows(objects.DecodeOnly([idIndex, nameIndex, parentIndex]))) - { - if (row[parentIndex] is int parent && parent == parentId - && string.Equals(row[nameIndex] as string, name, StringComparison.OrdinalIgnoreCase)) - throw new SchemaObjectExistsException(relationship - ? $"There is already a relationship named '{name}' in the current database." - : $"Object '{name}' already exists.", name); - if (row[idIndex] is int id && id < 0 && id >= nextId) nextId = id + 1; - } - - var writer = new CatalogWriter(_channel, _catalog); - writer.AddObjectRow(name, nextId, type, parentId, flags); - writer.AddPermissionRows(nextId, parentId); - return nextId; - } - - private void AddActionRows(int objectId, ActionQuerySpec spec) - { - TableDef mq = _catalog.RequireTable("MSysQueries"); - - Row(mq, objectId, StoredQueryFormat.AttrType, order: 1, flag: StoredQueryFormat.QueryTypeSelect); - Row(mq, objectId, StoredQueryFormat.AttrEnd, order: 1); - AddParameterRows(mq, objectId, spec.Parameters); - - if (spec.Kind == ActionQueryKind.DataDefinition) - { - // The whole DDL statement is stored verbatim in one row; Access records it with a leading space. - // Nothing is decomposed: a data-definition query has no sources, columns or predicate. - Row(mq, objectId, StoredQueryFormat.AttrOperation, order: 1, flag: StoredQueryFormat.ActionDdl, - expression: " " + spec.DdlSql); - return; - } - - // The action row carries the kind, and the target table for the two kinds that write into one. - short kind = spec.Kind switch - { - ActionQueryKind.Append => StoredQueryFormat.ActionAppend, - ActionQueryKind.Update => StoredQueryFormat.ActionUpdate, - ActionQueryKind.Delete => StoredQueryFormat.ActionDelete, - ActionQueryKind.MakeTable => StoredQueryFormat.ActionMakeTable, - _ => throw new NotSupportedException($"Action query kind {spec.Kind} is not stored yet."), - }; - Row(mq, objectId, StoredQueryFormat.AttrOperation, order: 1, flag: kind, - name1: spec.Kind is ActionQueryKind.Append or ActionQueryKind.MakeTable ? spec.TargetTable : null); - - // Sources first: Access processes the rows in order, and a derived-table source defines an alias the - // column expressions reference (the same reason the view path writes tables before columns). - AddSourceRows(mq, objectId, spec.Body); - - var values = spec.Values ?? []; - switch (spec.Kind) - { - case ActionQueryKind.Append: - // Name2 = target column, Expression = the value; the 0x8000 flag marks an INSERT … VALUES, - // where a column fed by the query's own source carries Flag 0. - for (int i = 0; i < values.Count; i++) - Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, - flag: spec.Body is null ? StoredQueryFormat.AppendValueFlag : (short)0, - expression: values[i].ValueExpression, name2: values[i].Column); - break; - - case ActionQueryKind.Update: - // One row per SET assignment, stored exactly as an append's columns are. - for (int i = 0; i < values.Count; i++) - Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, flag: 0, - expression: values[i].ValueExpression, name2: values[i].Column); - break; - - case ActionQueryKind.Delete: - // `DELETE t.* FROM …` keeps that target verbatim in a single column row; `DELETE FROM …` - // stores no column row at all, and Access renders it back as `DELETE * FROM …`. - if (spec.DeleteTarget is { } target) - Row(mq, objectId, StoredQueryFormat.AttrColumn, order: 1, flag: 0, expression: target); - break; - - case ActionQueryKind.MakeTable: - // An ordinary output list: Expression = the column, Name1 = its alias. - var columns = spec.Body?.Columns ?? []; - for (int i = 0; i < columns.Count; i++) - Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, flag: 0, - expression: columns[i].Expression, name1: columns[i].Alias); - break; - } - - AddJoinAndWhereRows(mq, objectId, spec.Body); - // The option row a view carries: a make-table or append query's DISTINCT and TOP, and any kind's WITH - // OWNERACCESS OPTION. - AddOptionRow(mq, objectId, spec.Body, spec.OwnerAccess); - } - - /// - /// The query's one 0x03 option row, when it has an option: DISTINCT, WITH OWNERACCESS OPTION, TOP and PERCENT - /// are bits of the same row, the TOP count in its Name1 (verified vs ACE: DISTINCT TOP is 18, with the option 22, - /// and with PERCENT too 54; the option alone is 4; a make-table query's DISTINCT is the same row as a view's). - /// - private void AddOptionRow(TableDef mq, int objectId, ViewSpec? body, bool ownerAccess) - { - short options = (short)((body?.Distinct == true ? StoredQueryFormat.FlagDistinct : 0) - | (ownerAccess ? StoredQueryFormat.FlagOwnerAccess : 0) - | (body?.Top is null ? 0 : StoredQueryFormat.FlagTop | (body.TopPercent ? StoredQueryFormat.FlagPercent : 0))); - if (options != 0) - Row(mq, objectId, StoredQueryFormat.AttrOption, order: 1, flag: options, - name1: body?.Top?.ToString(System.Globalization.CultureInfo.InvariantCulture)); - } - - /// The 0x02 parameter rows, in declaration order — written identically for a view and for - /// an action query. - private void AddParameterRows(TableDef mq, int objectId, IReadOnlyList? parameters) - { - for (int i = 0; i < (parameters?.Count ?? 0); i++) - { - ViewParameterSpec p = parameters![i]; - Row(mq, objectId, StoredQueryFormat.AttrParameter, order: i + 1, - flag: p.TypeCode, name1: p.Name, - lvExtra: StoredQueryFormat.PackParameterFacets((JetDataType)p.TypeCode, p.Size, p.Scale)); - } - } - - /// The 0x05 FROM rows: a named table in Name1 (alias in Name2), or a derived table whose - /// subquery SQL goes in Expression with Name1 empty. - private void AddSourceRows(TableDef mq, int objectId, ViewSpec? body) - { - var tables = body?.Tables ?? []; - for (int i = 0; i < tables.Count; i++) - { - ViewTableSpec t = tables[i]; - if (t.SubquerySql is { } sub) - Row(mq, objectId, StoredQueryFormat.AttrTable, order: i + 1, expression: sub, name2: t.Alias); - else - Row(mq, objectId, StoredQueryFormat.AttrTable, order: i + 1, name1: t.Table, name2: t.Alias); - } - } - - /// The 0x07 join rows (condition, kind, and the two tables the condition names) and the - /// single 0x08 WHERE row. - private void AddJoinAndWhereRows(TableDef mq, int objectId, ViewSpec? body) - { - var joins = body?.Joins ?? []; - for (int i = 0; i < joins.Count; i++) - { - ViewJoinSpec j = joins[i]; - Row(mq, objectId, StoredQueryFormat.AttrJoin, order: i + 1, flag: (short)j.Kind, - expression: j.Condition, name1: j.LeftAlias, name2: j.RightAlias); - } - if (body?.Where is { } where) - Row(mq, objectId, StoredQueryFormat.AttrWhere, order: 1, expression: where); - } - - private void AddQueryRows(int objectId, ViewSpec spec) - { - TableDef mq = _catalog.RequireTable("MSysQueries"); - - // ACE's row order (verified against every Northwind view): type, end, distinct, TABLES, COLUMNS, - // joins, where. Tables must precede columns — a derived-table source defines an alias that the - // column expressions reference, and Access processes the rows in order, so columns-before-tables - // makes it fail to run the view (it opens, but SELECT-from-view errors). Order fields are - // per-attribute counters, independent of this insertion order. - Row(mq, objectId, StoredQueryFormat.AttrType, order: 1, flag: StoredQueryFormat.QueryTypeSelect); - Row(mq, objectId, StoredQueryFormat.AttrEnd, order: 1); - // Declared parameters (CREATE PROCEDURE) come right after the End row, before the tables. - AddParameterRows(mq, objectId, spec.Parameters); - AddOptionRow(mq, objectId, spec, spec.OwnerAccess); - AddSourceRows(mq, objectId, spec); - for (int i = 0; i < spec.Columns.Count; i++) - Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, flag: 0, - expression: spec.Columns[i].Expression, name1: spec.Columns[i].Alias); - AddJoinAndWhereRows(mq, objectId, spec); - for (int i = 0; i < (spec.GroupBy?.Count ?? 0); i++) - Row(mq, objectId, StoredQueryFormat.AttrGroupBy, order: i + 1, flag: 0, expression: spec.GroupBy![i]); - // The group filter carries no flag of its own, exactly as the WHERE row doesn't. - if (spec.Having is { } having) - Row(mq, objectId, StoredQueryFormat.AttrHaving, order: 1, expression: having); - for (int i = 0; i < (spec.OrderBy?.Count ?? 0); i++) - Row(mq, objectId, StoredQueryFormat.AttrOrderBy, order: i + 1, expression: spec.OrderBy![i].Expression, - name1: spec.OrderBy[i].Descending ? "d" : null); - } - - private void Row(TableDef mq, int objectId, byte attribute, int order, - short? flag = null, string? expression = null, string? name1 = null, string? name2 = null, - int? lvExtra = null) - { - var values = new object?[mq.Columns.Count]; - CatalogWriter.Set(mq, values, "ObjectId", objectId); - CatalogWriter.Set(mq, values, "Attribute", attribute); - var orderBytes = new byte[4]; - BinaryPrimitives.WriteInt32BigEndian(orderBytes, order); // 4-byte big-endian per-attribute counter - CatalogWriter.Set(mq, values, "Order", orderBytes); - if (flag is { } f) CatalogWriter.Set(mq, values, "Flag", f); - if (expression is not null) CatalogWriter.Set(mq, values, "Expression", expression); - if (name1 is not null) CatalogWriter.Set(mq, values, "Name1", name1); - if (name2 is not null) CatalogWriter.Set(mq, values, "Name2", name2); - // A declared parameter's length; ACE writes it here and renders the PARAMETERS clause from it. - if (lvExtra is { } extra) CatalogWriter.Set(mq, values, "LvExtra", extra); - new RowInserter(_channel, mq).Insert(values, updateIndexes: true); - } -} \ No newline at end of file diff --git a/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedDatabaseModelFactory.cs b/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedDatabaseModelFactory.cs index 5a900f55..bf8c11c3 100644 --- a/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedDatabaseModelFactory.cs +++ b/src/LibRed/LibRed.EFCore/Scaffolding/Internal/LibRedDatabaseModelFactory.cs @@ -72,7 +72,7 @@ private List GetTables(JetDatabase database, DatabaseModel databa { var tables = new List(); - foreach (TableDef definition in database.Catalog.UserTables) + foreach (TableDefinition definition in database.Catalog.UserTables) { _logger.TableFound(definition.Name); @@ -95,7 +95,7 @@ private void GetColumns(JetDatabase database, IReadOnlyList table { foreach (DatabaseTable table in tables) { - TableDef definition = database.Catalog.FindTable(table.Name)!; + TableDefinition definition = database.Catalog.FindTable(table.Name)!; for (int ordinal = 0; ordinal < definition.Columns.Count; ordinal++) { @@ -163,7 +163,7 @@ private void GetIndexes(JetDatabase database, IReadOnlyList table { foreach (DatabaseTable table in tables) { - TableDef definition = database.Catalog.FindTable(table.Name)!; + TableDefinition definition = database.Catalog.FindTable(table.Name)!; foreach (IndexDef index in definition.Indexes) { diff --git a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDatabaseCreator.cs b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDatabaseCreator.cs index 1e24bab6..9ebd07be 100644 --- a/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDatabaseCreator.cs +++ b/src/LibRed/LibRed.EFCore/Storage/Internal/LibRedDatabaseCreator.cs @@ -20,7 +20,7 @@ public class LibRedDatabaseCreator( // 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: DatabaseCreator.CreateEmpty synthesises the file from scratch - no DAO/ADOX). + // remarks: JetDatabase.Create synthesises the file from scratch - no DAO/ADOX). public override void Create() => LibRedConnection.CreateDatabase(relationalConnection.DbConnection.ConnectionString); diff --git a/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs b/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs index 252932b5..c4a2d505 100644 --- a/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs +++ b/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs @@ -1,6 +1,7 @@ using LibRed.Catalog; using LibRed.Formats; using LibRed.Sql.Ast; +using LibRed.Storage; namespace LibRed.Engine.Execution; @@ -152,7 +153,7 @@ private static ColumnSpec MapType(ColumnDefinition column, JetVersion version) // VARIABLE region (verified: every GUID column ACE's DDL creates reads back fixed=False, at 1, // 2, 10, 250 and 252 columns alike — it is not a fallback for wide tables, and SELECT INTO // agrees). ACE's own system tables are the exception: MSysComplexType_GUID.Value is fixed, and - // DatabaseCreator reproduces that. ACE reads either layout back correctly, so this is about + // JetDatabase reproduces that. ACE reads either layout back correctly, so this is about // matching what ACE writes; it also stops a GUID column spending fixed-record budget ACE does // not spend, which made a 252-GUID table ACE creates happily exceed the declared record cap. "GUID" or "UNIQUEIDENTIFIER" @@ -183,7 +184,7 @@ private static ColumnSpec MapType(ColumnDefinition column, JetVersion version) // Up to 4000 bytes, stored inline like VARBINARY under its own type code, and bare BIGBINARY takes // the maximum (verified vs ACE). There is no fixed-length form in DDL. "BIGBINARY" - => Binary(column, isFixed: false, JetDataType.BigBinary, RecordLayout.MaxBigBinaryBytes), + => Binary(column, isFixed: false, JetDataType.BigBinary, RowCodec.MaxBigBinaryBytes), // Long-value columns: variable-length with no fixed byte length. The in-row value is a // 12-byte long-value descriptor; short values are stored inline after it. diff --git a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs index 501c4667..f4f7e207 100644 --- a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs +++ b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs @@ -2991,7 +2991,7 @@ private static int Compare(object left, object right, JetTextComparer text) // Binary (byte[]) columns: structural, length-sensitive byte compare — lexicographic then by // length, so a shorter value sorts before a longer one sharing its prefix (Jet's binary order, - // matching IndexKeyEncoder). Without this, byte[] falls through to ToString() ("System.Byte[]" + // matching IndexKeyCodec). Without this, byte[] falls through to ToString() ("System.Byte[]" // for every array) and all binaries compare *equal* — so `WHERE binKey = @p` matches every row. if (left is byte[] lb && right is byte[] rb) return CompareBytes(lb, rb); @@ -3005,7 +3005,7 @@ private static int Compare(object left, object right, JetTextComparer text) // serial and therefore puts later pre-epoch times first (verified in // LibRed.Core.Tests.AcePreEpochDateProbeTest: `06:00 < 18:00` is False, ORDER BY gives 1,3,2,4,5,6). // - // Matching it is not only about ACE parity: IndexKeyEncoder writes this same serial as the index key, + // Matching it is not only about ACE parity: IndexKeyCodec writes this same serial as the index key, // and that encoding cannot change because ACE writes those keys too. Comparing chronologically here // while the index compares by serial made an index seek and a table scan return DIFFERENT rows for a // pre-epoch range (see PreEpochDateOrderingTests). From the epoch onward the two orders are identical, diff --git a/src/LibRed/LibRed.Engine/Execution/ParameterBag.cs b/src/LibRed/LibRed.Engine/Execution/ParameterBag.cs index e6304bef..f3347867 100644 --- a/src/LibRed/LibRed.Engine/Execution/ParameterBag.cs +++ b/src/LibRed/LibRed.Engine/Execution/ParameterBag.cs @@ -1,3 +1,5 @@ +using LibRed.Storage.Types; + namespace LibRed.Engine.Execution; /// @@ -13,9 +15,6 @@ namespace LibRed.Engine.Execution; /// internal sealed class ParameterBag { - /// The OLE epoch: Jet stores a time as the epoch plus the time of day. - private static readonly DateTime OleEpoch = new(1899, 12, 30); - // IDE0028's only fix here is `[]`, which would silently drop the comparer and make parameter // lookup case-sensitive. #pragma warning disable IDE0028 @@ -36,8 +35,8 @@ public ParameterBag(IReadOnlyDictionary? values) if (_values.TryGetValue(Normalize(name), out object? value)) return value switch { - TimeSpan span => OleEpoch + span, - TimeOnly time => OleEpoch + time.ToTimeSpan(), + TimeSpan span => JetTypeCodec.OleEpoch + span, + TimeOnly time => JetTypeCodec.OleEpoch + time.ToTimeSpan(), _ => value, }; throw new InvalidOperationException($"No value was supplied for parameter '{name}'."); diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 901f1c4c..6a9c2b1b 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -135,7 +135,7 @@ private static void EnforceRequired(string table, IReadOnlyList colum /// row. A CHECK is violated only when its expression is explicitly FALSE; NULL/unknown passes (SQL /// three-valued CHECK semantics). The expression may reference the row's own columns and use (uncorrelated) /// subqueries. Matches Access's validation-rule enforcement. - private void EnforceCheckConstraints(TableDef definition, object?[] values) + private void EnforceCheckConstraints(TableDefinition definition, object?[] values) { if (definition.CheckConstraints.Count == 0) return; var schema = definition.Columns.Select(c => OutputColumn.Of(definition.Name, c)).ToList(); @@ -441,46 +441,7 @@ private void CascadeDelete(Table rootTable, IEnumerable<(RowId Id, object?[] Val } foreach (var (table, id, values) in order) - DeleteRow(table, id, values); - } - - /// Deletes one row — the values its complex columns hold, its index entries, then the row itself. - /// The one place a row is removed: a statement's own rows, a cascade's, and a complex column's values. - private void DeleteRow(Table table, RowId id, object?[] values) - { - DeleteComplexValues(table, values); - foreach (IndexDef index in table.Definition.RealIndexes) - table.RemoveIndexEntry(index, values, id); - table.Delete(id); - } - - /// - /// Removes the values every complex (multi-value / attachment) column of holds - /// for the row being deleted — the flat-table rows carrying that record's complex id. - /// - /// - /// Measured against ACE (ComplexDeleteCascadeProbeTest): deleting a record removes its values from - /// every complex column of the table — a record holding three attachments in one column and two in - /// another loses all five — and rolls no counter back. The owner's 0x1C and each flat - /// table's 0x14 keep their values, so the next row still takes the following id and the freed - /// value ids are never reissued. Leaving the rows behind would orphan values no record points at. - /// - private void DeleteComplexValues(Table table, object?[] values) - { - foreach (ComplexColumn complex in _database.Catalog.ComplexColumns) - { - if (!string.Equals(complex.OwnerTable.Name, table.Name, StringComparison.OrdinalIgnoreCase)) continue; - if (values[complex.OwnerTable.FindColumn(complex.ColumnName)!.Index] is not { } raw) continue; - int recordId = Convert.ToInt32(raw, System.Globalization.CultureInfo.InvariantCulture); - - Table flat = _database.OpenTable(complex.FlatTable.Name); - // Seeked by the owner link, whose index is what identifies it (see JetCatalog). - int linkColumn = complex.OwnerLink.Index; - foreach ((RowId flatId, object?[] flatValues) in flat.RowsWithKey([linkColumn], [recordId], values => - values[linkColumn] is { } link - && Convert.ToInt32(link, System.Globalization.CultureInfo.InvariantCulture) == recordId).ToList()) - DeleteRow(flat, flatId, flatValues); - } + table.Delete(id); } /// Applies each enforced relationship's ON DELETE action to a parent row about to be deleted: @@ -638,9 +599,6 @@ private void UpdateRow(Table table, RowId id, object?[] original, object?[] valu EnforceCheckConstraints(table.Definition, values); table.Update(id, values, changed); - foreach (IndexDef index in table.Definition.RealIndexes - .Where(i => i.Columns.Any(c => changed.Contains(c.Column.Index)))) - table.MoveIndexEntry(index, original, values, id); stillToWrite?.Remove((table.Definition.DefinitionPage, id)); // Parent side, once the row holds its new key: a changed referenced-key column triggers each relationship's @@ -1066,8 +1024,7 @@ 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) + if (_database.Catalog.FindTable(table) is { } def) foreach (ColumnDef column in def.Columns) available.TryAdd(column.Name, column); @@ -1127,7 +1084,7 @@ private sealed record RowDefaults(List<(int Index, Expression Expression)> Colum /// inserter (sequential counter, or a random Int32 for a GenUniqueID() "Random" AutoNumber), not by evaluating /// the DefaultValue — and GenUniqueID() is not a callable expression, so parsing it as a default would fail. /// - private RowDefaults DefaultsOf(TableDef definition) => new( + private RowDefaults DefaultsOf(TableDefinition definition) => new( definition.Columns .Where(c => c.DefaultValue is not null && !c.IsAutoNumber) .Select(c => (c.Index, Expression: ParseDefaultExpression(c.DefaultValue!))) @@ -1828,7 +1785,7 @@ private static (IndexDef Index, Expression Key)? SeekPlanFor(int i, List earlier = tables.Take(i).Select(t => t.Alias).ToHashSet(StringComparer.OrdinalIgnoreCase); @@ -1848,7 +1805,7 @@ private static IEnumerable Conjuncts(Expression e) => : [e]; private static (IndexDef Index, Expression Key)? MatchSeek( - Expression colSide, Expression keySide, string alias, TableDef def, HashSet earlier) + Expression colSide, Expression keySide, string alias, TableDefinition def, HashSet earlier) { if (colSide is not ColumnReference c || (c.Table is { } t && !string.Equals(t, alias, StringComparison.OrdinalIgnoreCase)) @@ -2061,4 +2018,4 @@ private int Delete(DeleteStatement statement) if (_session is not null) _session.RowCount = affected; return affected; } -} +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs b/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs index d006a4be..0eead233 100644 --- a/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs +++ b/src/LibRed/LibRed.Engine/Planning/ColumnPruning.cs @@ -97,9 +97,9 @@ FilterNode or SortNode or LimitNode or DerivedTableNode or WindowNode }; /// The decode mask for from the names a read may use — by - /// , as RowDecoder takes it — or null to decode every + /// , as RowCodec takes it — or null to decode every /// column, when there are no names or every column is among them. - internal static bool[]? Mask(LibRed.Catalog.TableDef table, IReadOnlySet? names) + internal static bool[]? Mask(LibRed.Catalog.TableDefinition table, IReadOnlySet? names) { if (names is null) return null; var mask = new bool[table.Columns.Count]; @@ -180,4 +180,4 @@ [.. type.GetProperties(BindingFlags.Public | BindingFlags.Instance) Linq.Expression read = Linq.Expression.Property(Linq.Expression.Convert(instance, property.DeclaringType!), property); return Linq.Expression.Lambda>(Linq.Expression.Convert(read, typeof(object)), instance).Compile(); } -} +} \ No newline at end of file diff --git a/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs b/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs index a6e63cbe..f321fbde 100644 --- a/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs +++ b/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs @@ -181,7 +181,7 @@ private static FilterNode RewriteFilterOverScan(FilterNode filter, ScanNode scan /// If is a range comparison of column against /// a value (either orientation), returns the operator as if the column were on the left (so 5 < K /// yields K > 5) and the value expression; else null. - private static (BinaryOperator Op, Expression Value)? Bound(Expression conjunct, string colName, string alias, TableDef def) + private static (BinaryOperator Op, Expression Value)? Bound(Expression conjunct, string colName, string alias, TableDefinition def) { if (conjunct is not BinaryExpression { Operator: var op } cmp || op is not (BinaryOperator.GreaterThan or BinaryOperator.GreaterThanOrEqual @@ -199,7 +199,7 @@ private static (BinaryOperator Op, Expression Value)? Bound(Expression conjunct, /// BETWEEN takes its bounds in either order, so which is the lower has to be known when planning. That /// limits this to two literals of the same kind: numbers, texts or dates. A Null bound makes the whole test Null, /// which a seek has no way to say, so it is left to the filter. - private static (Expression Low, Expression High)? Between(Expression conjunct, string colName, string alias, TableDef def) + private static (Expression Low, Expression High)? Between(Expression conjunct, string colName, string alias, TableDefinition def) { if (conjunct is not BetweenExpression { Negated: false } between || Column(between.Value, alias, def) is not { } column @@ -458,7 +458,7 @@ JetDataType.Boolean or JetDataType.Byte or JetDataType.Int16 or JetDataType.Int3 /// The column reference if is a column of the given scan (its qualifier is /// the scan's alias, or it is unqualified and the table has such a column), else null. - private static ColumnReference? Column(Expression e, string alias, TableDef def) + private static ColumnReference? Column(Expression e, string alias, TableDefinition def) { if (e is not ColumnReference c) return null; if (c.Table is { } t && !string.Equals(t, alias, StringComparison.OrdinalIgnoreCase)) return null; diff --git a/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs b/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs index dc3210f9..9bdf8512 100644 --- a/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs +++ b/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs @@ -358,12 +358,12 @@ private static (PlanNode Node, List Unplaced) Place( return (new JoinNode(left, right, j.Kind, on), outside); } - // Both APPLY kinds preserve their left side, so a conjunct confined to it can be pushed there: dropping - // a left row before the lateral runs removes exactly the output rows the WHERE would have removed - // after, and saves re-running the whole right side for it. Nothing is pushed into the RIGHT side — - // under OUTER APPLY a filter there can empty an otherwise non-empty result and so manufacture the very + // Both APPLY kinds and a LEFT JOIN preserve their left side, so a conjunct confined to it can be pushed + // there: dropping a left row before the join removes exactly the output rows the WHERE would have + // removed after, and saves joining it at all. Nothing is pushed into the RIGHT side — under OUTER APPLY + // or a LEFT JOIN a filter there can empty an otherwise matching row and so manufacture the very // null-padded row the WHERE was there to drop. - if (node is JoinNode { Kind: JoinKind.CrossApply or JoinKind.OuterApply } a) + if (node is JoinNode { Kind: JoinKind.CrossApply or JoinKind.OuterApply or JoinKind.Left } a) { (PlanNode left, List rest) = Place(a.Left, candidates, introduced); PlanNode lateral = a with { Left = left }; diff --git a/src/LibRed/LibRed.Engine/Planning/ViewExpander.cs b/src/LibRed/LibRed.Engine/Planning/ViewExpander.cs index 0f049357..6e6dcf59 100644 --- a/src/LibRed/LibRed.Engine/Planning/ViewExpander.cs +++ b/src/LibRed/LibRed.Engine/Planning/ViewExpander.cs @@ -1,3 +1,4 @@ +using LibRed.Catalog; using LibRed.Sql.Ast; using LibRed.Sql.Parsing; @@ -7,8 +8,10 @@ namespace LibRed.Engine.Planning; /// Rewrites a query so that any reference to a view becomes a derived table (a subquery over the view's /// stored SELECT) — the same shape the planner already handles for explicit subqueries. A view named in /// a FROM clause, or inside a scalar/EXISTS subquery in any clause (projection, WHERE, GROUP BY, HAVING, -/// ORDER BY), is expanded. View SQL is reconstructed by the catalog (JetCatalog.Views) and parsed -/// here. Views nested inside other views are expanded recursively. +/// ORDER BY), is expanded. View SQL is reconstructed by the catalog (JetCatalog.FindQuery) and parsed +/// here. Views nested inside other views are expanded recursively. A name that is a table is never a view — +/// tables and queries share one container, whose names are unique — so a name is only looked up as a query +/// when FindTable does not know it. /// /// /// The recursion carries the set of views currently being expanded, so a view that reaches itself is @@ -22,29 +25,29 @@ internal static class ViewExpander // IDE0028's only fix here is `[]`, which would silently drop the comparer and let a view expand // itself recursively when the cycle guard's names differ only by case. #pragma warning disable IDE0028 - public static SqlStatement Expand(SqlStatement statement, IReadOnlyDictionary views, ISqlParser parser) => - views.Count == 0 ? statement : Rewrite(statement, views, parser, new HashSet(StringComparer.OrdinalIgnoreCase)); + public static SqlStatement Expand(SqlStatement statement, JetCatalog catalog, ISqlParser parser) => + Rewrite(statement, catalog, parser, new HashSet(StringComparer.OrdinalIgnoreCase)); #pragma warning restore IDE0028 private static SqlStatement Rewrite( - SqlStatement statement, IReadOnlyDictionary views, ISqlParser parser, HashSet active) => statement switch + SqlStatement statement, JetCatalog catalog, ISqlParser parser, HashSet active) => statement switch { - SelectStatement s => RewriteSelect(s, views, parser, active), + SelectStatement s => RewriteSelect(s, catalog, parser, active), SetOperationStatement so => so with { - Left = Rewrite(so.Left, views, parser, active), - Right = Rewrite(so.Right, views, parser, active), + Left = Rewrite(so.Left, catalog, parser, active), + Right = Rewrite(so.Right, catalog, parser, active), }, _ => statement, }; private static SelectStatement RewriteSelect( - SelectStatement select, IReadOnlyDictionary views, ISqlParser parser, HashSet active) + SelectStatement select, JetCatalog catalog, ISqlParser parser, HashSet active) { - Expression Expr(Expression e) => RewriteExpression(e, views, parser, active); + Expression Expr(Expression e) => RewriteExpression(e, catalog, parser, active); return select with { - From = select.From is null ? null : RewriteSource(select.From, views, parser, active), + From = select.From is null ? null : RewriteSource(select.From, catalog, parser, active), Projection = select.Projection.Select(i => i with { Value = Expr(i.Value) }).ToList(), Where = select.Where is { } w ? Expr(w) : null, GroupBy = select.GroupBy.Select(Expr).ToList(), @@ -56,29 +59,29 @@ private static SelectStatement RewriteSelect( /// Rewrites views referenced inside expression subqueries (scalar / EXISTS), recursing through /// the operator/function tree; leaf expressions are returned unchanged. private static Expression RewriteExpression( - Expression expr, IReadOnlyDictionary views, ISqlParser parser, HashSet active) => expr switch + Expression expr, JetCatalog catalog, ISqlParser parser, HashSet active) => expr switch { // Rewrite, not RewriteSelect: a subquery may be a set operation, whose arms each need view expansion. - ScalarSubquery s => new ScalarSubquery(Rewrite(s.Query, views, parser, active)), - ExistsExpression x => new ExistsExpression(Rewrite(x.Query, views, parser, active)), + ScalarSubquery s => new ScalarSubquery(Rewrite(s.Query, catalog, parser, active)), + ExistsExpression x => new ExistsExpression(Rewrite(x.Query, catalog, parser, active)), InSubqueryExpression i => i with { - Value = RewriteExpression(i.Value, views, parser, active), - Query = Rewrite(i.Query, views, parser, active), + Value = RewriteExpression(i.Value, catalog, parser, active), + Query = Rewrite(i.Query, catalog, parser, active), }, - _ => expr.MapOperands(o => RewriteExpression(o, views, parser, active)), + _ => expr.MapOperands(o => RewriteExpression(o, catalog, parser, active)), }; private static TableReference RewriteSource( - TableReference source, IReadOnlyDictionary views, ISqlParser parser, HashSet active) => source switch + TableReference source, JetCatalog catalog, ISqlParser parser, HashSet active) => source switch { - NamedTable n when views.TryGetValue(n.Name, out string? sql) => ExpandView(n, sql, views, parser, active), + NamedTable n when catalog.FindQuery(n.Name) is { IsAction: false, Sql: { } sql } => ExpandView(n, sql, catalog, parser, active), JoinTable j => j with { - Left = RewriteSource(j.Left, views, parser, active), - Right = RewriteSource(j.Right, views, parser, active), + Left = RewriteSource(j.Left, catalog, parser, active), + Right = RewriteSource(j.Right, catalog, parser, active), }, - SubqueryTable sq => sq with { Query = Rewrite(sq.Query, views, parser, active) }, + SubqueryTable sq => sq with { Query = Rewrite(sq.Query, catalog, parser, active) }, _ => source, }; @@ -86,7 +89,7 @@ NamedTable n when views.TryGetValue(n.Name, out string? sql) => ExpandView(n, sq /// The name is removed again on the way out, so two sibling references to the same view are fine — only a /// reference reached from INSIDE that view's own expansion is a cycle. private static SubqueryTable ExpandView( - NamedTable table, string sql, IReadOnlyDictionary views, ISqlParser parser, HashSet active) + NamedTable table, string sql, JetCatalog catalog, ISqlParser parser, HashSet active) { if (!active.Add(table.Name)) throw new InvalidOperationException( @@ -94,7 +97,7 @@ private static SubqueryTable ExpandView( try { return new SubqueryTable( - Rewrite(parser.ParseStatement(sql), views, parser, active), // expand views nested in the view + Rewrite(parser.ParseStatement(sql), catalog, parser, active), // expand views nested in the view table.Alias ?? table.Name); } finally { active.Remove(table.Name); } diff --git a/src/LibRed/LibRed.Engine/QueryEngine.cs b/src/LibRed/LibRed.Engine/QueryEngine.cs index 4daeb6c0..36e8d84e 100644 --- a/src/LibRed/LibRed.Engine/QueryEngine.cs +++ b/src/LibRed/LibRed.Engine/QueryEngine.cs @@ -1,4 +1,3 @@ -using LibRed.Catalog; using LibRed.Engine.Execution; using LibRed.Engine.Plan; using LibRed.Engine.Planning; @@ -63,7 +62,7 @@ private ResultSet ExecuteQueryCore(SqlStatement parsed, IReadOnlyDictionary? par /// , and an unknown name throws . public int ExecuteStoredActionQuery(string name) { - if (!_database.Catalog.ActionQueries.TryGetValue(name, out StoredActionQuery? query)) + if (_database.Catalog.FindQuery(name) is not { IsAction: true } query) throw new InvalidOperationException($"No stored action query named '{name}'."); if (query.Sql is null) throw new NotSupportedException(query.UnsupportedReason ?? $"Stored query '{name}' cannot be executed by LibRed yet."); @@ -165,7 +165,7 @@ public CommandResult Execute(string sql, IReadOnlyDictionary? p 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); + SqlStatement ast = ViewExpander.Expand(parsed, _database.Catalog, _parser); BoundStatement bound = _binder.Bind(ast); return Route(bound.Statement, parameters); } @@ -278,7 +278,7 @@ private PlanNode PlanWithIndexes(BoundStatement bound) => /// (e.g. that an unindexed equi-join becomes a hash join). internal PlanNode PlanFor(string sql) { - SqlStatement ast = ViewExpander.Expand(_parser.ParseStatement(sql), _database.Catalog.Views, _parser); + SqlStatement ast = ViewExpander.Expand(_parser.ParseStatement(sql), _database.Catalog, _parser); return PlanWithIndexes(_binder.Bind(ast)); } @@ -289,13 +289,12 @@ internal PlanNode PlanFor(string sql) /// private CommandResult ExecuteProcedure(ExecuteStatement exec, IReadOnlyDictionary? parameters) { - JetCatalog catalog = _database.Catalog; + var query = _database.Catalog.FindQuery(exec.Procedure); var bag = new ParameterBag(parameters); var executor = new QueryExecutor(_database, parameters, _session); var evaluator = new ExpressionEvaluator(new EvalScope([], [], null), executor, bag, _session); - List paramNames = catalog.QueryParameters.TryGetValue(exec.Procedure, out var declared) - ? declared.Select(p => p.Name).ToList() : []; + List paramNames = query?.Parameters.Select(p => p.Name).ToList() ?? []; // Access EXEC arguments take three shapes (EF emits all of them): // procParam = value → a NAMED argument (bind the proc's named parameter to the value) @@ -340,10 +339,10 @@ private CommandResult ExecuteProcedure(ExecuteStatement exec, IReadOnlyDictionar } // A stored SELECT (with its PARAMETERS clause) → rows; a stored action query → rows-affected. - if (catalog.Views.TryGetValue(exec.Procedure, out string? viewSql)) + if (query is { IsAction: false, Sql: { } viewSql }) return Execute(viewSql, args); - if (catalog.ActionQueries.TryGetValue(exec.Procedure, out StoredActionQuery? action)) + if (query is { IsAction: true } action) { if (action.Sql is null) throw new NotSupportedException( diff --git a/src/LibRed/LibRed.Engine/Schema/CatalogSchemaProvider.cs b/src/LibRed/LibRed.Engine/Schema/CatalogSchemaProvider.cs index 82cd8e01..7a5e017a 100644 --- a/src/LibRed/LibRed.Engine/Schema/CatalogSchemaProvider.cs +++ b/src/LibRed/LibRed.Engine/Schema/CatalogSchemaProvider.cs @@ -14,12 +14,12 @@ public sealed class CatalogSchemaProvider(JetCatalog catalog) : ISchemaProvider public ITableSchema? GetTable(string name) { - // The magic INFORMATION_SCHEMA. tables are virtual (no catalog TableDef) — expose a synthetic + // The magic INFORMATION_SCHEMA. tables are virtual (no catalog TableDefinition) — expose a synthetic // schema so binding validates; the executor materialises their rows from the catalog. if (InformationSchema.IsInformationSchema(name)) return new InformationSchemaTable(name); - TableDef? def = _catalog.FindTable(name); + TableDefinition? def = _catalog.FindTable(name); return def is null ? null : new TableSchema(def); } @@ -40,7 +40,7 @@ private sealed class VirtualColumn(string name, Type clrType) : IColumnSchema public Type ClrType => clrType; } - private sealed class TableSchema(TableDef def) : ITableSchema + private sealed class TableSchema(TableDefinition def) : ITableSchema { public string Name => def.Name; public IReadOnlyList Columns { get; } = diff --git a/src/LibRed/LibRed.Engine/Schema/InformationSchema.cs b/src/LibRed/LibRed.Engine/Schema/InformationSchema.cs index a085f7fa..cfc46469 100644 --- a/src/LibRed/LibRed.Engine/Schema/InformationSchema.cs +++ b/src/LibRed/LibRed.Engine/Schema/InformationSchema.cs @@ -79,12 +79,12 @@ public static bool IsInformationSchema(string tableName) => // Same table set as every other view — user tables plus the '#Dual' internal helper, no // MSys*/temp system tables — each classified by TABLE_TYPE. Consumers filter by type anyway // (e.g. the creator's "has tables" check is TABLE_TYPE IN ('BASE TABLE','VIEW')). - foreach (TableDef t in tables) + foreach (TableDefinition t in tables) rows.Add([t.Name, TableType(t), t.ValidationRule, t.ValidationText]); break; case "COLUMNS": - foreach (TableDef t in tables) + foreach (TableDefinition t in tables) foreach (ColumnDef c in t.Columns) // DATA_TYPE / IS_NULLABLE / CHARACTER_MAXIMUM_LENGTH come from the shared JetStoreType so // they always agree with the scaffolder. Precision/Scale are cast to int (they are byte on @@ -99,7 +99,7 @@ public static bool IsInformationSchema(string tableName) => break; case "INDEXES": - foreach (TableDef t in tables) + foreach (TableDefinition t in tables) foreach (IndexDef ix in t.Indexes) // IS_NULLABLE / IGNORES_NULLS mirror AdoxSchema.GetIndexes' AllowNullsEnum derivation, // NOT uniqueness: an index is non-nullable only when it DISALLOW NULLs (Required, flag @@ -109,7 +109,7 @@ public static bool IsInformationSchema(string tableName) => break; case "INDEX_COLUMNS": - foreach (TableDef t in tables) + foreach (TableDefinition t in tables) foreach (IndexDef ix in t.Indexes) // ORDINAL_POSITION is 0-based here too, matching AdoxSchema.GetIndexColumns (which uses the // loop index k directly). RELATION_COLUMNS below stays 1-based, as ADOX does there. @@ -134,7 +134,7 @@ public static bool IsInformationSchema(string tableName) => break; case "CHECK_CONSTRAINTS": - foreach (TableDef t in tables) + foreach (TableDefinition t in tables) foreach ((string name, string expr) in t.CheckConstraints) rows.Add([t.Name, name, expr]); break; @@ -146,7 +146,7 @@ public static bool IsInformationSchema(string tableName) => // Classify TABLE_TYPE as EFCore.Jet's AdoxSchema / SchemaProvider does: a '#'-prefixed helper (#Dual) is an // INTERNAL TABLE, an MSys* table is a SYSTEM TABLE, everything else is a user BASE TABLE. - private static string TableType(TableDef t) => + private static string TableType(TableDefinition t) => t.Name.StartsWith('#') ? "INTERNAL TABLE" : t.Name.StartsWith("MSys", StringComparison.OrdinalIgnoreCase) ? "SYSTEM TABLE" : "BASE TABLE"; diff --git a/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs b/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs index 3c6e2206..f503d365 100644 --- a/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs +++ b/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs @@ -1,4 +1,5 @@ using LibRed.Catalog; +using LibRed.Formats; using LibRed.Storage; namespace LibRed.Engine.Schema; @@ -144,10 +145,10 @@ public static bool IsKnown(string collection) => case "Tables": // Tables and views together, ordered by name, as ACE returns them. A stored query with declared // parameters is a procedure rather than a view, so it appears in neither this rowset nor Views. - foreach (TableDef t in catalog.Tables) + foreach (TableDefinition t in catalog.Tables) if (TableType(t) is { } type) rows.Add([null, null, t.Name, type, null, null, null, null, null]); - foreach (string name in ViewNames(catalog)) + foreach ((string name, _) in Views(catalog)) rows.Add([null, null, name, "VIEW", null, null, null, null, null]); rows.Sort(ByName(database, 2)); break; @@ -155,7 +156,7 @@ public static bool IsKnown(string collection) => case "Columns": // Every object ACE lists columns for except the system tables: user tables, the Access-owned // hidden tables, and views. - foreach (TableDef t in catalog.Tables) + foreach (TableDefinition t in catalog.Tables) { if (TableType(t) is not { } type || type == "SYSTEM TABLE") continue; foreach (ColumnDef c in t.Columns) @@ -172,7 +173,7 @@ public static bool IsKnown(string collection) => // A column the query passes through unchanged reports the stored column behind it, facets and // all; a computed one has only the type the expression yields. Either way the writability flag // is WRITE rather than the WRITEUNKNOWN a stored column carries, as ACE reports a view's. - foreach (string view in ViewNames(catalog)) + foreach ((string view, _) in Views(catalog)) { var described = ViewColumns(database, view); for (int i = 0; i < described.Count; i++) @@ -211,7 +212,7 @@ public static bool IsKnown(string collection) => // to a table). Every LOGICAL index is listed, so a column covered by a named index, a primary // key and a relationship appears once under each name, all reporting the one real index's // columns and statistics — as ACE lists them. CARDINALITY is the distinct-entry count. - foreach (TableDef t in catalog.Tables) + foreach (TableDefinition t in catalog.Tables) { if (TableType(t) is not { } type || type == "SYSTEM TABLE") continue; foreach ((string name, IndexDef ix, bool isPrimaryKey) in LogicalIndexes(t)) @@ -225,8 +226,8 @@ public static bool IsKnown(string collection) => break; case "Views": - foreach (string name in ViewNames(catalog)) - rows.Add([null, null, name, catalog.Views[name], null, true, null, null, null]); + foreach ((string name, string sql) in Views(catalog)) + rows.Add([null, null, name, sql, null, true, null, null, null]); rows.Sort(ByName(database, 2)); break; @@ -234,11 +235,9 @@ public static bool IsKnown(string collection) => // A stored query is a procedure when it declares parameters, and an action query always is. // ACE's PROCEDURE_DEFINITION carries the PARAMETERS clause ahead of the statement; LibRed does // not reconstruct the parameters' declared types yet, so the statement alone is reported. - foreach ((string name, string sql) in catalog.Views) - if (catalog.QueryParameters.ContainsKey(name)) - rows.Add([null, null, name, ProcedureTypeReturnsRows, sql, null, null, null]); - foreach ((string name, StoredActionQuery query) in catalog.ActionQueries) - rows.Add([null, null, name, ProcedureTypeReturnsRows, query.Sql, null, null, null]); + foreach ((string name, StoredQuery query) in catalog.Queries) + if (query.IsAction || query is { Sql: not null, Parameters.Count: > 0 }) + rows.Add([null, null, name, ProcedureTypeReturnsRows, query.Sql, null, null, null]); rows.Sort(ByName(database, 2)); break; @@ -258,7 +257,7 @@ public static bool IsKnown(string collection) => break; case "PrimaryKeys": - foreach (TableDef t in ConstraintTables(catalog)) + foreach (TableDefinition t in ConstraintTables(catalog)) if (t.Indexes.FirstOrDefault(ix => ix.IsPrimaryKey) is { } pk) for (int i = 0; i < pk.Columns.Count; i++) rows.Add([null, null, t.Name, pk.Columns[i].Column.Name, null, null, (long)(i + 1), pk.Name]); @@ -266,7 +265,7 @@ public static bool IsKnown(string collection) => case "TableConstraints": // ACE orders the rows by constraint name, then — for the many keys Access names alike — by table. - foreach ((TableDef table, string name, string kind, IndexDef? _, ForeignKey? __) in Constraints(catalog)) + foreach ((TableDefinition table, string name, string kind, IndexDef? _, ForeignKey? __) in Constraints(catalog)) rows.Add([null, null, name, null, null, table.Name, kind, false, false, null]); rows.Sort(ByName(database, 2, thenBy: 5)); break; @@ -277,7 +276,7 @@ public static bool IsKnown(string collection) => // table's primary-key column instead of the one the relationship's index actually holds // (Employees.ReportsTo read back as EmployeeID); the rule behind those two is not established, // and the key's own columns are what the rowset is defined to carry. - foreach ((TableDef table, string name, string _, IndexDef? index, ForeignKey? fk) in Constraints(catalog)) + foreach ((TableDefinition table, string name, string _, IndexDef? index, ForeignKey? fk) in Constraints(catalog)) { if (fk is not null) for (int i = 0; i < fk.Columns.Count; i++) @@ -291,7 +290,7 @@ public static bool IsKnown(string collection) => break; case "ConstraintColumnUsage": - foreach ((TableDef table, string name, string _, IndexDef? index, ForeignKey? __) in Constraints(catalog)) + foreach ((TableDefinition table, string name, string _, IndexDef? index, ForeignKey? __) in Constraints(catalog)) if (index is not null) foreach (var column in index.Columns) rows.Add([null, null, table.Name, column.Column.Name, null, null, null, null, name]); @@ -307,7 +306,7 @@ public static bool IsKnown(string collection) => break; case "CheckConstraints": - foreach (TableDef t in catalog.Tables) + foreach (TableDefinition t in catalog.Tables) { if (TableType(t) is not { } type || type == "SYSTEM TABLE") continue; foreach ((string name, string expression) in t.CheckConstraints) @@ -318,7 +317,7 @@ public static bool IsKnown(string collection) => case "Statistics": // Every table, system ones included, with the row count the TDEF carries. Views have no // cardinality to report and ACE lists none. - foreach (TableDef t in catalog.Tables) + foreach (TableDefinition t in catalog.Tables) if (TableType(t) is not null) rows.Add([null, null, t.Name, (decimal)t.RowCount]); rows.Sort(ByName(database, 2)); @@ -328,10 +327,10 @@ public static bool IsKnown(string collection) => // Each stored query's declared parameters, in declaration order. Access declares a parameter's // type but neither a default nor whether it takes null, so those report as the rowset's // "no default" and nullable. An untyped parameter — Access's `Value` — reports no type. - foreach ((string name, IReadOnlyList parameters) in catalog.QueryParameters) - for (int i = 0; i < parameters.Count; i++) + foreach ((string name, StoredQuery query) in catalog.Queries) + for (int i = 0; i < query.Parameters.Count; i++) { - StoredQueryParameter p = parameters[i]; + StoredQueryParameter p = query.Parameters[i]; // The declared facets where the parameter row records them: a text length (reported // in characters and in bytes, two per character as the Columns collection reports a // column's), and a decimal's precision and scale. A type that records none falls back @@ -352,7 +351,7 @@ public static bool IsKnown(string collection) => // Which stored column each of a view's columns comes from, where one does — the provenance the // planner already tracks. A computed column has no base column and so no row here. var owners = ColumnOwners(catalog); - foreach (string view in ViewNames(catalog)) + foreach ((string view, _) in Views(catalog)) foreach (var column in ViewColumns(database, view)) if (column.Source is { } source && owners.TryGetValue(source, out string? owner)) rows.Add([null, null, view, null, null, owner, source.Name]); @@ -363,16 +362,16 @@ public static bool IsKnown(string collection) => /// The tables whose constraints are reported: the same set whose columns are, so a system table's /// constraints stay out of the rowsets as ACE keeps them. - private static IEnumerable ConstraintTables(JetCatalog catalog) => + private static IEnumerable ConstraintTables(JetCatalog catalog) => catalog.Tables.Where(t => TableType(t) is { } type && type != "SYSTEM TABLE"); /// Every constraint on those tables: a primary key, each other unique index, and each foreign key /// the table declares. A foreign key's columns come from the index backing it, so it reports the columns /// it constrains. - private static IEnumerable<(TableDef Table, string Name, string Kind, IndexDef? Index, ForeignKey? ForeignKey)> + private static IEnumerable<(TableDefinition Table, string Name, string Kind, IndexDef? Index, ForeignKey? ForeignKey)> Constraints(JetCatalog catalog) { - foreach (TableDef t in ConstraintTables(catalog)) + foreach (TableDefinition t in ConstraintTables(catalog)) { foreach (IndexDef ix in t.Indexes.Where(ix => ix.IsPrimaryKey)) yield return (t, ix.Name, "PRIMARY KEY", ix, null); @@ -386,7 +385,7 @@ private static IEnumerable ConstraintTables(JetCatalog catalog) => } /// The index on the child table that stores a relationship's key columns. - private static IndexDef? BackingIndex(ForeignKey fk, TableDef child) + private static IndexDef? BackingIndex(ForeignKey fk, TableDefinition child) { var columns = fk.Columns.Select(c => c.Column).ToHashSet(StringComparer.OrdinalIgnoreCase); return child.Indexes.FirstOrDefault(ix => @@ -397,8 +396,7 @@ private static IEnumerable ConstraintTables(JetCatalog catalog) => /// relationship names some other unique index. private static string? ParentKeyName(ForeignKey fk, JetCatalog catalog) { - TableDef? parent = catalog.Tables.FirstOrDefault(t => - t.Name.Equals(fk.ReferencedTable, StringComparison.OrdinalIgnoreCase)); + TableDefinition? parent = catalog.FindTable(fk.ReferencedTable); if (parent is null) return null; var columns = fk.Columns.Select(c => c.ReferencedColumn).ToHashSet(StringComparer.OrdinalIgnoreCase); @@ -445,7 +443,7 @@ private static string RefAction(bool cascade, bool setNull) => continue; } - TableDef? owner = catalog.Tables.FirstOrDefault(t => t.Name == table); + TableDefinition? owner = catalog.FindTable(table); described.Add(new Execution.ResultColumn( column.Name, clrType, table, c.Name, AllowNull: Nullable(c), @@ -475,7 +473,7 @@ private static Dictionary ColumnOwners(JetCatalog catalog) { var owners = new Dictionary(ReferenceEqualityComparer.Instance as IEqualityComparer ?? EqualityComparer.Default); - foreach (TableDef t in catalog.Tables) + foreach (TableDefinition t in catalog.Tables) foreach (ColumnDef c in t.Columns) owners[c] = t.Name; return owners; @@ -515,26 +513,19 @@ private static Dictionary ColumnOwners(JetCatalog catalog) _ => "VarBinary", }; - // MSysObjects.Flags decides what an object is called in the schema rowsets, the way Access classifies it — - // not its name (measured on ACE 16): the system bit makes it a SYSTEM TABLE, the hidden bit an ACCESS TABLE - // (the navigation-pane and resource tables), and an object carrying ExcludedFlags is not listed at all — - // the MSysComplexType_* tables, flags 0x80030000. - private const uint SystemFlag = 0x80000000; - private const uint HiddenFlag = 0x00000008; - private const uint ExcludedFlags = 0x00030000; - - /// What ACE calls this object in the schema rowsets, or null when it lists the object nowhere. - private static string? TableType(TableDef t) => - (t.ObjectFlags & ExcludedFlags) != 0 ? null - : (t.ObjectFlags & SystemFlag) != 0 ? "SYSTEM TABLE" - : (t.ObjectFlags & HiddenFlag) != 0 ? "ACCESS TABLE" + /// What ACE calls this object in the schema rowsets, or null when it lists the object nowhere. + /// MSysObjects.Flags decides it, the way Access classifies an object — not its name (measured on ACE 16). + private static string? TableType(TableDefinition t) => + (t.ObjectFlags & ObjectAttributes.ComplexStorage) != 0 ? null + : (t.ObjectFlags & ObjectAttributes.System) != 0 ? "SYSTEM TABLE" + : (t.ObjectFlags & ObjectAttributes.Hidden) != 0 ? "ACCESS TABLE" : "TABLE"; /// Each of the table's logical indexes paired with the real index that stores it, primary key /// first and the rest by name, as ACE orders them. The parent half of a relationship is left out: Access /// names it .r… and keeps it out of its own schema views. A table read from a definition that /// carries no logical list (one LibRed built itself, say) falls back to its real indexes, one name each. - private static IEnumerable<(string Name, IndexDef Index, bool IsPrimaryKey)> LogicalIndexes(TableDef t) + private static IEnumerable<(string Name, IndexDef Index, bool IsPrimaryKey)> LogicalIndexes(TableDefinition t) { IEnumerable<(string Name, IndexDef Index, bool IsPrimaryKey)> all = t.LogicalIndexes.Count > 0 ? t.LogicalIndexes @@ -549,9 +540,12 @@ private static Dictionary ColumnOwners(JetCatalog catalog) .ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase); } - /// The stored SELECT queries that are views: a query declaring parameters is a procedure. - private static IEnumerable ViewNames(JetCatalog catalog) => - catalog.Views.Keys.Where(name => !catalog.QueryParameters.ContainsKey(name)); + /// The stored SELECT queries that are views, with their SQL: a query declaring parameters is a procedure, + /// and one that does not reconstruct has no SQL to report. + private static IEnumerable<(string Name, string Sql)> Views(JetCatalog catalog) => + catalog.Queries + .Where(q => q.Value is { IsAction: false, Sql: not null, Parameters.Count: 0 }) + .Select(q => (q.Key, q.Value.Sql!)); /// Orders rows by the name in , then by , as ACE does: in /// the database's collation — Order_Details before Orders, an underscore sorting before letters — not diff --git a/src/LibRed/LibRed.Sql/Ast/Statements.cs b/src/LibRed/LibRed.Sql/Ast/Statements.cs index cae5f67d..df9188a2 100644 --- a/src/LibRed/LibRed.Sql/Ast/Statements.cs +++ b/src/LibRed/LibRed.Sql/Ast/Statements.cs @@ -191,7 +191,7 @@ public sealed record CreateTableStatement( IReadOnlyList CheckConstraints, // The PRIMARY KEY's CONSTRAINT name, if one was given (column- or table-level). ACE names the primary // key index after the constraint (verified: the scaffolder round-trips it), so it must be preserved. - // When null (no name given), the engine picks its own stable fallback in TableCreator. + // When null (no name given), the engine picks its own stable fallback in SchemaEditor. string? PrimaryKeyName = null, // How many columns are declared before the PRIMARY KEY constraint — see UniqueConstraint. int PrimaryKeyDeclaredAfterColumns = 0) : SqlStatement; @@ -373,4 +373,4 @@ public sealed record UpdateStatement( public sealed record DeleteStatement( string? TargetTable, TableReference From, - Expression? Where) : SqlStatement; + Expression? Where) : SqlStatement; \ No newline at end of file diff --git a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 index 8f3098c5..8e75d610 100644 --- a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 +++ b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 @@ -282,7 +282,7 @@ rowValue : DEFAULT | expression ; queryExpression : queryTerm (setOperator queryTerm)* orderByClause? offsetFetchClause? ownerAccessOption? ; // WITH OWNERACCESS OPTION: run the query with its owner's permissions. LibRed has no users to act for, so it -// changes nothing when a query runs; a stored query keeps it (StoredQueryFormat.FlagOwnerAccess). ACE takes it +// changes nothing when a query runs; a stored query keeps it (QueryOptions.OwnerAccess). ACE takes it // at the end of every SELECT — the first of a UNION's and a subquery's too — after a query's ORDER BY but not // before it, and at the end of an INSERT, UPDATE or DELETE (verified). ownerAccessOption : WITH OWNERACCESS OPTION ; diff --git a/src/LibRed/README.md b/src/LibRed/README.md index 34079a93..59b2e253 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -39,19 +39,30 @@ EFCore → Ado → Engine → Sql EF Core services **both** providers share. Nothing under `src/LibRed` references `EFCore.Jet` or `EFCore.Jet.Data`, which is what keeps LibRed off the ACE-bound, Windows-only path. -## Layering inside LibRed.Core - -``` -Storage (Table, TableCursor, RowDecoder, UsageMap, Types/JetTypeCodec) - ↓ -Catalog (JetCatalog → TableDef / ColumnDef / IndexDef, JetDataType) - ↓ -Pages (DatabaseDefinition, TableDefinition, Data, Index, UsageMap, Lval) - ↓ -IO (PageChannel, PageBuffer) ← Crypto decrypts pages here - ↓ -Formats (JetFormatBase — Jet 4 / ACE offsets & constants; Jet3Format is a stub overriding none yet) -``` +## Ownership inside LibRed.Core + +Each stored structure owns its layout and both directions of its codec. Schema changes use these owners +through `SchemaEditor`; there is one table definition, one database definition and one allocator per channel. + +Engine uses Core's public API only. `JetDatabase` owns schema operations, and `Table.Insert`, `Update` and +`Delete` maintain the row and its indexes together; deletion also removes the row's owned complex values. +Raw page writes, allocation, schema writers and separate +index mutations are internal. Published table and catalog metadata collections are read-only views. +SQL expression checks, relationships and cascades are enforced by Engine before calling these storage operations. + +| Area | Owners | +| --- | --- | +| Database and data pages | `DatabaseDefinitionPage`, `DataPage` (including released states and relocation targets) | +| Table definitions | `TableDefinition`: metadata, descriptors, index blocks, chain reads and writes | +| Rows and values | `RowCodec` (layout and declaration limits), `JetTypeCodec`, `CalculatedValue`, `LongValueStore` | +| Indexes | `IndexTree` (leaf and intermediate pages), `IndexKeyCodec` (encoding, decoding and truncation checksum), `IndexCursor` (traversal) | +| Maps and allocation | `UsageMap` reads and writes maps; `PageChannel.Allocator` owns page reuse, growth and release | +| Catalog and schema | `JetCatalog`, `SchemaEditor`, `PropertyBlob`, `NameMap`, `StoredQuery`, `ComplexAttachment` | +| File access | `PageChannel`, `PageBuffer`, cache, transactions and locks; Crypto codecs transform pages here | +| Version differences | `JetFormatBase` and its Jet/ACE implementations; format code and flag enums | + +Page types `0x0108` and `0x0109` are release states of `TableDefinition` and `DataPage`. The unknown +`0x0106` and `0x0107` layouts have no implementation. See [the format reference](docs/format/README.md). ## Status @@ -178,7 +189,7 @@ Treat the number as of its date — an EF Core version bump moves it. Column `DEFAULT` values are persisted to the table's `LvProp` property blob (on an LVAL page), read back onto the column, and applied when an insert omits the column — **and Access honors them too**. Table-level `CHECK` constraints are persisted to the `LvProp` `CheckConstraints` property (verbatim - expression text) and read back onto `TableDef.CheckConstraints` — **and Access enforces them**. + expression text) and read back onto `TableDefinition.CheckConstraints` — **and Access enforces them**. From files Access wrote, a relationship's 1:1 flag (`grbit` `0x01`) is read and reported as ONE/MANY by `INFORMATION_SCHEMA`, and a column's `ValidationRule`/`ValidationText` are read off the same property blob, reported by `INFORMATION_SCHEMA` and kept through a rewrite. @@ -200,7 +211,7 @@ Treat the number as of its date — an EF Core version bump moves it. `No`/`Off` are `True` and `False`; any word, reserved or not, names a column after a `.` or `!` (`qryHistory.Key`); and an unbracketed name may be in any script (`Bevételek.Datum`, `Номер`). `WITH COMPRESSION` on a Text column is honoured end to end — it maps onto `SupportsCompressedUnicode`, - `TdefBuilder` writes the `0x01` extended-flag bit, and `JetTypeCodec` gates compressed encoding on it. + `TableDefinition` writes the `0x01` extended-flag bit, and `JetTypeCodec` gates compressed encoding on it. Function arguments are arity-checked against a per-function range table, so a wrong count raises rather than being silently ignored. Plan nodes: Scan / IndexScan / IndexSeek / IndexRangeSeek / Filter / Project / Join / HashJoin / Aggregate / @@ -364,4 +375,4 @@ skips the Jet-dialect rewrites (and `JetCompatibilityExpressionVisitor`, whose r Each mode has its own EF Core specification suite, because the same LINQ query produces different SQL and so needs different baselines: `test/EFCore.LibRed.FunctionalTests` (compatible) and `test/EFCore.LibRed.Extended.FunctionalTests` (extended). Both run across all five platforms on every push -that touches LibRed. +that touches LibRed. \ No newline at end of file diff --git a/src/LibRed/docs/design/index-key-checksum.md b/src/LibRed/docs/design/index-key-checksum.md index 1ae24a09..4d74b834 100644 --- a/src/LibRed/docs/design/index-key-checksum.md +++ b/src/LibRed/docs/design/index-key-checksum.md @@ -95,7 +95,7 @@ for each byte b: Confirmed numerically against the shipped implementation over 12,800 random inputs at every length from 1 to 64 bytes, plus the all-zero, all-`0xFF` and leading-zero edges. The shipped code keeps the `fold-then-XOR-the-tail` shape because it is the cheaper expression of the same function; the equivalence is -recorded in `JetIndexKeyChecksum`'s remarks so the "special case" is not mistaken for a guess again. +recorded in `IndexKeyCodec`'s remarks so the "special case" is not mistaken for a guess again. ## The other three questions @@ -121,7 +121,7 @@ recorded in `JetIndexKeyChecksum`'s remarks so the "special case" is not mistake all-text past the cap, numeric-tailed below and past it for each of `LONG`/`CURRENCY`/`DOUBLE`, and a dropped word-sort record at seven mark/position combinations. The probe pattern, if you need to go further: create and insert through ACE, read the entries back with `IndexCursor(...).RawEntries()`, and compare against -`IndexKeyEncoder.EncodeWithoutLengthLimit` — the untruncated key is what you need in order to reason about +`IndexKeyCodec.EncodeWithoutLengthLimit` — the untruncated key is what you need in order to reason about what ACE dropped. The one methodological lesson worth carrying: **a rule derived from inputs that cannot falsify it is not a diff --git a/src/LibRed/docs/design/transactions.md b/src/LibRed/docs/design/transactions.md index 8ae44c82..6f9e3215 100644 --- a/src/LibRed/docs/design/transactions.md +++ b/src/LibRed/docs/design/transactions.md @@ -290,7 +290,7 @@ Build correctness first with lock seams stubbed; drop the Jet lock manager in la writes now buffer in the overlay, savepoint frames live in `Transaction`, and rollback discards pages rather than restoring and truncating them. 2. **L3 statement atomicity.** ✅ done. Wrap every `QueryEngine` statement in an implicit txn; - convert the audit's non-atomic writers (`RowInserter`, `TableCreator`, `ViewCreator`, + convert the audit's non-atomic writers (`RowInserter`, `SchemaEditor`, `JetCatalog`, usage-map/LVAL) to rely on it. Regression: inject a late failure mid-statement, assert no partial state. 3. **Cascade worklist.** ✅ done for DELETE. Replace recursive cascade delete with the diff --git a/src/LibRed/docs/format/README.md b/src/LibRed/docs/format/README.md index 7a525569..877a8441 100644 --- a/src/LibRed/docs/format/README.md +++ b/src/LibRed/docs/format/README.md @@ -10,9 +10,14 @@ Unless noted, everything here describes **Jet 4 and ACE (12/14/16/17)**, which s structural layout. **Jet 3** (Access 97) differs in many of these and is *not yet implemented* — see [Version differences](#version-differences). -Implemented by `src/LibRed/LibRed.Core/`. The canonical offsets live under `Formats/` — -`JetFormatBase.cs` for the page, TDEF and row layout, and `IndexBlockFormat.cs`, `LongValueFormat.cs` and -`CatalogFormat.cs` for their own sub-structures. +Implemented by `src/LibRed/LibRed.Core/`. The canonical offsets and sizes live under `Formats/` — +named in `JetFormatBase.cs` and set per format in `Jet4Format.cs` (the ACE formats derive from it). The flag +and code fields are enums in their own files there (`IndexAttributes`, `ObjectType`, `RelationshipAction`, …), +Version-dependent fields stay in those format classes. Each structure's layout and read/write operations +live with its owner: `DatabaseDefinitionPage`, `TableDefinition`, `DataPage`, `RowCodec`, `IndexTree`, +`IndexKeyCodec`, `UsageMap`, and `LongValueStore`. A `PageChannel` owns one `PageAllocator`, used for page +reuse, file growth and release. Catalog records, properties and stored queries are read and written through +`JetCatalog`, `PropertyBlob`, `NameMap`, and `StoredQuery`. > **This reference is split across several files** — one per page type, plus cross-cutting topics; each is > self-contained (its structures *and* its read/write mechanics live together). Most describe the **on-disk @@ -64,18 +69,20 @@ catalogued one level up in [`../functions.md`](../functions.md). | --- | --- | --- | --- | --- | | `0x0100` | `00 01` | Database definition (page 0 only) | `DatabaseDefinitionPage` | [page-00](page-00-database.md) | | `0x0101` | `01 01` | Data page (also long-value/LVAL pages) | `DataPage` | [page-01](page-01-data-and-rows.md) / [long-values](long-values.md) | - | `0x0102` | `02 01` | Table definition (TDEF) | `TableDefinitionPage` | [page-02a](page-02a-tdef.md) | - | `0x0103` | `03 01` | Index B-tree node (intermediate) | `IndexCursor` | [page-03-04](page-03-04-index-btree.md) | - | `0x0104` | `04 01` | Index B-tree leaf | `IndexCursor` | [page-03-04](page-03-04-index-btree.md) | + | `0x0102` | `02 01` | Table definition (TDEF) | `TableDefinition` | [page-02a](page-02a-tdef.md) | + | `0x0103` | `03 01` | Index B-tree node (intermediate) | `IndexTree` | [page-03-04](page-03-04-index-btree.md) | + | `0x0104` | `04 01` | Index B-tree leaf | `IndexTree` | [page-03-04](page-03-04-index-btree.md) | | `0x0105` | `05 01` | Page-usage bitmap | `UsageMap` | [page-05](page-05-usage-maps.md) | | `0x0106` | `06 01` | *Unknown* — never seen in a file, but ACE treats it as row-bearing (below) | — | — | | `0x0107` | `07 01` | *Unknown* — likewise | — | — | - | `0x0108` | `08 01` | Released table definition (a dropped table's TDEF) | `PageType.ReleasedTableDefinition` | [page-08](page-08-released-tdef.md) | - | `0x0109` | `09 01` | Released data page (emptied by DELETE, or of its packed long values) | `PageType.ReleasedDataPage` | [page-09](page-09-released-data.md) | + | `0x0108` | `08 01` | Released table definition (a dropped table's TDEF) | `TableDefinition.MarkReleased` | [page-08](page-08-released-tdef.md) | + | `0x0109` | `09 01` | Released data page (emptied by DELETE, or of its packed long values) | `DataPage.MarkReleased` | [page-09](page-09-released-data.md) | `0x0108` and `0x0109` mark pages that have been **given back**. Neither needs handling on read — allocation selects on the global free map, not on the type — but both are written, so a file LibRed produces carries - the same markers Access would. A page never written reads as all zeros, word `0x0000`. + the same markers Access would. They are states of their original page classes, with only the type's low + byte changing when marked; they have no separate parser or layout class. The unknown `0x0106` and + `0x0107` layouts remain unimplemented. A page never written reads as all zeros, word `0x0000`. **Which types ACE reads as rows (verified).** Scanning a table, ACE parses a page its owned-pages map names as a data page when the word is `0x0101`, `0x0103`, `0x0104`, `0x0106`, `0x0107` or `0x0109`, and skips diff --git a/src/LibRed/docs/format/appendix-structures.md b/src/LibRed/docs/format/appendix-structures.md index 1ebdd845..6ddea539 100644 --- a/src/LibRed/docs/format/appendix-structures.md +++ b/src/LibRed/docs/format/appendix-structures.md @@ -34,9 +34,9 @@ All integers little-endian unless noted; offsets are hex, relative to the struct | `0x15` | 1 | Version minor byte (`0x01` on a file created as ACE14/Access 2010, else `0x00`; a version raise writes `0x00`) | | `0x16` | 2 | Unknown (zero) | | `0x18`–`0x98` | 128 | **Obfuscated header** — XOR'd with the RC4 keystream of the key `C7 DA 39 6B`; the `0x18`–`0x72` fields below are offsets into it (Jet3 masks 126) | -| `0x18` | 4 | Global free-pages map pointer `[row:1][page:3]` (`0x00000100` = page 1 row 0) | -| `0x1C` | 4 | Global released-pages map pointer `[row:1][page:3]` (`0x00000101` = page 1 row 1) | -| `0x20`–`0x2C` | 4×4 | Catalog bootstrap pointers — `MSysObjects`/`MSysACEs`/`MSysQueries`/`MSysRelationships` TDEF pages (`2`/`3`/`4`/`5`); `0x20` = catalog root | +| `0x18` | 4 | Global free-pages map pointer `[row:1][page:3]` (`0x00000100` = page 1 row 0 where ACE creates it) | +| `0x1C` | 4 | Global released-pages map pointer `[row:1][page:3]` (`0x00000101` = page 1 row 1 where ACE creates it) | +| `0x20`–`0x2C` | 4×4 | Catalog bootstrap pointers — `MSysObjects`/`MSysACEs`/`MSysQueries`/`MSysRelationships` TDEF pages, wherever the creator put them (`2`/`3`/`4`/`5` where ACE creates them); `0x20` = catalog root | | `0x30`–`0x3B` | 12 | `0x30`/`0x34`: `MSysAccounts`/`MSysGroups` TDEF pages in a workgroup file (`6`/`7`), zero otherwise; `0x38` zero | | `0x3C` | 2 | ANSI code page (LE; `0x04E4` = 1252) | | `0x3E` | 4 | Database/encryption key (`0` = not encrypted) | @@ -179,7 +179,7 @@ descriptor. | `0x0D` | 4 | Matching logical block's `index_num` on the other table (`0xFFFFFFFF` = none) | | `0x11` | 4 | FK table page (other table's TDEF; non-zero ⇒ relationship) | | `0x15` | 1 | Update action: `0x04` plain, `0x00`/`0x01` no-cascade/cascade | -| `0x16` | 1 | Delete action (same encoding) | +| `0x16` | 1 | Delete action: the same, plus `0x02` set null ([page-02d](page-02d-constraints.md)) | | `0x17` | 1 | Index type: `0x00` secondary, `0x01` primary, `0x02` foreign | | `0x18` | 4 | Unknown / reserved (zero) — trailing bytes of the 28-byte block | @@ -209,8 +209,9 @@ descriptor. **Inline (type `0x00`):** `[0x00][startPage:4][bitmap…]` — bit `i` ⇒ page `startPage+i` owned. **Reference (type `0x01`, 69 bytes):** `[0x01][17 × 4-byte bitmap-page pointers]`. **Bitmap page (page type `0x0105`):** header `[05 01][00 00]` — the type word, then two zero bytes — bitmap from offset 4. -Global maps, located by page 0: free pages at `0x18` (page 1 row 0 as ACE writes it) — set bit = **free** -(opposite of a table map); released pages at `0x1C` (page 1 row 1) — set bit = freed, not reusable until close. +Global maps, located by page 0's pointers wherever they point: free pages at `0x18` (page 1 row 0 where ACE +creates it) — set bit = **free** (opposite of a table map); released pages at `0x1C` (page 1 row 1 where ACE +creates it) — set bit = freed, not reusable until close. --- @@ -254,7 +255,7 @@ Global maps, located by page 0: free pages at `0x18` (page 1 row 0 as ACE writes ## Catalog tables → [system-catalog](system-catalog.md) -- **MSysObjects** (TDEF page 2): `Id`, `Name`, `Type` (1 table, 2 database, 3 container, 5 query/view, 6 linked table, 8 relationship; negative for Access documents — see system-catalog §11 *Object kinds*), `Flags`, `ParentId` (the container; `Name` is unique within it), `Owner`, `DateCreate`/`DateUpdate`, `LvProp` (property blob). +- **MSysObjects** (TDEF named by page 0's `0x20`): `Id`, `Name`, `Type` (1 table, 2 database, 3 container, 5 query/view, 6 linked table, 8 relationship; negative for Access documents — see system-catalog §11 *Object kinds*), `Flags`, `ParentId` (the container; `Name` is unique within it), `Owner`, `DateCreate`/`DateUpdate`, `LvProp` (property blob). - **MSysACEs** (4 cols): `ObjectId`, `SID`, `ACM`, `FInheritable` — two rows per object. - **MSysQueries** (8 cols): `ObjectId`, `Attribute`, `Flag`, `Name1`, `Name2`, `Expression`, `Order`, `LvExtra`; PK `(ObjectId, Attribute, Order)`. - **MSysRelationships**: `szRelationship`, `szObject`, `szColumn`, `szReferencedObject`, `szReferencedColumn`, `icolumn`, `ccolumn`, `grbit` (`0x01` one-to-one, `0x02` don't-enforce, `0x100` cascade-update, `0x1000` cascade-delete, `0x2000` delete-set-null, `0x1000000`/`0x2000000` join type: all records from the parent / from the child). The cascades ACE applies come from the index-info blocks' `0x15`/`0x16`, not from these bits. @@ -293,8 +294,8 @@ table records how each ceiling is actually held, not merely that it exists. | Narrow field | Ceiling it imposes | How it is held | | --- | --- | --- | -| Index leaf entry addresses a row as `page << 8 \| row` — 1 byte of slot | **255 rows per data page**: the pointer allows 256 but **ACE writes at most 255**, not for space (a filled page keeps ~2,297 of 4,096 bytes free) and it drops the page from the free-pages map on reaching it | **Enforced at ACE's 255.** `FindPageWithRoom` refuses a page at `RowPointer.MaxRowsPerPage`, covering both the insert path and `WriteHiddenRow` (a relocation target is named by the same pointer). Overfilling costs more than indexed reads — ACE parses the full 16-bit count but caps at 256 slots, so it silently cannot see the rest of the page's rows | -| Long-value descriptor names its row in 1 byte (`d[4]`) | **256 rows per LVAL page** | **Enforced** in `TryAppend`. Unreachable in practice — a payload ≤ 64 bytes inlines, and the free-map drop at `MinLvalRow` (258 bytes free) caps a page at 104 rows even for the smallest thing that can arrive (a 33-character memo compressed to 35 bytes; compression is applied *after* the inline test, so the floor is below the 65 bytes the inline limit suggests) | +| Index leaf entry addresses a row as `page << 8 \| row` — 1 byte of slot | **255 rows per data page**: the pointer allows 256 but **ACE writes at most 255**, not for space (a filled page keeps ~2,297 of 4,096 bytes free) and it drops the page from the free-pages map on reaching it | **Enforced at ACE's 255.** `FindPageWithRoom` refuses a page at the format's `MaxRowsPerPage`, covering both the insert path and `WriteHiddenRow` (a relocation target is named by the same pointer). Overfilling costs more than indexed reads — ACE parses the full 16-bit count but caps at 256 slots, so it silently cannot see the rest of the page's rows | +| Long-value descriptor names its row in 1 byte (`d[4]`) | **256 rows per LVAL page** | **Enforced** in `TryAppend`. Unreachable in practice — a payload ≤ 64 bytes inlines, and the free-map drop below `MinLvalValue` (256) plus its 2-byte slot (258 bytes free) caps a page at 104 rows even for the smallest thing that can arrive (a 33-character memo compressed to 35 bytes; compression is applied *after* the inline test, so the floor is below the 65 bytes the inline limit suggests) | | Page numbers are 3 bytes in the TDEF usage-map pointer, the long-value descriptor (`d[5..7]`) and an LVAL chunk's next-pointer | **page < 2²⁴** (16,777,216) | **Safe with 32× headroom**, because `PageChannel.WritePage` enforces the 2 GiB file limit at 524,288 pages. The 24-bit fields are never the binding constraint | | Usage-map pointer names its record row in 1 byte | **256 records per usage-map page** | **Safe by a louder guard.** A record is 69 bytes, so `AppendEmptyUsageMapRow`'s space check admits 57 and refuses the 58th — 4.5× tighter than the byte — and it throws rather than truncating | | Reference usage map holds 17 bitmap-page slots | **~2.28 GB of page coverage** | **Enforced** — `NotSupportedException` on both the set-bit and inline→reference conversion paths. Just past the 2 GiB file limit, by design | diff --git a/src/LibRed/docs/format/data-types.md b/src/LibRed/docs/format/data-types.md index b3707315..334607bc 100644 --- a/src/LibRed/docs/format/data-types.md +++ b/src/LibRed/docs/format/data-types.md @@ -107,7 +107,7 @@ a declaration to something its existing data would violate. > zero**. `1.23456` into a `DECIMAL(18,4)` stores `1.2345`, `1.99999` stores `1.9999`, `-1.23455` stores > `-1.2345` — truncation in every case, over both midpoint parities and both signs. > -> LibRed matches it in `JetTypeCodec.EncodeNumeric`, and `IndexKeyEncoder.EncodeFixedPoint` **must** quantise +> LibRed matches it in `JetTypeCodec.EncodeNumeric`, and `IndexKeyCodec.EncodeFixedPoint` **must** quantise > identically: the key is the same unscaled integer the row stores, so quantising one differently files a value > under a number its row does not contain. Rounding (`decimal.Round(…, 0)`, `ToEven`) is the obvious > implementation and is wrong: it stores `1.2346` and `2.0000` for ACE's `1.2345` and `1.9999`, and a row and @@ -128,7 +128,7 @@ the row keeps the bytes; the encoding itself is [§10.4](page-03-04-index-btree. **`GUID` is variable-length too — but only where ACE's DDL made it.** Every GUID column ACE's SQL creates carries length 16 with the fixed flag clear, whatever the table's width (it is not a fallback for wide tables), and `SELECT … INTO` produces the same. ACE's **own system tables are the exception**: -`MSysComplexType_GUID` `Value` is *fixed* (verified), and `DatabaseCreator` reproduces that — so "GUID is +`MSysComplexType_GUID` `Value` is *fixed* (verified), and `JetDatabase` reproduces that — so "GUID is variable" is a rule about declarations, not about GUID storage everywhere. Unlike BIGINT this is not a wrong-value hazard: ACE reads a value back correctly from either layout @@ -283,7 +283,7 @@ Points verified against ACE that aren't obvious from that page: amount of data you attempted to add."* The fixed form is the one worth recording: because ACE stores fixed text space-padded to the full width, a writer that pads is one line away from silently *truncating* the over-long case instead of refusing it. The width check therefore belongs on the encode path for fixed - columns (`JetTypeCodec.EnsureFitsFixedWidth`, before padding) and on the shared row-assembly path for variable ones (`RowEncoder.AssembleRow`, so the ALTER re-lay passes it too). + columns (`JetTypeCodec.EnsureFitsFixedWidth`, before padding) and on the shared row-assembly path for variable ones (`RowCodec.AssembleRow`, so the ALTER re-lay passes it too). - **Narrowing an existing column is checked against its rows.** `ALTER TABLE … ALTER COLUMN c TEXT(5)` on a column holding wider values is refused rather than leaving rows that violate the declaration — and so is a retype whose converted values are too wide: a Memo or a `LONG` to `TEXT(1)`, a `LONG` to `BINARY(1)`. ACE @@ -297,7 +297,7 @@ Points verified against ACE that aren't obvious from that page: 255/510 above — while an explicit `(p)`/`(p,s)` is stamped exactly as written; the column is 17 bytes and fixed-length either way. **A precision of 0 cannot be declared at all**, which fits ACE's own OLE DB reader being unable to materialise such a column — a 0 leaves the value no declared shape to be read back into. - LibRed rejects out-of-range dimensions in `AccessTypeMapper` and, because a direct Core caller bypasses that, in `TdefBuilder` too; an unspecified + LibRed rejects out-of-range dimensions in `AccessTypeMapper` and, because a direct Core caller bypasses that, in `TableDefinition` too; an unspecified precision resolves to 18 on write rather than reaching the file as 0. - The grammar parses **two-word** type names (`CHARACTER VARYING`, `BIT VARYING`); three-word (`NATIONAL CHARACTER VARYING`) is not parsed yet. `HYPERLINK`/`XML`/`SQL_VARIANT`/`VARIANT`/`COMP` have no diff --git a/src/LibRed/docs/format/long-values.md b/src/LibRed/docs/format/long-values.md index 88ff6b30..3209149f 100644 --- a/src/LibRed/docs/format/long-values.md +++ b/src/LibRed/docs/format/long-values.md @@ -176,7 +176,7 @@ the long values: > **Writing.** LibRed inlines a memo/OLE value only up to **64 bytes** (same for Jet3/Jet4): the 12-byte > descriptor with length + the `0x80` flag (bytes `0x04`–`0x0B` zero) then the payload (memo = UTF-16LE, > OLE = raw bytes). A value of **65–3816 bytes** is written as one row on an **LVAL page** (`0x40` -> descriptor, `LongValueWriter`; rows share a page, see below) — `RowInserter` materialises it before +> descriptor, `LongValueStore`; rows share a page, see below) — `RowInserter` materialises it before > encoding. This matters for Access, not just LibRed: Access > tolerates an inline value its reader resolves, but **rejects an over-64-byte value inlined** (e.g. it > opens the database yet fails to *run* a view whose subquery `Expression` was inlined; on an LVAL page @@ -236,7 +236,7 @@ the long values: > §3.3.2 `used_pages`/`free_pages` pointers, and the index blocks' `+0x22` pointers, carry the resolved > (row, page). For a fresh table all these maps are empty. When LibRed writes a value to an LVAL page (§8), > it **sets that page's bit in the column's owned-pages *and* free-pages maps** — both §3.3.2 -> pointers are parsed from the TDEF (`TableDefinitionPage.LongValueOwnedMaps` / `LongValueFreeMaps`, keyed +> pointers are parsed from the TDEF (`TableDefinition.LongValueOwnedMaps` / `LongValueFreeMaps`, keyed > by column id) and the inline bitmap bit is set. **Pages are packed like Access:** a value up to one row > is appended to the first **free-map** page with room (many small values share a page as separate rows); > only when none has room is a fresh page allocated (owned + free). A page is dropped from the free map @@ -250,9 +250,9 @@ the long values: > tables' larger DEFAULT/CHECK blobs share one LvProp page. Verified against ACE: a table's blob of 51–63 > bytes (a single `Required` or `DefaultValue`) is inline in its MSysObjects row, and one of 65 bytes or more > is on a page. A chained value uses dedicated pages. A page outside the inline -> map's window is handled by the shared `UsageMapWriter.SetBit`, which grows the inline record in place and +> map's window is handled by the shared `UsageMap.SetBit`, which grows the inline record in place and converts it to a reference map when it no longer fits — a long-value column's maps are not a special case, -and `MapPages` reads either form back. +and `UsageMap.PagesInMap` reads either form back. > **The terminating `0xFFFF` is mandatory on write — even for a table with no long-value > columns** (where the list is empty and the `0xFFFF` is the only bytes here). Omitting it makes diff --git a/src/LibRed/docs/format/page-00-database.md b/src/LibRed/docs/format/page-00-database.md index b7a89859..aedeec3a 100644 --- a/src/LibRed/docs/format/page-00-database.md +++ b/src/LibRed/docs/format/page-00-database.md @@ -14,9 +14,9 @@ | `0x15` | 1 | Version **minor** byte: **`0x01` on a database created in the 2010 format (version `0x03`)**, `0x00` when created in any other. **A version raise writes `0x00`** whatever the target — including a raise *onto* `0x03`. Purpose otherwise unknown | | `0x16` | 2 | Unknown (zero observed) | | `0x18`–`0x98` | 128 | **Obfuscated header** — XOR'd with the RC4 keystream of the key `C7 DA 39 6B` (§2.1). Jet 3 masks 126 bytes. Fields below are offsets into it. | -| `0x18` | 4 | **Global free-pages map pointer** — `[row:1][page:3]`; `0x00000100` = page 1 row 0 in every file ACE writes ([page-05 §9.1](page-05-usage-maps.md)) | -| `0x1C` | 4 | **Global released-pages map pointer** — `[row:1][page:3]`; `0x00000101` = page 1 row 1 ([page-05 §9.1](page-05-usage-maps.md)) | -| `0x20`–`0x2C` | 4×4 | **System-catalog bootstrap pointers**: TDEF pages of `MSysObjects` / `MSysACEs` / `MSysQueries` / `MSysRelationships` = `2, 3, 4, 5`. `0x20` is the **catalog root** (how the engine finds `MSysObjects`). | +| `0x18` | 4 | **Global free-pages map pointer** — `[row:1][page:3]`; `0x00000100` = page 1 row 0 where ACE creates it; ACE follows the pointer wherever it points ([page-05 §9.1](page-05-usage-maps.md)) | +| `0x1C` | 4 | **Global released-pages map pointer** — `[row:1][page:3]`; `0x00000101` = page 1 row 1 where ACE creates it ([page-05 §9.1](page-05-usage-maps.md)) | +| `0x20`–`0x2C` | 4×4 | **System-catalog bootstrap pointers**: TDEF pages of `MSysObjects` / `MSysACEs` / `MSysQueries` / `MSysRelationships` — wherever the creator put them (`2, 3, 4, 5` in a file the reference engine creates). `0x20` is the **catalog root** (how the engine finds `MSysObjects`). | | `0x30`–`0x3B` | 12 | In a workgroup file (`Jet System DB`), `0x30`/`0x34` hold the TDEF pages of `MSysAccounts`/`MSysGroups`; zero in an ordinary database, which has neither table. ACE range-checks `0x30` and `0x34` as `[row:1][page:3]` pointers, against the largest possible page only (see below) | | `0x3C` | 2 | **ANSI code page** of the collation's language — LE (`0x04E4` = 1252, `0x04E2` = 1250, `0` = none); see below | | `0x3E` | 4 | **Database (encryption) key** — 0 when there is no password | @@ -62,7 +62,7 @@ files fall back to `0x03` (verified; Access 2013 defaults to the 2007-2016 forma > byte cannot be opened by any provider; restamping `0x14` to `0x03` makes the identical bytes open. > > The format at `0x04` is otherwise exactly `0x03`, so LibRed is deliberately **asymmetric**: `FromVersionByte` -> accepts the byte and reads the 2010 layout, while `DatabaseCreator.CreateEmpty` refuses to write it and points +> accepts the byte and reads the 2010 layout, while `JetDatabase.Create` refuses to write it and points > the caller at `Version14_2010`. A genuinely **unknown** version byte on an `.accdb` that still carries the cleartext `"4.0"` engine string at @@ -115,18 +115,21 @@ ten past the end of a 353-page file, or page 500,000, opens as readily — which exist" check `0x18` and `0x1C` get. `0x38` takes any value at all. **What they point at (verified):** the workgroup tables. In a workgroup file `0x30` holds the TDEF page of -`MSysAccounts` and `0x34` that of `MSysGroups` — pages 6 and 7, straight after the four catalog TDEFs at -`0x20`–`0x2C` — each with the row byte zero, so they read the same as a plain page number or as `[row][page]`. +`MSysAccounts` and `0x34` that of `MSysGroups` — wherever the creator put them (straight after the four catalog +TDEFs where Jet creates a workgroup file) — each with the row byte zero, so they read the same as a plain page number or as `[row][page]`. `0x38` is zero there too. An ordinary database has neither table and carries zero in both; zero is accepted, and a compact zeroes a value poked into one. A writer must leave all three as it found them. **Catalog bootstrap.** Reading the database is a two-step hop from page 0: the pointer at `0x20` gives the -`MSysObjects` TDEF page (2), and `MSysObjects` then lists every other object (each table's row `Id` is *its* -TDEF page). LibRed reads `0x20` into `DatabaseDefinitionPage.CatalogRootPage` and hands it to `JetCatalog` -(falling back to page 2 only if it reads 0). Verified: the four pointer values equal the objects' +`MSysObjects` TDEF page, and `MSysObjects` then lists every other object (each table's row `Id` is *its* +TDEF page). LibRed reads `0x20` into `DatabaseDefinitionPage.CatalogRootPage` and hands it to `JetCatalog`, +with no fallback: `MSysObjects`' own row `Id` names the same page, but that row can only be read once the +table has been found. Verified: the four pointer values equal the objects' `MSysObjects.Id` and each names a real TDEF page. -> **Creation from scratch (implemented — `DatabaseCreator`).** A minimal bootable page 0 needs the mask, the +> **Creation from scratch (implemented — `JetDatabase.Create`).** Catalog bootstrap definitions and initial +> registration live in `JetCatalog`; the global usage-map builder lives in `UsageMap`. +> A minimal bootable page 0 needs the mask, the > code page / collation / creation date, and this pointer block aimed at the four core system tables > (`MSysObjects`, `MSysACEs`, `MSysQueries`, `MSysRelationships`) — the minimum catalog a new file must > contain. LibRed synthesises all of this natively (no DAO/ADOX, no template copy) and the result opens @@ -136,6 +139,11 @@ TDEF page). LibRed reads `0x20` into `DatabaseDefinitionPage.CatalogRootPage` an > (with the nav-pane long SID) on first open (verified). > **How the reference engine lays out a new file** (DAO-created ACE 12, 42 pages). +> **None of these placements is part of the format.** Every structure here is found through a pointer — the +> global maps through `0x18`/`0x1C`, the core TDEFs through `0x20`–`0x2C`, each table's usage maps through its +> TDEF's `0x37`/`0x3B`, each index root through its index-data block, every other TDEF through its +> `MSysObjects.Id` — so any of them may sit on any page. This is only the order ACE allocates in when it +> creates a file, and a reader or writer must follow the pointers rather than assume it. > Per table the allocation order is **TDEF → usage-map page → one page per index root**, in table-creation > order; both usage maps share one page (owned = row 0, free = row 1, inline), which is what every TDEF's > `0x37`/`0x3B` pointers show. A **foreign key's** index root comes after its relationship's @@ -146,7 +154,7 @@ TDEF page). LibRed reads `0x20` into `DatabaseDefinitionPage.CatalogRootPage` an > | pages | contents | > | --- | --- | > | `0`, `1` | database definition; the global free-pages (row 0) and released-pages (row 1) maps | -> | `2`–`5` | the four core TDEFs — fixed, because page 0's bootstrap pointers name them | +> | `2`–`5` | the four core TDEFs — here by allocation order alone; page 0's bootstrap pointers are what name them | > | `6`, `9`, `11`, `13` | usage maps for MSysObjects / MSysACEs / MSysQueries / MSysRelationships | > | `7`, `8`, `10`, `12`, `14`–`16` | their index roots (2 + 1 + 1 + 3), each a leaf page | > | `17` | MSysObjects' first data page — the catalog rows | @@ -170,18 +178,18 @@ Observed values: | value | engine build | | --- | --- | | `0x000011A6` (4518) | ACE `12.0.4518` — Office 2007 RTM | +| `0x00002649` (9801) | Jet `4.0.9801` | | `0x0000261C` (9756) | Jet `4.0.9756` | | `0x000021AA` (8618) | Jet `4.0.8618` | | `0x00000B6F` (2927) | Jet `4.0.2927` — SP3 | | `0x000009D9` (2521) | Jet `4.0.2521` — RTM | **ACE writes 4518 unconditionally** — every ACE version (12/14/16/17) stamps its own RTM build, including when -writing a Jet 4 `.mdb`. A Jet 4 build appears only in a file a real `msjet40.dll` created, and is that engine's -service-pack build. +writing a Jet 4 `.mdb`. A Jet 4 build is stamped by `msjet40.dll`, and is that engine's service-pack build. > That the value *is* an engine build is **inferred** — from the observed values coinciding with shipped build > numbers — not measured. That the field varies, and varies only across Jet 4 files of differing vintage, is -> measured. LibRed writes 4518. +> measured. LibRed writes `0x2649` (9801) when it creates a Jet 4 `.mdb` and 4518 when it creates an ACCDB. ### 2.1 The obfuscated header (`0x18`–`0x98`) @@ -191,9 +199,9 @@ not a per-file salt. Past the window the bytes are in the clear: a fixed constan the NUL-terminated ASCII engine-version string **`"4.0"`** at `0x9C` (the Jet 4.0 version — identical in `.mdb` and `.accdb`), then zero padding. -**The mask.** The keystream's first 128 bytes are below; LibRed holds them as a constant -(`JetFormatBase.PageZeroHeaderMask`) and de-obfuscates the whole region once in -`DatabaseDefinitionPage.Read`. RC4 under `C7 DA 39 6B` reproduces them exactly (verified): +**The mask.** The keystream's first 128 bytes are below. RC4 under `C7 DA 39 6B` reproduces them exactly +(verified), so LibRed generates the mask from the key and length its format names +(`JetFormatBase.PageZeroHeaderMask`) and de-obfuscates the whole region once in `DatabaseDefinitionPage.Read`: ``` B5 6F 03 62 61 08 C2 55 EB A9 67 72 43 3F 00 9C ; 0x18 @@ -248,7 +256,7 @@ no-password files and an empty password that unmasks to the creation-date-derive whole-file encryption, password change and removal, and against an Access-encrypted file). The field changes with every new key, so Access's encryption moves the SID keystream (§2.3). `DatabaseEncryption` writes it the same way on every `.accdb` set, change and removal. -- **Writing it:** `DatabaseCreator.CreateEmpty(path, version, collation)` (and +- **Writing it:** `JetDatabase.Create(path, version, collation)` (and `LibRedConnection.CreateDatabase(connectionString, collation)`) set this pair, defaulting to General-Legacy. The chosen collation goes into page 0 *and* into the system tables' column descriptors, and `JetDatabase.Collation` reads it back so every table created later inherits it — matching Access, which @@ -347,7 +355,7 @@ unregistered slot — matching the idle `00 01`. > **Creation must seed this.** A freshly created file has no users, so every slot must be the neutral > `00 01`, **not** zero — an all-zero table reads as "every user is mid-write," which Access rejects as -> corrupt. `DatabaseCreator.BuildDefinitionPage` fills `0xE00`–`0xFFF` with the repeating `00 01`. +> corrupt. `DatabaseDefinitionPage.Build` fills `0xE00`–`0xFFF` with the repeating `00 01`. ### 2.3 Creation date ⇄ security-SID coupling (verified) @@ -389,7 +397,8 @@ by the `Engine` account (`02-03`) in every file, so `mask = MSysObjects.Owner ^ account follows from it: an owner of `680E` gives mask `6A-0D`, under which that file's `690C` / `680C` / `6809` are `admin` / `Users` / `Creator`. That is how a writer adding an object to a file it did not create gets the SIDs right ([system-catalog §11](system-catalog.md)). The whole keystream also follows from page 0 alone, by the -derivation below, which is what serves when that row cannot be read. +derivation below — and only page 0 is sure to: after a key change Access can leave `MSysObjects`' own owner +un-re-masked (below), so its row then gives the old mask. LibRed takes every SID from page 0. The keystream is **bound to the exact millisecond-precise creation-date `double`** at `0x72`: a file with self-consistent SIDs but a *different* creation date is rejected with *"Record(s) cannot be read; no read @@ -453,9 +462,9 @@ key = LE32(pageNumber XOR databaseKey) ``` and the page bytes are the RC4 keystream XOR'd over the plaintext. This is the same per-page key mixing ACE -Agile uses (`LE32(pageNumber) XOR encodingKey`), just feeding RC4 directly instead of deriving an AES IV. +Agile uses (`LE32(pageNumber XOR databaseKey)`), just feeding RC4 directly instead of deriving an AES IV. Verified against a real `System.mdw`: the page-number mixing is XOR, not ADD; every page decrypts to a valid -page type (page 1 → `0x0101` data, pages 2/3 → `0x0102` TDEF, index pages → `0x0104`), `MSysObjects`/`MSysACEs` +page type (the global map holder → `0x0101` data, the catalog TDEFs → `0x0102`, index pages → `0x0104`), `MSysObjects`/`MSysACEs` parse, and `MSysAccounts` yields the account SIDs in §2.3. Implemented as `LibRed.Crypto.JetLegacyEncryption`; `PageChannel` selects it for non-ACE (`!IsAccdb`) files with a nonzero database key. diff --git a/src/LibRed/docs/format/page-01-data-and-rows.md b/src/LibRed/docs/format/page-01-data-and-rows.md index 9d8d54d0..f340e3c3 100644 --- a/src/LibRed/docs/format/page-01-data-and-rows.md +++ b/src/LibRed/docs/format/page-01-data-and-rows.md @@ -23,7 +23,7 @@ > overfill the page, and the rows still scan back through a slot-walking reader, since a scan never forms a > pointer — which makes it look harmless. It is not: every row past slot 255 is **unaddressable by any > index**, its entry aliasing a different row on another page, and **ACE cannot read those rows at all**. -> `RowInserter`'s page placer refuses a page at `RowPointer.MaxRowsPerPage` instead of trusting free space +> `RowInserter`'s page placer refuses a page at the format's `MaxRowsPerPage` instead of trusting free space > alone. > > > **ACE reads the full 16-bit count and then caps at 256 slots.** Pages of 400 / 400 / 100 rows read back @@ -45,7 +45,7 @@ > correctly (600 rows across three pages), where a one-byte reader would see `0x00` for those pages and > report 88. > -> LibRed's `RowPointer.MaxRowsPerPage` is therefore **255**, matching ACE rather than the pointer's maximum, +> LibRed's `MaxRowsPerPage` (`Jet4Format`) is therefore **255**, matching ACE rather than the pointer's maximum, > so every page it writes is a shape Access also produces. Row slot entry: lower 13 bits (`& 0x1FFF`) = the row's byte offset in the page; `0x8000` = diff --git a/src/LibRed/docs/format/page-02a-tdef.md b/src/LibRed/docs/format/page-02a-tdef.md index 7ba0eba2..bcbf41d8 100644 --- a/src/LibRed/docs/format/page-02a-tdef.md +++ b/src/LibRed/docs/format/page-02a-tdef.md @@ -14,7 +14,7 @@ | `0x10` | 4 | Row count | | `0x14` | 4 | **Highest AutoNumber value assigned** = the id of the last row inserted (the *next* id is this **`+ increment`**, see `0x18`); `0` when the table has no AutoNumber column. On a freshly created custom counter it is **`Seed - Increment`** so the first insert yields the `Seed` (verified: `COUNTER(1000, 7)` → `0x14` = `993`, first id `1000`). Verified **directly against `@@IDENTITY`**. It is not the row count, and deleted ids are not reused: insert 3 rows, delete id `3`, insert again (assigned id `4`) → `0x14` = `4` while the row **count** is `3`. mdbtools labels this *"Next autonumber value"* — that's **off by one**; the stored value is the last assigned, and the next id is `+ increment`. It is a **plain signed int32 that wraps** — there is no "counter exhausted" state (see the wrap note below). **Write requirement:** a writer inserting into an AutoNumber table must advance this to the last id it writes (LibRed does so in `RowInserter`); leaving it stale makes Access reissue an existing id and reject the insert as a duplicate primary key — verified end-to-end. **This pair (`0x14`/`0x18`) describes exactly one column**, and a reader must not apply it to every column bearing the descriptor's `0x04` AutoNumber flag: complex columns carry that flag too and are allocated from `0x1C` instead, so a table can have several flagged columns while only the non-complex one has a seed and increment here (`MSysResources` has two — `Id` and the complex `Data`; `complex1.accdb`'s `Table1` has five). | | `0x18` | 4 | **AutoNumber increment** — a **signed 32-bit int** (same width as `0x14`); the step added to `0x14` for each new id. Default `1` (a plain `COUNTER`); a custom `COUNTER(seed, increment)` / `AUTOINCREMENT(seed, increment)` / `INTEGER IDENTITY(seed, increment)` sets it. **Confirmed a full int32, not a byte + 3 unknown** (verified vs ACE): `COUNTER(1, 300)` → `2C 01 00 00` (spans 2 bytes, ids `1, 301, 601`); `COUNTER(5, 100000)` → `A0 86 01 00` (3 bytes); and decisively `COUNTER(100, -5)` → `FB FF FF FF` = `-5` in two's-complement (all 4 bytes) with a **descending** sequence `100, 95, 90`. It reads `1` on every table (autonumber or not) because that is the default increment — mdbtools labels it a 1-byte constant / "autonumber enable" flag, which only *looks* right because the default increment is 1. The seed itself is not stored separately — it is recovered as `0x14 + increment` (correct on a freshly-created, un-inserted table). A writer/reader must treat it as a signed int32; the insert bump of `0x14` moves in the increment's direction (max for +, min for −) so a descending counter doesn't reissue an id — except at the int32 wrap, where the generated id is the correct continuation despite comparing as backwards (see the wrap note below). | -| `0x1C` | 4 | Complex-type AutoNumber (mdbtools `ct_autonum`) — the high-water **record** id for a table that has a complex (multi-value / attachment) column. Each row's inline 4-byte value in such a column is one of these ids, allocated when the **row** is created, whether or not any value is ever stored against it. There is **one id per row, shared by every complex column of the table** — the counter is a single TDEF word, not one per column: `complex1.accdb`'s `Table1` has four attachment columns and each record carries the same id in all four (verified, as is the two-column case in `LIBRARY.accdb`'s `Borrow`). Deleting a row does **not** roll the counter back. **Verified** across 108 tables carrying a non-zero value: it equals the largest live inline id, or exceeds it where rows have been deleted (`PasesDeSalida` `14` = max `14`; `Book` `10` against max `5`; `XSDFiles` `242` against max `1`). **It counts records, not values** — measured by changing one file: adding three attachments to an existing row of `complex1.accdb` left `0x1C` at `3` while the flat table's own `0x14` went `0` → `3`. Still `0` for a table with no complex column, which is what `TdefBuilder` writes. **Read into `TableDef.ComplexAutoNumber`** so it round-trips via the model, not only the raw surgery path. The *per-value* ids inside the flat table are **not** this counter — those come from an ordinary AutoNumber column with its own `0x14`; see [system-catalog.md](system-catalog.md), "Complex columns" | +| `0x1C` | 4 | Complex-type AutoNumber (mdbtools `ct_autonum`) — the high-water **record** id for a table that has a complex (multi-value / attachment) column. Each row's inline 4-byte value in such a column is one of these ids, allocated when the **row** is created, whether or not any value is ever stored against it. There is **one id per row, shared by every complex column of the table** — the counter is a single TDEF word, not one per column: `complex1.accdb`'s `Table1` has four attachment columns and each record carries the same id in all four (verified, as is the two-column case in `LIBRARY.accdb`'s `Borrow`). Deleting a row does **not** roll the counter back. **Verified** across 108 tables carrying a non-zero value: it equals the largest live inline id, or exceeds it where rows have been deleted (`PasesDeSalida` `14` = max `14`; `Book` `10` against max `5`; `XSDFiles` `242` against max `1`). **It counts records, not values** — measured by changing one file: adding three attachments to an existing row of `complex1.accdb` left `0x1C` at `3` while the flat table's own `0x14` went `0` → `3`. Still `0` for a table with no complex column, which is what `TableDefinition` writes. **Read into `TableDefinition.ComplexAutoNumber`** so it round-trips via the model, not only the raw surgery path. The *per-value* ids inside the flat table are **not** this counter — those come from an ordinary AutoNumber column with its own `0x14`; see [system-catalog.md](system-catalog.md), "Complex columns" | | `0x20` | 8 | **Uninitialised — not a field.** ACE leaves these 8 bytes as whatever its write buffer held: **non-zero in 754 of 1,676 tables** measured across the corpus, carrying Windows user-mode pointers (`00007FFDF1459118`), UTF-16 and ASCII text fragments (`00460020006D0061` = `a m F`, `65706F7250206D6F` = `om Prope`), float bit patterns (`41B0000044D82000`) and `FFFFFFFFFFFFFFFF`. One value recurs across tables written in the same operation — eight consecutive `MSysComplexType_*` tables share one, three unrelated tables in another file share another — which is buffer reuse rather than meaning. Preserve verbatim: it can be neither validated nor regenerated. (Whether ACE *reads* anything here is untested; it writes pointers, so almost certainly not.) | | `0x28` | 1 | Table type: `0x4E` 'N' user, `0x53` 'S' system | | `0x29` | 2 | Maximum column count | @@ -142,10 +142,10 @@ and a path that re-derives a rule from the *live* columns gets it wrong. The table is the enforcement surface. A blank cell means the path cannot reach that invariant, not that it is exempt. -| invariant | `RowEncoder`
(INSERT/UPDATE) | `BuildRelaidRecord`
(in-place ALTER) | `AddColumn` | `DropColumn` | `AlterColumn` retype | +| invariant | `RowCodec`
(INSERT/UPDATE) | `BuildRelaidRecord`
(in-place ALTER) | `AddColumn` | `DropColumn` | `AlterColumn` retype | |---|---|---|---|---|---| | `colCount` + null-bitmap width = `max(live column id) + 1` | ✅ | ✅ `newMaxId` | | | | -| variable slots per row = `0x2B` | ✅ via `TableDef.VariableColumnCount` | ✅ appends onto a full-width row | | | | +| variable slots per row = `0x2B` | ✅ via `TableDefinition.VariableColumnCount` | ✅ appends onto a full-width row | | | | | variable index from `0x2B`, hole abandoned | | | ✅ | ✅ leaves `0x2B` | ✅ | | fixed offset = live `max(offset+length)`, hole **reused** | ✅ | ✅ `newFixedLen` from the old row | ✅ | ✅ no renumber | ⚠️ **NOT the live max** — measure the fixed-region end from an existing row's var-data start, because a prior retype's dead slot makes the descriptors under-count it ([§3.8](page-02b-columns.md)) | | fixed region never shrinks below existing rows | ✅ `InferFixedDataLength` takes `max(pinned, derived)` | ✅ derived from the old row | | | | @@ -204,7 +204,7 @@ the documented 255-column, 32-index, and 64-character-name limits. > `0x04` points to the first continuation; each continuation carries up to `PageSize − 8` bytes (from offset > 8), definition first and then the reserve, as above — so a page the reserve fits on after its last data has > free space `PageSize − 8 − dataLen − 8`, and a page that is full has `0`. The definition-length field -> (`0x08`, on the first page) is the **total** length across all pages. LibRed writes this in `TableCreator.WriteDefinition`, used when `CREATE INDEX` +> (`0x08`, on the first page) is the **total** length across all pages. LibRed writes this in `TableDefinition.WriteChain`, used when `CREATE INDEX` > grows a definition past one page (verified: a 30-column, 30-index table spills to one continuation > page, `defLen 4115`, exactly as ACE writes it, and Access reads every index). @@ -217,7 +217,7 @@ the documented 255-column, 32-index, and 64-character-name limits. > `first → n+1 → n` (a `CREATE TABLE`'s chain runs the same way). When a definition falls back to one page, > the first page's `0x04` next pointer is zeroed and it follows the single-page rule below (8 bytes zeroed > past the new end, the rest left); a definition growing onto a second page keeps its first page and takes a -> fresh continuation. LibRed's `WriteDefinition` does all of this. +> fresh continuation. LibRed's `TableDefinition.WriteChain` does all of this. ### 3.3 Body layout (in order, after the header) @@ -281,8 +281,8 @@ Only a few fields are *not* fixed constants and so warrant a write note: empty index has exactly its root bit set); thereafter every page a split allocates is added, so the union of a table's index maps equals exactly the set of index pages present (verified byte-for-byte against ACE, including loads that split the trees several levels). LibRed reproduces this: - `IndexWriter.AllocateIndexPage` marks each page it allocates during a split, and `TableCreator` marks the root at creation (both `CreateTable` and `CREATE INDEX`). - Note this map is **advisory for LibRed's own reads** — `IndexWriter` navigates the B-tree structurally + `IndexTree.AllocateIndexPage` marks each page it allocates during a split, and `SchemaEditor` marks the root at creation (both `CreateTable` and `CREATE INDEX`). + Note this map is **advisory for LibRed's own reads** — `IndexTree` navigates the B-tree structurally (root child-pointers + leaf next-pointers), never by the map — but Access's maintenance relies on it, and it feeds the owned-map page-budget calculation (a growing index map shrinks the owned map's room; see §9). A **fresh table still has no data page** (Access allocates the first lazily on the first diff --git a/src/LibRed/docs/format/page-02b-columns.md b/src/LibRed/docs/format/page-02b-columns.md index 89a48379..bc0f9802 100644 --- a/src/LibRed/docs/format/page-02b-columns.md +++ b/src/LibRed/docs/format/page-02b-columns.md @@ -325,7 +325,7 @@ column's rank among the variable columns ordered by ascending id. Two limits bind a declaration, both enforced by ACE when it **opens the file**, so writing past either damages the database rather than just the table. Verified against ACE 16 (OLE DB); LibRed applies both in -`Catalog/RecordLayout.cs`, on create and on every incremental path. +`Catalog/RowCodec.cs`, on create and on every incremental path. **Per field: 510 bytes** — 255 Text characters, or 510 bytes of Binary, fixed or variable alike. ACE refuses a wider column through its own DDL identically on `CREATE TABLE`, `ALTER COLUMN` and `ADD COLUMN` diff --git a/src/LibRed/docs/format/page-02d-constraints.md b/src/LibRed/docs/format/page-02d-constraints.md index c4eb3b31..727edc72 100644 --- a/src/LibRed/docs/format/page-02d-constraints.md +++ b/src/LibRed/docs/format/page-02d-constraints.md @@ -76,7 +76,7 @@ differently from a delete: > The update rule could not be seen from an index built empty: its zero total leaves the pair alone whatever the > update does, which is how "an UPDATE changes neither count" looked right. - LibRed maintains both this way — inserts, deletes and updates in `RowInserter`, builds in `TableCreator`'s index + LibRed maintains both this way — inserts, deletes and updates in `RowInserter`, builds in `SchemaEditor`'s index back-fill, and the Memo/OLE retype (which LibRed does by rebuilding the whole table) restores every other index's counts afterwards — and exposes `+4` as `IndexDef.UniqueEntryCount`. @@ -98,8 +98,8 @@ one without it accepts the duplicate although `grbit` says one-to-one (verified | `0x30` | 4 | Unknown / reserved (zero observed) — trailing bytes of the 52-byte block | > **The 10-column cap must be enforced on the incremental path too**, as must the 32-index cap below. -> `TdefBuilder` rejects an over-wide index when a table is created with its indexes, but `CREATE INDEX` and -> `ADD FOREIGN KEY` on an existing table go through `TableCreator.InsertIndex`. A block builder that fills +> `TableDefinition` rejects an over-wide index when a table is created with its indexes, but `CREATE INDEX` and +> `ADD FOREIGN KEY` on an existing table go through `SchemaEditor.InsertIndex`. A block builder that fills > its ten slots and marks the rest unused silently stores a 10-column index for an 11-column request. ACE > refuses outright — *"Cannot have more than 10 fields in an index."* > @@ -111,7 +111,7 @@ one without it accepts the duplicate although `grbit` says one-to-one (verified > **not** `WITH IGNORE NULL`) rejects a duplicate **non-null** key but permits **multiple NULL** keys — two > rows may both be null in the indexed column(s). So uniqueness is enforced only over the non-null keys; a > row with a null in any indexed column is exempt (matching SQL's "nulls are distinct"). LibRed enforces -> this on insert/update (`IndexWriter.KeyExists`, skipping null-keyed rows), and rejects +> this on insert/update (`IndexTree.KeyExists`, skipping null-keyed rows), and rejects > `CREATE UNIQUE INDEX` when the rows that already exist are not unique — scanned up front, before the > TDEF is written, so a rejected statement leaves the file untouched. A `WITH IGNORE NULL` index > (`0x02`) additionally leaves null-keyed rows out of the B-tree entirely; a PK (`0x08` required) forbids diff --git a/src/LibRed/docs/format/page-02e-calculated-columns.md b/src/LibRed/docs/format/page-02e-calculated-columns.md index a2b6e9a9..eb99aa42 100644 --- a/src/LibRed/docs/format/page-02e-calculated-columns.md +++ b/src/LibRed/docs/format/page-02e-calculated-columns.md @@ -28,7 +28,7 @@ value is a cache of ACE's own evaluation and anything else there produces a file - `ResultType` decides how the payload is read, rather than it being inferred from the payload's width; - `CalculatedExpression` parses and validates, `CalculatedEvaluator` evaluates — every function on the whitelist below, with Access's null semantics and its per-function argument rules; -- `RowEncoder` recomputes on insert; `RowInserter.Update` does so only when a column the expression reads is +- `RowCodec` recomputes on insert; `RowInserter.Update` does so only when a column the expression reads is in the changed set; - a Memo result too large to inline spills to a long-value page through a callback `RowInserter` hands the encoder — a calculated column is the one value the encoder *derives* rather than receives, so it cannot @@ -219,7 +219,7 @@ primitives down into Core remains the answer if the duplication starts to bite. slot, the constant declared length. `CREATE TABLE` and `ADD COLUMN` write the `Expression`/`ResultType`/`FCMin*Ver` properties. A Memo result gets its long-value maps, keyed off the **result** type because the declared type is `Text` and says nothing — so neither the reader nor the -write-side guard in `TdefBuilder` may demand that a map's owner be Memo/OLE. The ACE-14 gate goes through +write-side guard in `TableDefinition` may demand that a map's owner be Memo/OLE. The ACE-14 gate goes through `EnsureFormatAtLeast`, raising the file rather than refusing, as for `BIGINT`/`DATETIME2`. **Validation runs before anything is written** (`CalculatedExpression.ParseValidated`): the whitelist above, @@ -269,7 +269,7 @@ changed set, since callers hand back the whole row they just read and the slot l cached value. **ACE has no route to change an existing calculated column's expression.** DAO refuses to mutate any field -of a *saved* `TableDef` — *"Operation is not supported for this type of object."* — and that refusal is +of a *saved* `TableDefinition` — *"Operation is not supported for this type of object."* — and that refusal is generic: an ordinary column gets it too, so it says nothing about calculated columns specifically. Access's own designer must reach it by a path DAO does not expose. There is therefore no oracle for "alter the expression", and anything LibRed offers there is its own extension rather than ACE-matching behaviour. diff --git a/src/LibRed/docs/format/page-03-04-index-btree.md b/src/LibRed/docs/format/page-03-04-index-btree.md index a575398f..b6f17c29 100644 --- a/src/LibRed/docs/format/page-03-04-index-btree.md +++ b/src/LibRed/docs/format/page-03-04-index-btree.md @@ -147,7 +147,8 @@ omits. Reconstruct: `fullEntry = prefix ++ stored`. ### 10.4 Key encoding (order-preserving) Each key column is encoded so that raw byte comparison equals value comparison. LibRed both -**decodes** these keys and **encodes** them (`IndexKeyEncoder`, the inverse), so it can insert +**decodes** and **encodes** these keys through `IndexKeyCodec`, which also owns their layout, +length limit and truncation checksum, so it can insert into an index. The encoder is verified **byte-for-byte against Access**: re-encoding the value decoded from Access's own stored key reproduces the exact bytes, and after a LibRed insert Access satisfies an indexed primary-key seek over the entry LibRed wrote. @@ -156,7 +157,7 @@ Access satisfies an indexed primary-key seek over the entry LibRed wrote. > the `(LANGID, sort id, version)` triple in its descriptor at `0x0B`–`0x0E` (§3.4) — here > (1033, 0, **0**), the Access 2000–2007 order Access later renamed "General legacy". The other orders use > the *same framing* and different weights: see **Two General orders** and **Locale-specific orders** below. -> Encoding must **gate on the whole triple** rather than assume, which is what `IndexKeyEncoder` does — it +> Encoding must **gate on the whole triple** rather than assume, which is what `IndexKeyCodec` does — it > throws on anything it has no table for instead of emitting General bytes. That matters more than it > sounds: a wrong key does not fail, it silently disagrees with ACE's. @@ -203,7 +204,7 @@ Then the value, transformed: byte-for-byte vs ACE: a 256- or 300-character memo yields exactly the key of its 255-character prefix, so two memos differing only past character 255 share a key (fine for a non-unique index). Index keys are therefore encoded from the **logical** row values, before memo/OLE - values are materialised into their `LongValueDescriptor`s. + values are materialised into their `LongValueStore.DescriptorValue`s. > **Two General orders, two weight tables.** Everything in this Text section describes **General-Legacy** > (sort-order version `0`). The Access-2010+ **General** order (version `1`, the byte at column `0x0E` / > page-0 `0x71`) uses the *same framing* — start flag, primary weights, `0x01`, secondary section, inline @@ -926,7 +927,7 @@ Then the value, transformed: 8-byte halves by a constant `0x09` marker, and terminated by `0x08` — a fixed **19-byte** key. Data bytes equal to `0x08`/`0x09` need no escaping (every field is at a fixed offset). Verified byte-for-byte against ACE; ACE also opens a LibRed-written GUID-PK - table and seeks a row by its key. Encoded/decoded by `IndexKeyEncoder`/`IndexKeyDecoder`. Example: + table and seeks a row by its key. Encoded/decoded by `IndexKeyCodec`. Example: `01020304-0506-0708-090a-0b0c0d0e0f10` → `7F 0102030405060708 09 090A0B0C0D0E0F10 08`. **Descending** inverts every byte of the ascending key **except the `0x09` field marker** (kept constant so the structure stays parseable — and it doesn't affect ordering since it's equal in every key): the @@ -947,7 +948,7 @@ Then the value, transformed: prefix order). **Descending** inverts every byte **except the `0x09` continuation markers** (mirrors GUID): flag → `0x80`, data bytes and the terminator inverted, markers unchanged. Ascending is verified byte-for-byte against ACE-written keys (single- and multi-chunk); descending is **unverified** against ACE, - extrapolated from the verified GUID descending. `IndexKeyEncoder.EncodeBinaryChunked`. + extrapolated from the verified GUID descending. `IndexKeyCodec.EncodeBinaryChunked`. ### 10.4a Entry removal — a leaf is rewritten, not repacked @@ -1047,7 +1048,7 @@ The function is **affine over GF(2)** — tails differing in one byte give `L(0x the end contributes `S^(d-1)` of itself whatever the message length. The eight table rows above follow from those contributions. -Equivalently, and how `JetIndexKeyChecksum` implements it: fold every byte but the last in the form +Equivalently, and how `IndexKeyCodec` implements it: fold every byte but the last in the form `crc = (crc >> 8) ^ T[crc & 0xFF] ^ b`, then XOR the last byte's `b << 8` into the result. The two are the same function. @@ -1087,7 +1088,7 @@ separator is the **maximum key of its child subtree**, stored as a full leaf key 4-byte row pointer), so descend into the first child whose separator `≥` the new full key, else the child-tail (`0x14`). Slot the new entry into the target leaf in key order and rewrite the page. -When a page would overflow, **split** it (LibRed's `IndexWriter`). Verified **against ACE** on a +When a page would overflow, **split** it (LibRed's `IndexTree`). Verified **against ACE** on a multi-level tree LibRed wrote: Access's indexed point seek, indexed range, full `COUNT(*)`, non-indexed scan and `SUM` all return the correct result — i.e. every row is reachable both by the tree and by the leaf-chain scan Access uses. @@ -1185,13 +1186,13 @@ The split mechanics: the same, and pages are allocated in the order those splits happen, leaves and nodes interleaved. Verified byte for byte, dead bytes included, on a tree of one node over three leaves and on one of two node levels. -> Newly allocated split pages are taken from the global free-page map (§ page 1) and are registered in the -> *index's own* owned-pages usage map as Access does — `IndexWriter.AllocateIndexPage` sets the bit for +> Newly allocated split pages are taken from the global free-page map ([page-05 §9.1](page-05-usage-maps.md)) and are registered in the +> *index's own* owned-pages usage map as Access does — `IndexTree.AllocateIndexPage` sets the bit for > every page it hands out, so the map covers the whole B-tree rather than just the root. See > [page-05 §9](page-05-usage-maps.md), which owns that rule. -> **Indexable types — coverage vs ACE (§10.4).** `IndexKeyEncoder` encodes **every type ACE lets you +> **Indexable types — coverage vs ACE (§10.4).** `IndexKeyCodec` encodes **every type ACE lets you > index**, all byte-verified: Boolean, Byte, Int16, Int32, Currency, Single, Double, DateTime, Text, GUID, > Binary, FixedPoint, Memo (its first 255 chars), **`Int64`/BIGINT** (`0x13`) and > **`DateTimeExtended`/DATETIME2** (`0x14`). ACE correctly **refuses** to index `OLE` (`0x0B`) and `Complex` @@ -1199,7 +1200,7 @@ The split mechanics: > > `Int64`/BIGINT keys exactly as Currency does — an int64, sign bit flipped, big-endian — verified against ACE > including both extremes, ascending and descending. Note its **variable-length storage does not change -> this**: the key dispatch is on the column's type, not on where the row keeps the bytes. `IndexKeyDecoder` decodes it too, unlike DATETIME2 — +> this**: the key dispatch is on the column's type, not on where the row keeps the bytes. `IndexKeyCodec` decodes it too, unlike DATETIME2 — > it is a plain fixed-width numeric key. > > `DateTimeExtended` is **not** a fixed-width numeric key. ACE runs its whole 42-byte stored value through the @@ -1208,5 +1209,5 @@ The split mechanics: > because the stored encoding is already order-preserving (both fields zero-padded to 19 digits), and it means > the value's trailing NUL is part of the key ([data-types](data-types.md)). Descending inverts every byte > except the `0x09` markers, exactly as for Binary. Verified against ACE in both directions. -> `IndexKeyDecoder` does not decode it, for the same reason it does not decode Binary or Text: the chunked +> `IndexKeyCodec` does not decode it, for the same reason it does not decode Binary or Text: the chunked > form stops the in-place walk, and the caller falls back to reading the row. diff --git a/src/LibRed/docs/format/page-05-usage-maps.md b/src/LibRed/docs/format/page-05-usage-maps.md index 1c329ba1..d7365a04 100644 --- a/src/LibRed/docs/format/page-05-usage-maps.md +++ b/src/LibRed/docs/format/page-05-usage-maps.md @@ -139,7 +139,7 @@ free = row 1); an index's map spills the same way rather than being squeezed in. > > Beware comparing thresholds across table shapes: a **primary-keyed** table and a **key-less** one > > convert at different page counts under the identical rule, because only the budget differs. A keyed > > table's index usage map also *grows* with the index's pages (its own B-tree), further shrinking the -> > owned map's budget — LibRed matches this (`IndexWriter` marks each index page it allocates). +> > owned map's budget — LibRed matches this (`IndexTree` marks each index page it allocates). > **Owned-row recycle on an index rebuild (verified vs ACE, §3.8).** When ACE rebuilds an index (e.g. an > `ALTER COLUMN` on an indexed column) it gives the index a **new** owned-pages usage-map row rather than @@ -180,20 +180,22 @@ free = row 1); an index's map spills the same way rather than being squeezed in. Besides the per-table maps, the database has two **global** usage maps, found through page 0 rather than the catalog ([page-00 §2](page-00-database.md)): -| Page 0 | Map | In every file ACE writes | +| Page 0 | Map | Where ACE creates it | | --- | --- | --- | | `0x18` | **free pages** — a set bit is a page available for allocation | page 1, row 0 | | `0x1C` | **released pages** — a set bit is a page freed but not yet reusable | page 1, row 1 | -Both are ordinary usage-map records (inline or reference form, §9) on a data page whose owner field -(`0x04`) reads `0x00000001`, each starting as a 69-byte inline map with start page `0`. In the free map a +Both are ordinary usage-map records (inline or reference form, §9) on a data page — whose owner field +(`0x04`) reads `0x00000001` where ACE creates it, though nothing checks it (below) — each starting as a +69-byte inline map with start page `0`. In the free map a **set bit means the page is free / available**, the *opposite* of a per-table owned map — verified by diffing before/after an ACE `CREATE TABLE`. **ACE follows the pointers; the location is not fixed.** Allocation uses whichever record `0x18` names, row -included: pointed at page 1 row 1, ACE allocates from that map and leaves row 0 untouched. With both maps +included: pointed at the released map's row on the same page, ACE allocates from that map and leaves the free +map's row untouched. With both maps copied to another data page and the pointers aimed there, ACE allocates, releases and reopens entirely on -that page and never reads or writes page 1; the holder page's owner field (`0x04`) is not checked. The two +that page and never reads or writes the original holder; the holder page's owner field (`0x04`) is not checked. The two pointers must name **different** records — naming the same one clears the free map when the released map is emptied, and freed pages are lost. @@ -249,9 +251,9 @@ to reference form. A non-empty one is *inferred* to be a release interrupted bef **Page allocation works through the free-pages map.** Access does **not** simply grow the file: it finds a set bit (a free page), **clears it** (marking the page used), and reuses that page — only growing the file when no free page remains. Verified: an ACE `CREATE TABLE` reuses free pages (for the TDEF, -usage map, etc.), and the only change to page 1 is one cleared bit per page taken. +usage map, etc.), and the only change to the free map's holder page is one cleared bit per page taken. -> LibRed allocates **through** this map (`PageAllocator`), found as ACE finds it — through page 0's `0x18` +> LibRed allocates **through** this map (the `PageChannel`'s single `PageAllocator`, which also owns file growth), found as ACE finds it — through page 0's `0x18` > pointer, row included — and never takes a page set in the released-pages map named at `0x1C`. A released > page at the end of the file is materialized, so the file stays contiguous, but not handed out. It takes a > free page, clears its bit, and reuses it — only growing the file when none is free — so its pages match diff --git a/src/LibRed/docs/format/page-08-released-tdef.md b/src/LibRed/docs/format/page-08-released-tdef.md index 6118899a..5e6672a5 100644 --- a/src/LibRed/docs/format/page-08-released-tdef.md +++ b/src/LibRed/docs/format/page-08-released-tdef.md @@ -25,7 +25,7 @@ TDEF is handed out and overwritten like any other free page. LibRed names it ## Writing -LibRed sets the same marker in `TableCreator.DropTable`, as the last step of giving the table's pages back +LibRed sets the same marker through `TableDefinition.MarkReleased` in `SchemaEditor.DropTable`, as the last step of giving the table's pages back (the owned/free maps, the per-column long-value maps, and the map-holder rows are covered in [page-05 §9](page-05-usage-maps.md)). diff --git a/src/LibRed/docs/format/page-09-released-data.md b/src/LibRed/docs/format/page-09-released-data.md index 98d15918..2f192431 100644 --- a/src/LibRed/docs/format/page-09-released-data.md +++ b/src/LibRed/docs/format/page-09-released-data.md @@ -76,7 +76,7 @@ scan, meeting one in a table's owned map, reads it as rows like a live data page ## Writing -LibRed does the same on both routes. Every page a row has been reclaimed from goes through +LibRed does the same on both routes, marking the existing page through `DataPage.MarkReleased`. Every page a row has been reclaimed from goes through `RowInserter.WriteReclaimedPage`, which sets the type, clears the page from the owning maps and returns it to the allocator once nothing live is left and the page is not the table's first; `RowInserter.ReleasePackedValue` applies the same rule to an LVAL page against the column's maps. A long-value page that survives goes back diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index 56056d10..223c936d 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -4,13 +4,14 @@ ## 11. System catalog -- **MSysObjects** (TDEF at page **2**) lists every object. Columns include `Id`, `Name`, +- **MSysObjects** (its TDEF is the page page 0's catalog-root pointer at `0x20` names) lists every object. Columns include `Id`, `Name`, `Type`, `Flags`, `ParentId`. For a **table** object (`Type == 1`), **`Id` is the table's TDEF page number**. An object is excluded from the **user-table** list (as Access's own schema view does — it hides system *and* hidden objects) if `Flags & 0x80000002` (system: `0x80000000` + `0x00000002`) **or** `Flags & 0x00000008` (**hidden** — observed on nav-pane tables and on EFCore.Jet's `#Dual` helper) is set, **or** its name begins with `MSys` / `~` / `#`. Bootstrap: - build a TableDef for MSysObjects from page 2 and read its rows like any table. + build a TableDefinition for MSysObjects from the page `0x20` names ([page-00 §2](page-00-database.md)) and read + its rows like any table. > **Why the hidden bit / `#` prefix matter.** Missing them makes a hidden helper such as > EFCore.Jet's `#Dual` (`Flags = 0x08`) count as a *user* table, so a "has any user tables?" check @@ -98,7 +99,9 @@ > follows from it — a file whose `MSysObjects.Owner` is `680E` has mask `6A-0D`, under which `690C` is > admin `03-01`, `680C` Users `02-01` and `6809` Creator `02-04`. An object written with a **different** > file's SIDs carries an owner that names no account in the file it sits in, so a writer adding an object to - > a database it did not create must take the mask from that database. + > a database it did not create must take the mask from that database. Page 0 gives the whole keystream too, + > and is the surer source: after a key change Access can leave `MSysObjects`' own owner un-re-masked + > ([page-00 §2.3](page-00-database.md)). > **Property blob (`LvProp`) format — verified byte-for-byte against ACE.** A 4-byte signature > (`MR2\0` on ACE, `KKD\0` on older MDB) then blocks, each `[int length][short type][body]` with the @@ -157,7 +160,7 @@ > The `MSysDb` object — an `MSysObjects` row of `Type=2` with no table behind it — carries the > database-level properties, among them `AccessVersion`. Access writes those, not the engine: a DAO-created > database has none, at any `dbVersion`, and Access adds them (with `MSysAccessStorage` and the nav-pane - > tables) the first time it opens the file. Which is why `DatabaseCreator` does not write them either. The + > tables) the first time it opens the file. Which is why `JetDatabase` does not write them either. The > one place the value matters is [data-types.md](data-types.md), where it says which files carry the > legacy `MSysAccessObjects` store and its BigBinary (`0x11`) column. > @@ -215,12 +218,12 @@ > would mangle a numeric one). > > LibRed **writes** `DefaultValue`, `Required` and `CheckConstraints` properties (`PropertyBlob.Write`) and - > **reads** them back (`ColumnDef.DefaultValue`, `ColumnDef.IsNullable`, `TableDef.CheckConstraints`), + > **reads** them back (`ColumnDef.DefaultValue`, `ColumnDef.IsNullable`, `TableDefinition.CheckConstraints`), > applying the default when an insert omits the column and **rejecting** an insert that leaves a required > column null ("You must enter a value in the '
.' field.", matching Access). Access > **applies the default**, **enforces Required**, and **enforces the CHECK** on its own inserts — > including on a LibRed-created table (verified: ACE rejects an insert omitting a LibRed `NOT NULL` - > column). `LvProp` is stored on a **single LVAL page** (`LongValueWriter`, descriptor flag `0x40`) — the + > column). `LvProp` is stored on a **single LVAL page** (`LongValueStore`, descriptor flag `0x40`) — the > form Access's property loader requires. **Verified:** Access opens the file and **applies the default** on > its own insert that omits the column. (An *inline* value, flag `0x80`, is valid long-value storage but is > **not** recognised by Access's property loader.) @@ -310,7 +313,7 @@ `[length-and-flags:4][row:1][page:3][4 reserved]`. The first word is little-endian, with a 30-bit byte length and two flag bits: byte `0x03` masked with `0xC0` gives `0x40` = single page (`0x80` = inline, payload follows the descriptor; `0x00` = chained across pages). LibRed writes the - single-page form (`LongValueWriter`) and chained pages for payloads larger than one page. + single-page form (`LongValueStore`) and chained pages for payloads larger than one page. > With those fields set, Access **enumerates** a LibRed-created table (it appears in the > schema/Tables rowset) — verified via OLE DB. Maintaining MSysObjects' indexes (the composite @@ -515,7 +518,7 @@ > whole, and not a fixed system-table bind (the `CREATE VIEW` error names the table outright). It is > **read-only** from ACE's side: `CREATE TABLE` and `CREATE INDEX` leave it at **0 rows**. The dependency is > on this table specifically — without `MSysComplexType_Text` the statements still succeed, and without - > `MSysQueries` they fail with a different error. LibRed creates all ten in `DatabaseCreator.CreateEmpty` for version ≥ `0x02`, which is what lets ACE run DDL in a + > `MSysQueries` they fail with a different error. LibRed creates all ten in `JetDatabase.Create` for version ≥ `0x02`, which is what lets ACE run DDL in a > LibRed-created database. > > **What DDL does to a complex column's three links (verified).** A complex column hangs off its descriptor's @@ -796,6 +799,10 @@ Four layers — the user table, then three kinds of ordinary hidden/system table > value ids are never reissued. A delete path must therefore remove the flat rows of **every** complex column > on the table for that id, and must leave both high-waters alone. +LibRed's `Table.Delete` performs this cleanup before removing the owner's index entries and row. Each +backing row uses the same table deletion path, including its indexes and long-value reclamation. All writes +join the owner's transaction; a standalone deletion starts a transaction covering the entire operation. + > **Both id spaces are sparse high-water counters**, so neither is dense or ordered: record ids run > `1,2,3,4,7,14` over six rows, and `XSDFiles`' value ids reach `116` over 37 values. And an id is allocated > when the **row** is created, with or without values — `complex1.accdb`'s `Table1` has three rows with ids @@ -902,4 +909,4 @@ The stream is standard zlib (RFC 1950, opening `78 5E`), so `ZLibStream` reads i **To materialize**: read the row's `0x12` id → find the column's row in `MSysComplexColumns` (by `ComplexID` from the descriptor's `0x0B`, or by name) → open `FlatTableID`'s `f_` table → select the rows whose -**owner-link** column equals that id. All readable today by hand; LibRed does not yet do it for you. +**owner-link** column equals that id. All readable today by hand; LibRed does not yet do it for you. \ No newline at end of file diff --git a/src/LibRed/docs/functions.md b/src/LibRed/docs/functions.md index 1e09f614..ca3e8c55 100644 --- a/src/LibRed/docs/functions.md +++ b/src/LibRed/docs/functions.md @@ -33,7 +33,7 @@ reference throws "Column not found" — a default is row-blind by design). The e - Values follow VBA sign and rounding conventions (`CInt`/`CLng`/`CByte` use banker's rounding). - **Dates compare by their OLE Automation serial**, not chronologically: below the 1899-12-30 epoch the day count goes negative while the time fraction stays positive, so 1899-12-29 06:00 is -1.25 and 18:00 is -1.75, and ACE - orders the later time first. LibRed matches that, because `IndexKeyEncoder` writes the same serial as the index + orders the later time first. LibRed matches that, because `IndexKeyCodec` writes the same serial as the index key and the two paths must agree (see `AcePreEpochDateProbeTest`). Date *functions* are unaffected — they work in date space. - **Argument counts are validated** against a per-function range, including optional arguments and Jet quirks: diff --git a/test/EFCore.Jet.FunctionalTests/Query/NorthwindJoinQueryJetTest.cs b/test/EFCore.Jet.FunctionalTests/Query/NorthwindJoinQueryJetTest.cs index 0b882342..98ae14cc 100644 --- a/test/EFCore.Jet.FunctionalTests/Query/NorthwindJoinQueryJetTest.cs +++ b/test/EFCore.Jet.FunctionalTests/Query/NorthwindJoinQueryJetTest.cs @@ -1037,7 +1037,7 @@ INNER JOIN (SELECT @p1 AS `Value` FROM (SELECT COUNT(*) FROM `#Dual`) AS `p_0` UNION SELECT @p2 AS `Value` -FROM (SELECT COUNT(*) FROM `#Dual`) AS `p_1`) AS `p` ON `e`.`EmployeeID` = ASCW(`p`.`Value`) +FROM (SELECT COUNT(*) FROM `#Dual`) AS `p_1`) AS `p` ON `e`.`EmployeeID` = (CLNG(ASCW(`p`.`Value`)) BAND 65535) """, // """ @@ -1046,7 +1046,7 @@ SELECT @p2 AS `Value` SELECT `e`.`EmployeeID` FROM `Employees` AS `e` INNER JOIN (SELECT @p1 AS `Value` -FROM (SELECT COUNT(*) FROM `#Dual`) AS `p_0`) AS `p` ON `e`.`EmployeeID` = ASCW(`p`.`Value`) +FROM (SELECT COUNT(*) FROM `#Dual`) AS `p_0`) AS `p` ON `e`.`EmployeeID` = (CLNG(ASCW(`p`.`Value`)) BAND 65535) """); } @@ -1168,4 +1168,4 @@ private void AssertSql(params string[] expected) protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); } -} +} \ No newline at end of file diff --git a/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs b/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs index e98130d4..3ddfa0da 100644 --- a/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs +++ b/test/EFCore.LibRed.Extended.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs @@ -1051,7 +1051,7 @@ public override async Task Join_local_string_closure_is_cached_correctly(bool as SELECT `e`.`EmployeeID` FROM `Employees` AS `e` -INNER JOIN (VALUES (@p1), (@p2)) AS `p`(`Value`) ON `e`.`EmployeeID` = ASCW(`p`.`Value`) +INNER JOIN (VALUES (@p1), (@p2)) AS `p`(`Value`) ON `e`.`EmployeeID` = (CLNG(ASCW(`p`.`Value`)) BAND 65535) """, // """ @@ -1059,7 +1059,7 @@ INNER JOIN (VALUES (@p1), (@p2)) AS `p`(`Value`) ON `e`.`EmployeeID` = ASCW(`p`. SELECT `e`.`EmployeeID` FROM `Employees` AS `e` -INNER JOIN (VALUES (@p1)) AS `p`(`Value`) ON `e`.`EmployeeID` = ASCW(`p`.`Value`) +INNER JOIN (VALUES (@p1)) AS `p`(`Value`) ON `e`.`EmployeeID` = (CLNG(ASCW(`p`.`Value`)) BAND 65535) """); } @@ -1183,4 +1183,4 @@ private void AssertSql(params string[] expected) protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); } -} +} \ No newline at end of file diff --git a/test/EFCore.LibRed.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs b/test/EFCore.LibRed.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs index f54bc6bb..e28fd9ab 100644 --- a/test/EFCore.LibRed.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs +++ b/test/EFCore.LibRed.FunctionalTests/Query/NorthwindJoinQueryLibRedTest.cs @@ -1057,7 +1057,7 @@ INNER JOIN (SELECT @p1 AS `Value` FROM (SELECT COUNT(*) FROM `#Dual`) AS `p_0` UNION SELECT @p2 AS `Value` -FROM (SELECT COUNT(*) FROM `#Dual`) AS `p_1`) AS `p` ON `e`.`EmployeeID` = ASCW(`p`.`Value`) +FROM (SELECT COUNT(*) FROM `#Dual`) AS `p_1`) AS `p` ON `e`.`EmployeeID` = (CLNG(ASCW(`p`.`Value`)) BAND 65535) """, // """ @@ -1066,7 +1066,7 @@ SELECT @p2 AS `Value` SELECT `e`.`EmployeeID` FROM `Employees` AS `e` INNER JOIN (SELECT @p1 AS `Value` -FROM (SELECT COUNT(*) FROM `#Dual`) AS `p_0`) AS `p` ON `e`.`EmployeeID` = ASCW(`p`.`Value`) +FROM (SELECT COUNT(*) FROM `#Dual`) AS `p_0`) AS `p` ON `e`.`EmployeeID` = (CLNG(ASCW(`p`.`Value`)) BAND 65535) """); } @@ -1195,4 +1195,4 @@ private void AssertSql(params string[] expected) protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Ado.Tests/CreateDatabaseDualTableTests.cs b/test/LibRed.Ado.Tests/CreateDatabaseDualTableTests.cs index 0c7362ec..fab96f1b 100644 --- a/test/LibRed.Ado.Tests/CreateDatabaseDualTableTests.cs +++ b/test/LibRed.Ado.Tests/CreateDatabaseDualTableTests.cs @@ -7,7 +7,7 @@ namespace LibRed.Ado.Tests; /// A LibRed-created database must contain the single-row #Dual helper table. EFCore.Jet's query generator /// renders FROM-less scalar queries (All/Any/Count/constant projections) as FROM (SELECT COUNT(*) FROM `#Dual`), /// so without it those queries fail to bind. The DAO/ADOX creation path created it via EnsureDualTable; native -/// creation (DatabaseCreator.CreateEmpty) must do the same — this guards that it does. +/// creation (JetDatabase.Create) must do the same — this guards that it does. /// public class CreateDatabaseDualTableTests { @@ -45,4 +45,4 @@ public void Dual_table_is_hidden_from_user_tables() } finally { if (File.Exists(path)) File.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Benchmarks/Harness/Corpus.cs b/test/LibRed.Benchmarks/Harness/Corpus.cs index 638928af..bd4adefd 100644 --- a/test/LibRed.Benchmarks/Harness/Corpus.cs +++ b/test/LibRed.Benchmarks/Harness/Corpus.cs @@ -21,7 +21,7 @@ namespace LibRed.Benchmarks.Harness; /// state a compacted database is in — a benchmark against an incrementally-grown index would be measuring /// fragmentation history rather than the seek. /// -/// The file is generated from scratch by LibRed itself (), so the +/// The file is generated from scratch by LibRed itself (), so the /// corpus needs no Access engine and the benchmarks build on Linux and macOS exactly as on Windows. /// public static class Corpus @@ -91,7 +91,7 @@ public static string CachePath(int scale) => private static void Build(string path, int scale) { var sw = Stopwatch.StartNew(); - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var db = JetDatabase.Open(path, readOnly: false); var engine = new QueryEngine(db); @@ -213,4 +213,4 @@ private sealed class RowWriter(Table table) public void Insert() => table.Insert(_values); } -} +} \ No newline at end of file diff --git a/test/LibRed.Benchmarks/Suites/CatalogBenchmarks.cs b/test/LibRed.Benchmarks/Suites/CatalogBenchmarks.cs index 59ef1ef5..4e7182fd 100644 --- a/test/LibRed.Benchmarks/Suites/CatalogBenchmarks.cs +++ b/test/LibRed.Benchmarks/Suites/CatalogBenchmarks.cs @@ -53,8 +53,8 @@ public int EnumerateSchema() { using JetDatabase database = JetDatabase.Open(_path); int total = 0; - foreach (TableDef table in database.Catalog.UserTables) + foreach (TableDefinition table in database.Catalog.UserTables) total += table.Columns.Count + table.Indexes.Count; return total; } -} +} \ No newline at end of file diff --git a/test/LibRed.Benchmarks/Suites/DdlBenchmarks.cs b/test/LibRed.Benchmarks/Suites/DdlBenchmarks.cs index de20f9d7..3e8009b9 100644 --- a/test/LibRed.Benchmarks/Suites/DdlBenchmarks.cs +++ b/test/LibRed.Benchmarks/Suites/DdlBenchmarks.cs @@ -70,7 +70,7 @@ public void CreateEmptyDatabase() { string path = Path.Combine(_scratchDirectory, $"new-{_sequence++}.accdb"); _created.Add(path); - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); } } @@ -103,4 +103,4 @@ public void AddColumn() for (int i = 0; i < ColumnBatch; i++) _scratch.Engine.ExecuteNonQuery($"ALTER TABLE Bench ADD COLUMN Extra{_sequence++} LONG"); } -} +} \ No newline at end of file diff --git a/test/LibRed.Benchmarks/Suites/StorageBenchmarks.cs b/test/LibRed.Benchmarks/Suites/StorageBenchmarks.cs index 4c002d87..e1f5cfbc 100644 --- a/test/LibRed.Benchmarks/Suites/StorageBenchmarks.cs +++ b/test/LibRed.Benchmarks/Suites/StorageBenchmarks.cs @@ -21,7 +21,7 @@ public class StorageBenchmarks private IndexDef _primaryKey = null!; private IndexDef _onK = null!; - // Seek values are addressed by the table's column ordinal, not by key position (IndexKeyEncoder reads + // Seek values are addressed by the table's column ordinal, not by key position (IndexKeyCodec reads // values[column.Index]), so a seek key is a full-width row with only the key columns filled in. private object?[] _idKey = null!; private object?[] _kKey = null!; @@ -59,7 +59,7 @@ public void Setup() [GlobalCleanup] public void Cleanup() => _database.Dispose(); - /// Every row, decoded in full: the cost of the page walk plus RowDecoder for the whole + /// Every row, decoded in full: the cost of the page walk plus RowCodec for the whole /// table. The upper bound on how fast any unfiltered query over this table can be. [Benchmark] public long FullScan() => Consume.Drain(_bench.Rows()); @@ -78,4 +78,4 @@ public void Setup() /// rows of , so the two together separate descent from traversal. [Benchmark] public long RangeSeek() => Consume.Drain(_bench.SeekRangeRows(_onK, _kLow, _kHigh)); -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/AceCreatedDatabaseTests.cs b/test/LibRed.Core.AccessTests/AceCreatedDatabaseTests.cs index b2543baf..e68ef4d0 100644 --- a/test/LibRed.Core.AccessTests/AceCreatedDatabaseTests.cs +++ b/test/LibRed.Core.AccessTests/AceCreatedDatabaseTests.cs @@ -19,7 +19,7 @@ public void Real_Access_opens_and_reads_a_libred_created_database() try { // Create the database, a user table, and a row entirely through LibRed — no Access, no DAO/ADOX. - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using (var db = JetDatabase.Open(path, readOnly: false)) { db.CreateTable("People", new[] @@ -45,4 +45,4 @@ public void Real_Access_opens_and_reads_a_libred_created_database() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/AceDateTime2UpgradeTests.cs b/test/LibRed.Core.AccessTests/AceDateTime2UpgradeTests.cs index b36eeb1d..26ab76dc 100644 --- a/test/LibRed.Core.AccessTests/AceDateTime2UpgradeTests.cs +++ b/test/LibRed.Core.AccessTests/AceDateTime2UpgradeTests.cs @@ -27,7 +27,7 @@ public void Writing_the_version_byte_is_a_complete_upgrade_to_datetime2() string path = CopyAce12Database("dt2-upgrade-"); try { - Assert.Equal(0x02, VersionByte(path)); + Assert.Equal(Formats.JetVersion.Version12_2007, TestDatabases.FormatOf(path).Version); // Start from a file that already holds data, which is what a real upgrade has to preserve. using (DbConnection connection = AceTestDatabase.Open(path)) @@ -35,10 +35,12 @@ public void Writing_the_version_byte_is_a_complete_upgrade_to_datetime2() Execute(connection, "CREATE TABLE T (Id LONG, D DATETIME)"); Execute(connection, "INSERT INTO T (Id, D) VALUES (1, #2020-01-02 03:04:05#)"); } - Assert.Equal(0x02, VersionByte(path)); // an ordinary DATETIME does not move it + // An ordinary DATETIME does not move it. + Assert.Equal(Formats.JetVersion.Version12_2007, TestDatabases.FormatOf(path).Version); - SetVersionByte(path, 0x06); - Assert.Equal(0x06, VersionByte(path)); + using (var raised = JetDatabase.Open(path, readOnly: false)) + raised.EnsureFormatAtLeast(Formats.JetVersion.Version17_2019); + Assert.Equal(Formats.JetVersion.Version17_2019, TestDatabases.FormatOf(path).Version); using (DbConnection connection = AceTestDatabase.Open(path)) { @@ -56,7 +58,7 @@ public void Writing_the_version_byte_is_a_complete_upgrade_to_datetime2() } // ACE left the version byte where we put it - it had no upgrade of its own left to do. - Assert.Equal(0x06, VersionByte(path)); + Assert.Equal(Formats.JetVersion.Version17_2019, TestDatabases.FormatOf(path).Version); using var db = JetDatabase.Open(path); ColumnDef extended = db.Catalog.Tables.Single(t => t.Name == "T").Columns.Single(c => c.Name == "E"); @@ -98,7 +100,8 @@ public void LibRed_decodes_datetime2_values_that_ace_reads_back_wrongly() string path = CopyAce12Database("dt2-decode-"); try { - SetVersionByte(path, 0x06); + using (var raised = JetDatabase.Open(path, readOnly: false)) + raised.EnsureFormatAtLeast(Formats.JetVersion.Version17_2019); using (DbConnection connection = AceTestDatabase.Open(path)) { @@ -147,19 +150,4 @@ private static void Execute(DbConnection connection, string sql) command.CommandText = sql; return command.ExecuteScalar(); } - - private static byte VersionByte(string path) - { - using var stream = File.OpenRead(path); - stream.Seek(0x14, SeekOrigin.Begin); - int b = stream.ReadByte(); - return b < 0 ? throw new EndOfStreamException() : (byte)b; - } - - private static void SetVersionByte(string path, byte version) - { - using var stream = new FileStream(path, FileMode.Open, FileAccess.Write); - stream.Seek(0x14, SeekOrigin.Begin); - stream.WriteByte(version); - } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/AceDdlOnLibRedDatabaseProbeTest.cs b/test/LibRed.Core.AccessTests/AceDdlOnLibRedDatabaseProbeTest.cs index aef956e6..aaa26941 100644 --- a/test/LibRed.Core.AccessTests/AceDdlOnLibRedDatabaseProbeTest.cs +++ b/test/LibRed.Core.AccessTests/AceDdlOnLibRedDatabaseProbeTest.cs @@ -11,7 +11,7 @@ namespace LibRed.Core.Tests; // // ANSWERED: the file was missing MSysComplexColumns. ACE consults it whenever it creates a catalog object — // CREATE TABLE and CREATE VIEW, and only those; DML, CREATE INDEX, ALTER ADD COLUMN and DROP TABLE all work -// without it. DatabaseCreator.CreateEmpty now writes it (and the nine MSysComplexType_* tables) for version +// without it. JetDatabase.Create now writes it (and the nine MSysComplexType_* tables) for version // >= 0x02. See docs/format/system-catalog.md. The DAO probes that isolated it — dropping one system table at a // time from a DAO-created database — are in git history. // @@ -28,7 +28,7 @@ public void Ace_runs_ddl_against_a_libred_created_database(string label, byte ve string path = TemporaryDatabase.CreatePath($"ace-ddl-{label}-"); try { - DatabaseCreator.CreateEmpty(path, collation: new Collation(CollatingOrder.General, version)); + JetDatabase.Create(path, collation: new Collation(CollatingOrder.General, version)); using var connection = AceTestDatabase.Open(path); output.WriteLine($"{label}: ACE opened the database"); @@ -58,7 +58,7 @@ public void Probe_system_tables_libred_creates_versus_access() string libred = TemporaryDatabase.CreatePath("systables-libred-"); try { - DatabaseCreator.CreateEmpty(libred); + JetDatabase.Create(libred); string[] mine = SystemTables(libred); output.WriteLine($"LibRed-created ({mine.Length}): {string.Join(", ", mine)}"); @@ -82,4 +82,4 @@ private static string[] SystemTables(string path) using var db = JetDatabase.Open(path); return [.. db.Catalog.Tables.Select(t => t.Name).OrderBy(n => n, StringComparer.OrdinalIgnoreCase)]; } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/AceDecimalDeclarationProbeTest.cs b/test/LibRed.Core.AccessTests/AceDecimalDeclarationProbeTest.cs index a81fa0a3..ac9bbbc5 100644 --- a/test/LibRed.Core.AccessTests/AceDecimalDeclarationProbeTest.cs +++ b/test/LibRed.Core.AccessTests/AceDecimalDeclarationProbeTest.cs @@ -7,7 +7,7 @@ namespace LibRed.Core.Tests; // What precision and scale does ACE actually stamp on a NUMERIC/DECIMAL column's descriptor? // -// TdefBuilder.EffectivePrecision resolves a ColumnSpec's default Precision of 0 to 18 before writing, on the +// TableDefinition.EffectivePrecision resolves a ColumnSpec's default Precision of 0 to 18 before writing, on the // grounds that 18 is ACE's own default for a bare DECIMAL. That figure was inherited from AccessTypeMapper // (`column.Size ?? 18`) rather than measured, so this measures it: ACE creates the columns, LibRed reads the // descriptor bytes back. @@ -39,7 +39,7 @@ public void What_ace_stamps_for_each_decimal_declaration() } using var db = JetDatabase.Open(path, readOnly: true); - TableDef table = db.Catalog.Tables.Single(t => t.Name == "DecDecl"); + TableDefinition table = db.Catalog.Tables.Single(t => t.Name == "DecDecl"); foreach ((string column, string declared) in cases) { ColumnDef def = table.Columns.Single(c => c.Name == column); @@ -134,7 +134,7 @@ public void What_a_decimal_28_28_can_hold(string literal) File.Delete(path); try { - Storage.DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var db = JetDatabase.Open(path, readOnly: false); db.CreateTable("D", [ @@ -154,4 +154,4 @@ public void What_a_decimal_28_28_can_hold(string literal) finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/AcePreEpochDateProbeTest.cs b/test/LibRed.Core.AccessTests/AcePreEpochDateProbeTest.cs index f5a33e5c..b25a2108 100644 --- a/test/LibRed.Core.AccessTests/AcePreEpochDateProbeTest.cs +++ b/test/LibRed.Core.AccessTests/AcePreEpochDateProbeTest.cs @@ -13,7 +13,7 @@ namespace LibRed.Core.Tests; // consequences for LibRed, which manipulates these serials directly: // * ExpressionEvaluator.Arithmetic adds/subtracts raw OA doubles before calling FromOADate, which is not the // same as adding days to a date once the value is negative. -// * IndexKeyEncoder encodes the OA double as the sort key, and raw doubles do not order correctly within a +// * IndexKeyCodec encodes the OA double as the sort key, and raw doubles do not order correctly within a // pre-epoch day (see above). // ACE may well have inherited the same weirdness, in which case LibRed is already bug-compatible and should stay // that way. This probe establishes which it is. The existing DateAdd/DateDiff functional tests do not cover it: @@ -119,7 +119,7 @@ public void Ace_uses_oa_serial_comparison_and_date_space_functions_before_the_ep // COMPARISON AND ORDERING are not: they use the raw double, so within a pre-epoch day ACE puts // later times FIRST. 06:00 < 18:00 evaluates to False (0), and ORDER BY returns 1,3,2,4,5,6 — // row 3 (18:00) ahead of row 2 (06:00). This is a genuine ACE defect, faithfully inherited from - // the OA DATE representation. LibRed's IndexKeyEncoder encodes the same raw serial, so it is + // the OA DATE representation. LibRed's IndexKeyCodec encodes the same raw serial, so it is // expected to be bug-compatible here; the evaluator comparing CLR DateTime values is NOT, and // would order these correctly — a parity gap, in the direction of being right. Assert.Equal((short)0, Scalar(conn, "SELECT (#12/29/1899 06:00:00# < #12/29/1899 18:00:00#) FROM `P`")); @@ -203,4 +203,4 @@ [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/AceVbaConversionProbeTest.cs b/test/LibRed.Core.AccessTests/AceVbaConversionProbeTest.cs index d2b6d4f6..3ad9b47f 100644 --- a/test/LibRed.Core.AccessTests/AceVbaConversionProbeTest.cs +++ b/test/LibRed.Core.AccessTests/AceVbaConversionProbeTest.cs @@ -163,7 +163,7 @@ public void Ace_vba_conversion_values_and_provider_types_are_pinned() } // A Currency is an int64 scaled by 10,000, so every conversion into one has to resolve a 5th decimal place. - // LibRed resolves it half-to-even (JetTypeCodec and IndexKeyEncoder both call decimal.Round without a mode, + // LibRed resolves it half-to-even (JetTypeCodec and IndexKeyCodec both call decimal.Round without a mode, // which is ToEven). VB's own CCur is documented as banker's rounding, but the Jet Expression Service is not // the VBA runtime - CStr(True) already differs - so the rule is measured rather than assumed, on both routes // into a Currency: the CCur function, and storing into a CURRENCY column. @@ -212,7 +212,7 @@ public void Ace_currency_midpoint_rounding_is_pinned() // A midpoint over an even 4th digit stays put and one over an odd digit lifts, which rules out // AwayFromZero; the negatives move the same distance as their positives, which rules out // ToPositiveInfinity. This is what LibRed already does - decimal.Round without a mode is ToEven - // in JetTypeCodec.Encode and IndexKeyEncoder.EncodeFixed - so the two agree. + // in JetTypeCodec.Encode and IndexKeyCodec.EncodeFixed - so the two agree. // --------------------------------------------------------------------------------------------- Assert.Equal(0.0312m, Scalar(conn, "SELECT CCur(CDbl(0.03125)) FROM `P`")); Assert.Equal(0.1562m, Scalar(conn, "SELECT CCur(CDbl(0.15625)) FROM `P`")); @@ -232,4 +232,4 @@ public void Ace_currency_midpoint_rounding_is_pinned() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/ActionQueryProcedureAccessTests.cs b/test/LibRed.Core.AccessTests/ActionQueryProcedureAccessTests.cs index be3a1f3b..59d5ccd7 100644 --- a/test/LibRed.Core.AccessTests/ActionQueryProcedureAccessTests.cs +++ b/test/LibRed.Core.AccessTests/ActionQueryProcedureAccessTests.cs @@ -114,7 +114,7 @@ public void Ace_written_action_query_is_read_back_as_runnable_sql(short kind, st using (var conn = OpenOleDb(path)) CreateProcedure(conn, "P", body); using var db = JetDatabase.Open(path); - StoredActionQuery query = db.Catalog.ActionQueries["P"]; + StoredQuery query = db.Catalog.FindQuery("P")!; Assert.Equal(kind, ActionFlag(db, "P")); Assert.Null(query.UnsupportedReason); Assert.Equal(expected, query.Sql); @@ -138,9 +138,9 @@ public void Ace_parameterized_update_is_read_back_with_its_parameters_clause() } using var db = JetDatabase.Open(path); - Assert.Equal(["pTitle", "pCountry"], db.Catalog.QueryParameters["UpdateByCountry"].Select(p => p.Name)); + Assert.Equal(["pTitle", "pCountry"], db.Catalog.FindQuery("UpdateByCountry")!.Parameters.Select(p => p.Name)); - StoredActionQuery query = db.Catalog.ActionQueries["UpdateByCountry"]; + StoredQuery query = db.Catalog.FindQuery("UpdateByCountry")!; Assert.Equal((short)4, ActionFlag(db, "UpdateByCountry")); // An action query declares its parameters exactly as a SELECT does, and is rebuilt with the same // leading clause — which is what makes the body's references to them parameters and not columns. @@ -149,7 +149,7 @@ public void Ace_parameterized_update_is_read_back_with_its_parameters_clause() "PARAMETERS [pTitle] TEXT(50), [pCountry] TEXT(20); " + "UPDATE [Customers] SET [ContactTitle] = pTitle WHERE Country = pCountry", query.Sql); - Assert.Equal([50, 20], db.Catalog.QueryParameters["UpdateByCountry"].Select(p => p.Size)); + Assert.Equal([50, 20], db.Catalog.FindQuery("UpdateByCountry")!.Parameters.Select(p => p.Size)); } finally { TemporaryDatabase.Delete(path); } } @@ -165,7 +165,7 @@ public void A_kind_libred_cannot_run_still_reports_which_kind_it_is() "TRANSFORM Count(*) SELECT Country FROM Customers GROUP BY Country PIVOT City"); using var db = JetDatabase.Open(path); - StoredActionQuery query = db.Catalog.ActionQueries["ByCountry"]; + StoredQuery query = db.Catalog.FindQuery("ByCountry")!; Assert.Null(query.Sql); // "Not supported" that doesn't say what it is leaves a caller no way to tell an unimplemented // feature from an unreadable file. @@ -191,13 +191,15 @@ public void Ace_having_view_is_not_misclassified_as_an_action_query() } using var db = JetDatabase.Open(path); - Assert.False(db.Catalog.ActionQueries.ContainsKey("CountriesWithManyCustomers")); + StoredQuery view = db.Catalog.FindQuery("CountriesWithManyCustomers")!; + Assert.False(view.IsAction); // A grouped SELECT is a view, and its HAVING rides along: the Attribute=0x0A row beside the // Attribute=9 GROUP BY ones. Reading the GROUP BY while silently dropping the HAVING would // reconstruct a query that returns every country — a wrong answer, not a missing feature — so the // clause has to survive into the rebuilt SQL. - Assert.True(db.Catalog.Views.TryGetValue("CountriesWithManyCustomers", out string? sql)); + string? sql = view.Sql; + Assert.NotNull(sql); Assert.Contains("GROUP BY", sql, StringComparison.OrdinalIgnoreCase); Assert.Contains("HAVING", sql, StringComparison.OrdinalIgnoreCase); } @@ -213,13 +215,13 @@ private static void CreateProcedure(OleDbConnection connection, string name, str private static short ActionFlag(JetDatabase database, string queryName) { - TableDef objectsDef = database.Catalog.FindTable("MSysObjects")!; + TableDefinition objectsDef = database.Catalog.FindTable("MSysObjects")!; int objectIdIndex = ColumnIndex(objectsDef, "Id"); int objectNameIndex = ColumnIndex(objectsDef, "Name"); int objectId = (int)database.OpenTable("MSysObjects") .Rows().Single(row => Equals(row[objectNameIndex], queryName))[objectIdIndex]!; - TableDef queriesDef = database.Catalog.FindTable("MSysQueries")!; + TableDefinition queriesDef = database.Catalog.FindTable("MSysQueries")!; int queryObjectIdIndex = ColumnIndex(queriesDef, "ObjectId"); int attributeIndex = ColumnIndex(queriesDef, "Attribute"); int flagIndex = ColumnIndex(queriesDef, "Flag"); @@ -228,6 +230,6 @@ private static short ActionFlag(JetDatabase database, string queryName) return (short)action[flagIndex]!; } - private static int ColumnIndex(TableDef definition, string name) => + private static int ColumnIndex(TableDefinition definition, string name) => definition.Columns.ToList().FindIndex(column => column.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/AppendOnlyDropAccessTests.cs b/test/LibRed.Core.AccessTests/AppendOnlyDropAccessTests.cs index aeafbdcd..9d93862f 100644 --- a/test/LibRed.Core.AccessTests/AppendOnlyDropAccessTests.cs +++ b/test/LibRed.Core.AccessTests/AppendOnlyDropAccessTests.cs @@ -44,12 +44,12 @@ public void Dropping_append_only_memos_leaves_the_catalog_ACE_leaves() } using var result = JetDatabase.Open(libredPath, readOnly: true); - TableDef t = result.Catalog.FindTable("T")!; - TableDef a = result.Catalog.FindTable("A")!; + TableDefinition t = result.Catalog.FindTable("T")!; + TableDefinition a = result.Catalog.FindTable("A")!; Assert.Equal(["ID"], t.Columns.Select(c => c.Name)); - Assert.Equal(0u, t.ObjectFlags & (uint)CatalogFormat.ObjectFlagOwnsComplexColumns); + Assert.False(t.ObjectFlags.HasFlag(ObjectAttributes.OwnsComplexColumns)); Assert.Equal(["ID", "Files"], a.Columns.Select(c => c.Name)); - Assert.NotEqual(0u, a.ObjectFlags & (uint)CatalogFormat.ObjectFlagOwnsComplexColumns); // the attachment remains + Assert.True(a.ObjectFlags.HasFlag(ObjectAttributes.OwnsComplexColumns)); // the attachment remains Assert.DoesNotContain(result.Catalog.Tables, x => x.Name.StartsWith("MSysComplexTypeVH_", StringComparison.Ordinal)); } finally @@ -98,11 +98,11 @@ private static string Catalog(string path) var rows = objects.Rows().ToList(); foreach (string table in (string[])["T", "A"]) { - TableDef t = db.Catalog.FindTable(table)!; + TableDefinition t = db.Catalog.FindTable(table)!; object?[] row = rows.Single(r => (string)r[od.FindColumn("Name")!.Index]! == table); var props = row[od.FindColumn("LvProp")!.Index] is byte[] { Length: > 0 } lv ? PropertyBlob.Read(lv) : []; lines.Add($"{table}: [{string.Join(",", t.Columns.Select(c => c.Name))}] indexes [{string.Join(",", t.Indexes.Select(i => i.Name))}] " + - $"flags 0x{t.ObjectFlags:X8} AppendOnly [{string.Join(",", props.Where(p => p.Name == "AppendOnly").Select(p => p.Owner))}]"); + $"flags 0x{(uint)t.ObjectFlags:X8}AppendOnly [{string.Join(",", props.Where(p => p.Name == "AppendOnly").Select(p => p.Owner))}]"); } foreach (ComplexColumn c in db.Catalog.ComplexColumns) lines.Add($"complex {c.OwnerTable.Name}.{c.ColumnName} [{string.Join(",", db.Catalog.FindTable(c.ElementTypeName!)!.Columns.Select(x => x.Name))}] " + @@ -123,4 +123,4 @@ private static object Item(object collection, string name) => private static object? Invoke(object target, string member, params object?[] args) => target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/ArabicShaddaCollationAccessTests.cs b/test/LibRed.Core.AccessTests/ArabicShaddaCollationAccessTests.cs index 04a20b84..b0b401ee 100644 --- a/test/LibRed.Core.AccessTests/ArabicShaddaCollationAccessTests.cs +++ b/test/LibRed.Core.AccessTests/ArabicShaddaCollationAccessTests.cs @@ -74,7 +74,7 @@ public void Libred_encodes_a_shadda_as_ace_does(string description, string value Assert.Equal( Convert.ToHexString(stored), - Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [value]))); + Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [value]))); } finally { TemporaryDatabase.Delete(path); } } @@ -85,4 +85,4 @@ private static void Exec(System.Data.OleDb.OleDbConnection connection, string sq command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/AstralCollationProbeTest.cs b/test/LibRed.Core.AccessTests/AstralCollationProbeTest.cs index 32b791a0..4431c7e7 100644 --- a/test/LibRed.Core.AccessTests/AstralCollationProbeTest.cs +++ b/test/LibRed.Core.AccessTests/AstralCollationProbeTest.cs @@ -53,7 +53,7 @@ public void Probe_astral_reconnaissance(byte version) stored++; string? ours = null; - try { ours = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [text])); } + try { ours = Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [text])); } catch (NotSupportedException) { } if (ours == key) { matched++; continue; } @@ -107,7 +107,7 @@ public void Probe_astral_plane(byte version, int plane) stored++; string? ours = null; - try { ours = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [text])); } + try { ours = Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [text])); } catch (NotSupportedException) { } if (ours == key) { matched++; continue; } @@ -169,7 +169,7 @@ public void Probe_maximum_indexable_length(byte version) previousKeyBytes = keyBytes; string? ours = null; - try { ours = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [text])); } + try { ours = Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [text])); } catch (NotSupportedException) { } if (ours == key) longestMatching = text.Length; } @@ -224,7 +224,7 @@ public void Probe_key_truncation_boundary(byte version) previous = key; string? ours = null; - try { ours = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [text])); } + try { ours = Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [text])); } catch (NotSupportedException) { } if (ours == key) libredMatches = text.Length; } @@ -263,7 +263,7 @@ public void Probe_key_at_the_boundary(byte version) { if (!ace.TryGetValue(text, out string? key)) { output.WriteLine($" {text.Length,4} (not stored)"); continue; } string? ours = null; - try { ours = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [text])); } + try { ours = Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [text])); } catch (NotSupportedException) { } // Only the tails differ, so show those rather than 500 identical bytes. @@ -304,7 +304,7 @@ public void Probe_exact_key_length_boundary(byte version) foreach (string text in samples) { string? ours = null; - try { ours = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [text])); } + try { ours = Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [text])); } catch (NotSupportedException) { } string key = ace.GetValueOrDefault(text, ""); @@ -333,7 +333,7 @@ public void Probe_multi_column_key_limit() "set LIBRED_ASTRAL=1 — this probe needs ACE"); string path = TemporaryDatabase.CreatePath("general-v1-multi-"); - DatabaseCreator.CreateEmpty(path, collation: Collation.General); + JetDatabase.Create(path, collation: Collation.General); try { using (var connection = AceTestDatabase.Open(path)) @@ -373,7 +373,7 @@ private static (string Source, string? Created, ColumnDef Column) Fixture(byte v if (v1) { created = TemporaryDatabase.CreatePath("general-v1-astral-"); - DatabaseCreator.CreateEmpty(created, collation: Collation.General); + JetDatabase.Create(created, collation: Collation.General); } return (created ?? TestDatabases.NorthwindAccdb, created, new ColumnDef @@ -423,4 +423,4 @@ private static void Exec(System.Data.OleDb.OleDbConnection connection, string sq command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/BigIntKeyEncodingTests.cs b/test/LibRed.Core.AccessTests/BigIntKeyEncodingTests.cs index 955e9dad..1da6d828 100644 --- a/test/LibRed.Core.AccessTests/BigIntKeyEncodingTests.cs +++ b/test/LibRed.Core.AccessTests/BigIntKeyEncodingTests.cs @@ -47,13 +47,13 @@ public void Adding_a_bigint_column_makes_ace_raise_the_file_to_ace16() string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "libred-bigint-ver-"); try { - Assert.Equal(0x02, VersionByte(path)); + Assert.Equal(JetVersion.Version12_2007, TestDatabases.FormatOf(path).Version); using (OleDbConnection connection = AceTestDatabase.Open(path)) Exec(connection, "CREATE TABLE BVer (K BIGINT, V LONG)"); // 0x05, not the 0x06 that DATETIME2 forces: the two types arrived in different formats. - Assert.Equal(0x05, VersionByte(path)); + Assert.Equal(JetVersion.Version16_2016, TestDatabases.FormatOf(path).Version); using var db = JetDatabase.Open(path); Assert.Equal(JetVersion.Version16_2016, db.Format.Version); } @@ -116,7 +116,7 @@ private static void AssertKeysMatchAccess(string indexDdl) var def = table.Definition; IndexDef index = def.Indexes.Single(i => i.Columns.Any(c => c.Column.Name == "K")); int kIdx = def.FindColumn("K")!.Index; - var decoder = new RowDecoder(def.Columns, db.Format); + var decoder = new RowCodec(def.Columns, db.Format); int checkedKeys = 0; foreach ((byte[] accessKey, RowId rowId) in new IndexCursor(table.Channel, index.RootPage).RawEntries()) @@ -125,7 +125,7 @@ private static void AssertKeysMatchAccess(string indexDdl) var values = new object?[def.Columns.Count]; values[kIdx] = value; - byte[] ours = IndexKeyEncoder.Encode(index.Columns, values); + byte[] ours = IndexKeyCodec.Encode(index.Columns, values); Assert.True(accessKey.AsSpan().SequenceEqual(ours), $"{value}: access={Convert.ToHexString(accessKey)} ours={Convert.ToHexString(ours)}"); @@ -143,11 +143,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } - - private static byte VersionByte(string path) - { - using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - stream.Seek(0x14, SeekOrigin.Begin); - return (byte)stream.ReadByte(); - } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/CalculatedColumnAccessTests.cs b/test/LibRed.Core.AccessTests/CalculatedColumnAccessTests.cs index ce69c225..aecd13f5 100644 --- a/test/LibRed.Core.AccessTests/CalculatedColumnAccessTests.cs +++ b/test/LibRed.Core.AccessTests/CalculatedColumnAccessTests.cs @@ -14,7 +14,7 @@ namespace LibRed.Core.Tests; // // LibRed cannot create a calculated column (Access SQL has no syntax for one), so DAO's object model is // the author here — the same path Access's UI uses. Each column gets its own table because ACE validates -// the expression when the TableDef is appended, and one rejected expression would take the rest with it. +// the expression when the TableDefinition is appended, and one rejected expression would take the rest with it. [Collection(AceCollection.Name)] public class CalculatedColumnAccessTests(ITestOutputHelper output) { @@ -249,11 +249,13 @@ public void Spills_an_oversized_calculated_memo_to_a_long_value_page() // A bare 12-byte descriptor with a non-inline flag is the proof it spilled; an inline slot would // carry its payload with it and the test would be passing on a value that never left the row. - Assert.Equal(LibRed.Formats.LongValueFormat.DescriptorSize, fromAce.Length); - Assert.Equal(LibRed.Formats.LongValueFormat.DescriptorSize, fromLibRed.Length); + LibRed.Formats.JetFormatBase format; + using (var db = JetDatabase.Open(path, readOnly: true)) format = db.Format; + Assert.Equal(format.LongValueDescriptorSize, fromAce.Length); + Assert.Equal(format.LongValueDescriptorSize, fromLibRed.Length); Assert.NotEqual( - LibRed.Formats.LongValueFormat.FlagInline, - (byte)(fromLibRed[3] & LibRed.Formats.LongValueFormat.FlagMask)); + LibRed.Storage.LongValueStore.StorageKind.Inline, + LibRed.Storage.LongValueStore.Read(fromLibRed, format).Storage); using var check = AceTestDatabase.Open(path); using var select = check.CreateCommand(); @@ -463,7 +465,7 @@ public void Creates_a_calculated_column_ace_accepts( string path = TemporaryDatabase.CreatePath("calc-create-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using (var db = JetDatabase.Open(path, readOnly: false)) { db.CreateTable("T", [ @@ -534,7 +536,7 @@ public void A_calculated_column_raises_page_zero_as_ACE_does() string libPath = TemporaryDatabase.CreatePath("calc-raise-lib-"); try { - DatabaseCreator.CreateEmpty(basePath); + JetDatabase.Create(basePath); File.Copy(basePath, acePath, overwrite: true); File.Copy(basePath, libPath, overwrite: true); @@ -551,9 +553,11 @@ public void A_calculated_column_raises_page_zero_as_ACE_does() ]); byte[] ace = PageZero(acePath), lib = PageZero(libPath); - output.WriteLine($"ACE: version 0x{ace[0x14]:X2} minor 0x{ace[0x15]:X2}"); - output.WriteLine($"lib: version 0x{lib[0x14]:X2} minor 0x{lib[0x15]:X2}"); - var differences = Enumerable.Range(0, 0xE00) + Formats.JetFormatBase format = TestDatabases.FormatOf(acePath); + int version = Formats.JetFormatBase.VersionOffset, minor = format.MinorVersionOffset; + output.WriteLine($"ACE: version 0x{ace[version]:X2} minor 0x{ace[minor]:X2}"); + output.WriteLine($"lib: version 0x{lib[version]:X2} minor 0x{lib[minor]:X2}"); + var differences = Enumerable.Range(0, format.CommitByteTableOffset) .Where(i => ace[i] != lib[i]) .Select(i => $"0x{i:X3} ace={ace[i]:X2} lib={lib[i]:X2}") .ToList(); @@ -586,7 +590,7 @@ public void Refuses_to_author_an_expression_ace_would_reject(string expression, string path = TemporaryDatabase.CreatePath("calc-invalid-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var db = JetDatabase.Open(path, readOnly: false); var ex = Assert.Throws( () => db.CreateTable("T", [ @@ -711,8 +715,8 @@ private static (byte[] Ace, byte[] LibRed) StoredEnvelopes(string path, string t { if (page.Rows[row].IsDeleted) continue; byte[] raw = page.GetRow(row).ToArray(); - byId[BitConverter.ToInt32(raw, 2 + id.FixedOffset)] = - RowDecoder.CalculatedSlots(t.Definition.Columns, t.Channel.Format, raw)[index]; + byId[BitConverter.ToInt32(raw, t.Channel.Format.RowColumnCountSize + id.FixedOffset)] = + RowCodec.CalculatedSlots(t.Definition.Columns, t.Channel.Format, raw)[index]; } } return (byId[1], byId[2]); @@ -1388,7 +1392,7 @@ public void Records_whether_the_expression_service_knows_backtick_quoting() string path = TemporaryDatabase.CreatePath("calc-tick-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using (var connection = AceTestDatabase.Open(path)) { Execute(connection, "CREATE TABLE T (Id LONG, Qty LONG)"); @@ -1443,7 +1447,7 @@ public void Records_what_ace_does_when_a_calculated_column_is_altered() object workspace = Invoke(engine!, "CreateWorkspace", "", "admin", "", UseJet)!; - // Control first: DAO refuses to mutate a field of a SAVED TableDef whether or not anything is + // Control first: DAO refuses to mutate a field of a SAVED TableDefinition whether or not anything is // calculated, so a refusal there says nothing about calculated columns. output.WriteLine($" [control] DAO rename an ordinary column: {DaoField(workspace, path, "T_CLong", "A", f => SetProperty(f, "Name", "Renamed"))}"); @@ -1601,7 +1605,7 @@ public void Access_reads_a_calculated_column_after_an_unrelated_full_column_rewr } /// Opens the database with DAO, applies to one field of a saved - /// TableDef, and reports what happened. + /// TableDefinition, and reports what happened. private static string DaoField(object workspace, string path, string table, string column, Action mutate) { object? database = null; @@ -1665,4 +1669,4 @@ private static string LibRedExpression(string path, string table, string column) private static void SetProperty(object target, string member, object? value) => target.GetType().InvokeMember(member, BindingFlags.SetProperty, null, target, [value]); -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/ChainedLongValueAccessTests.cs b/test/LibRed.Core.AccessTests/ChainedLongValueAccessTests.cs index 847771eb..7764e588 100644 --- a/test/LibRed.Core.AccessTests/ChainedLongValueAccessTests.cs +++ b/test/LibRed.Core.AccessTests/ChainedLongValueAccessTests.cs @@ -1,6 +1,8 @@ +using System.Buffers.Binary; using System.Data.OleDb; using LibRed; using LibRed.Catalog; +using LibRed.Formats; using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -75,26 +77,28 @@ [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), } using var channel = LibRed.IO.PageChannel.Open(path); - TableDef definition = new JetCatalog(channel).FindTable("Big")!; + TableDefinition definition = new JetCatalog(channel).FindTable("Big")!; int columnId = definition.Columns.First(c => c.Name == "M").ColumnId; byte[] descriptor = new UsageMap(channel, definition).DataPages() .Select(p => { var page = new DataPage(); page.Read(channel.ReadPage(p), channel.Format); return page; }) .SelectMany(page => Enumerable.Range(0, page.RowCount) .Where(row => !page.Rows[row].IsDeleted) - .SelectMany(row => RowDecoder.LongValueDescriptors( + .SelectMany(row => RowCodec.LongValueDescriptors( definition.Columns, channel.Format, page.GetRow(row)))) - .Single(d => d.Key == columnId).Value[..12]; + .Single(d => d.Key == columnId).Value[..channel.Format.LongValueDescriptorSize]; - Assert.Equal(0x00, descriptor[3] & 0xC0); // chained, or the stamp would not apply - int firstChainPage = descriptor[5] | (descriptor[6] << 8) | (descriptor[7] << 16); + var parsedDescriptor = LongValueStore.Read(descriptor, channel.Format); + Assert.Equal(LongValueStore.StorageKind.Chained, parsedDescriptor.Storage); // chained, or the stamp would not apply + int firstChainPage = parsedDescriptor.Page; var header = new byte[channel.PageSize]; channel.ReadPage(firstChainPage, header); - Assert.Equal(Convert.ToHexString(descriptor[8..12]), Convert.ToHexString(header[8..12])); + Assert.Equal(parsedDescriptor.Stamp, + BinaryPrimitives.ReadUInt32LittleEndian(header.AsSpan(channel.Format.DataChainStampOffset, sizeof(uint)))); // And it is a real tag, not two zeroes agreeing by accident — zero would satisfy the check while // proving nothing, which is what LibRed used to write. - Assert.NotEqual("00000000", Convert.ToHexString(descriptor[8..12])); + Assert.NotEqual(0u, parsedDescriptor.Stamp); } finally { TemporaryDatabase.Delete(path); } } @@ -120,15 +124,15 @@ [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), int firstChainPage; using (var channel = LibRed.IO.PageChannel.Open(path, readOnly: false)) { - TableDef definition = new JetCatalog(channel).FindTable("Big")!; + TableDefinition definition = new JetCatalog(channel).FindTable("Big")!; int columnId = definition.Columns.First(c => c.Name == "M").ColumnId; int dataPage = new UsageMap(channel, definition).DataPages().First(); var parsed = new DataPage(); parsed.Read(channel.ReadPage(dataPage), channel.Format); - byte[] descriptor = RowDecoder + byte[] descriptor = RowCodec .LongValueDescriptors(definition.Columns, channel.Format, parsed.GetRow(0)) .Single(d => d.Key == columnId).Value; - firstChainPage = descriptor[5] | (descriptor[6] << 8) | (descriptor[7] << 16); + firstChainPage = LongValueStore.Read(descriptor, channel.Format).Page; // Restamp the chain page alone, as a rewrite by another writer would. var page = new byte[channel.PageSize]; @@ -146,4 +150,4 @@ [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/CjkSortOrderGeneratorTest.cs b/test/LibRed.Core.AccessTests/CjkSortOrderGeneratorTest.cs index c2b9618a..a5adc9b4 100644 --- a/test/LibRed.Core.AccessTests/CjkSortOrderGeneratorTest.cs +++ b/test/LibRed.Core.AccessTests/CjkSortOrderGeneratorTest.cs @@ -56,7 +56,7 @@ public void Generate_the_sort_order_resource(string name) { if (!ace.TryGetValue(text, out string? key)) { refused++; continue; } string ours; - try { ours = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [text])); } + try { ours = Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [text])); } catch (NotSupportedException) { ours = "(refused)"; } if (ours == key) { agreed++; continue; } if (TryReadOneWeight(key, out byte[] primary, out byte secondary)) @@ -245,4 +245,4 @@ private static void Exec(System.Data.OleDb.OleDbConnection connection, string sq command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/CollationQueryProbeTests.cs b/test/LibRed.Core.AccessTests/CollationQueryProbeTests.cs index 81c4b4bb..a356142a 100644 --- a/test/LibRed.Core.AccessTests/CollationQueryProbeTests.cs +++ b/test/LibRed.Core.AccessTests/CollationQueryProbeTests.cs @@ -53,7 +53,7 @@ public void Group_by_folds_what_the_collation_folds(byte version) string path = TemporaryDatabase.CreatePath("collation-group-", ".accdb"); try { - DatabaseCreator.CreateEmpty(path, collation: new Collation(CollatingOrder.General, version)); + JetDatabase.Create(path, collation: new Collation(CollatingOrder.General, version)); using OleDbConnection ace = AceTestDatabase.Open(path); Exec(ace, "CREATE TABLE G (Id LONG, S TEXT(20))"); string[] values = ["Straße", "STRASSE", "strasse ", "x", "X", "café", "cafe"]; @@ -90,7 +90,7 @@ private static Dictionary Probe(bool stampHeader, bool stampCo string path = TemporaryDatabase.CreatePath("collation-query-", ".accdb"); try { - DatabaseCreator.CreateEmpty(path, collation: GeneralV1); + JetDatabase.Create(path, collation: GeneralV1); using (OleDbConnection connection = AceTestDatabase.Open(path)) Exec(connection, "CREATE TABLE T (Id LONG, S TEXT(20), P TEXT(20))"); @@ -99,7 +99,7 @@ private static Dictionary Probe(bool stampHeader, bool stampCo // The stamp landed where it was meant to, or the answer says nothing about where ACE looks. using (var check = JetDatabase.Open(path)) { - TableDef table = check.Catalog.FindTable("T")!; + TableDefinition table = check.Catalog.FindTable("T")!; Assert.Equal(stampHeader ? CroatianV1 : GeneralV1, check.Collation); Assert.Equal(stampColumn ? CroatianV1 : GeneralV1, table.FindColumn("S")!.Collation); Assert.Equal(GeneralV1, table.FindColumn("P")!.Collation); @@ -152,4 +152,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/CollationSurveyProbeTests.cs b/test/LibRed.Core.AccessTests/CollationSurveyProbeTests.cs index 48b1fd28..89dc17c3 100644 --- a/test/LibRed.Core.AccessTests/CollationSurveyProbeTests.cs +++ b/test/LibRed.Core.AccessTests/CollationSurveyProbeTests.cs @@ -741,7 +741,7 @@ private static Dictionary LibRedKeys(string[] samples, Collation foreach (string sample in samples) { var column = new ColumnDef { Name = "K", Type = JetDataType.Text, Index = 0, Collation = collation }; - try { keys[sample] = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [sample])); } + try { keys[sample] = Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [sample])); } catch (NotSupportedException) { /* not encodable - simply absent */ } } return keys; @@ -1072,4 +1072,4 @@ private static void Exec(System.Data.OleDb.OleDbConnection connection, string sq private static object? Invoke(object target, string member, params object?[] args) => target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/CollationV1PatchProbeTests.cs b/test/LibRed.Core.AccessTests/CollationV1PatchProbeTests.cs index 0cf4f936..24aa640e 100644 --- a/test/LibRed.Core.AccessTests/CollationV1PatchProbeTests.cs +++ b/test/LibRed.Core.AccessTests/CollationV1PatchProbeTests.cs @@ -4,6 +4,7 @@ using LibRed.Catalog; using LibRed.Formats; using LibRed.IO; +using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -152,7 +153,7 @@ public void Survey_version_1_across_every_known_lcid() Name = "K", Type = JetDataType.Text, Index = 0, Collation = collation, }; return keys.Count(k => - Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [k.Key])) != k.Value); + Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [k.Key])) != k.Value); } /// @@ -166,7 +167,7 @@ private Dictionary AceKeysThroughPatchedFile( string path = TemporaryDatabase.CreatePath("v1patch-", ".accdb"); try { - DatabaseCreator.CreateEmpty(path, collation: GeneralV1); + JetDatabase.Create(path, collation: GeneralV1); using (OleDbConnection connection = AceTestDatabase.Open(path)) { @@ -229,28 +230,23 @@ internal static void Stamp(string path, string tableName, string? columnName, Co if (header) { byte[] page0 = channel.ReadPage(0).Span.ToArray(); - byte[] mask = JetFormatBase.PageZeroHeaderMask.ToArray(); - int maskStart = JetFormatBase.PageZeroHeaderMaskStart; - ushort langId = (ushort)target.Order; - page0[0x6E] = (byte)((langId & 0xFF) ^ mask[0x6E - maskStart]); - page0[0x6F] = (byte)((langId >> 8) ^ mask[0x6F - maskStart]); - page0[0x70] = (byte)(target.SortId ^ mask[0x70 - maskStart]); - page0[0x71] = (byte)(target.Version ^ mask[0x71 - maskStart]); + Span langId = stackalloc byte[sizeof(ushort)]; + BinaryPrimitives.WriteUInt16LittleEndian(langId, (ushort)target.Order); + DatabaseDefinitionPage.WriteMasked(page0, format.CollationSortOrderOffset, langId, format); + DatabaseDefinitionPage.WriteMasked(page0, format.CollationSortIdOffset, [target.SortId], format); + DatabaseDefinitionPage.WriteMasked(page0, format.CollationVersionOffset, [target.Version], format); channel.WritePage(0, page0); } if (columnName is null) return; - TableDef table = new JetCatalog(channel).FindTable(tableName)!; + TableDefinition table = new JetCatalog(channel).FindTable(tableName)!; int columnIndex = table.Columns.Single(c => c.Name == columnName).Index; // The descriptors follow the real-index data blocks; each is ColumnDescriptorSize bytes, in column // order. Single-page TDEF only, which is all this probe's table needs. Not masked, unlike page 0. byte[] tdef = channel.ReadPage(table.DefinitionPage).Span.ToArray(); - int realIndexes = BinaryPrimitives.ReadInt32LittleEndian( - tdef.AsSpan(format.TdefIndexCountOffset, 4)); - int descriptor = format.TdefRealIndexBlockOffset + realIndexes * format.RealIndexEntrySize - + columnIndex * format.ColumnDescriptorSize; + int descriptor = TableDefinition.Regions.Of(tdef, format).ColumnDescriptors + columnIndex * format.ColumnDescriptorSize; BinaryPrimitives.WriteUInt16LittleEndian( tdef.AsSpan(descriptor + format.ColumnLocaleOffset, 2), (ushort)target.Order); @@ -265,4 +261,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/ColumnRetypeRelayAccessTests.cs b/test/LibRed.Core.AccessTests/ColumnRetypeRelayAccessTests.cs index c59a7f2a..ca11bf76 100644 --- a/test/LibRed.Core.AccessTests/ColumnRetypeRelayAccessTests.cs +++ b/test/LibRed.Core.AccessTests/ColumnRetypeRelayAccessTests.cs @@ -112,7 +112,7 @@ public void A_retype_handles_rows_of_two_different_fixed_widths() finally { TemporaryDatabase.Delete(path); } } - // The re-lay reaches RowEncoder.AssembleRow but never RowEncoder.Encode, where the declared-width check + // The re-lay reaches RowCodec.AssembleRow but never RowCodec.Encode, where the declared-width check // used to live — so a narrowing retype wrote values wider than the column declares. [Fact] public void A_retype_that_would_overflow_the_new_width_is_refused() @@ -177,7 +177,7 @@ public void Retyping_away_from_decimal_restores_the_collation_bytes() private static string CreateWithAce(string path, string[] ddl) { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var connection = AceTestDatabase.Open(path); foreach (string sql in ddl) Exec(connection, sql); return path; @@ -198,4 +198,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/ColumnWidthLimitAccessTests.cs b/test/LibRed.Core.AccessTests/ColumnWidthLimitAccessTests.cs index 983e4554..83838870 100644 --- a/test/LibRed.Core.AccessTests/ColumnWidthLimitAccessTests.cs +++ b/test/LibRed.Core.AccessTests/ColumnWidthLimitAccessTests.cs @@ -11,7 +11,7 @@ namespace LibRed.Core.Tests; // DDL on CREATE TABLE, ALTER COLUMN and ADD COLUMN alike ("Size of field is too long"); LibRed accepted // Text(2000) and Binary(4000) on all three and produced a table ACE opens but cannot query. // -// DECLARED RECORD SIZE. TdefBuilder checked only that the fixed region fits the TDEF's 2-byte offset +// DECLARED RECORD SIZE. TableDefinition checked only that the fixed region fits the TDEF's 2-byte offset // fields (65535). ACE's real limit is the 4060-byte record cap applied to the widest row the declaration // permits, and a table over it makes the whole database unopenable - "Unrecognized database format", the // same damage as a 100-character rename. A plain CreateTable of 252 GUID columns did exactly that. @@ -125,4 +125,4 @@ private static JetDatabase Fresh(out string path) database.OpenTable("T").Insert([1, "hello"]); return database; } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/CommitByteTableTests.cs b/test/LibRed.Core.AccessTests/CommitByteTableTests.cs index a0f876bb..e5c7296d 100644 --- a/test/LibRed.Core.AccessTests/CommitByteTableTests.cs +++ b/test/LibRed.Core.AccessTests/CommitByteTableTests.cs @@ -1,9 +1,10 @@ +using System.Buffers.Binary; using System.Data.OleDb; using Xunit; namespace LibRed.Core.Tests; -// What the 2-byte user commit slots at 0xE00 actually hold (measured 2026-08-26 against +// What the 2-byte user commit slots at the end of page 0 actually hold (measured 2026-08-26 against // Microsoft.ACE.OLEDB.16.0). The format docs had described them two different ways — page-00 §2.2 as // "per-file last-commit states", page-05 as an undecoded "counter" — so this pins it down. // @@ -15,7 +16,10 @@ namespace LibRed.Core.Tests; [Collection(AceCollection.Name)] public class CommitByteTableTests { - private const int Slot1 = 0xE02; // 0xE00 is slot 0 (exclusive mode); slot 1 is the first shared user + private static readonly Formats.JetFormatBase Format = TestDatabases.FormatOf(TestDatabases.NorthwindAccdb); + + // Slot 0 is the exclusive-mode state; slot 1 is the first shared user. + private static readonly int Slot1 = Format.CommitByteTableOffset + Format.CommitByteSlotSize; // The counter on disk LAGS the last write by one: its final increment is not flushed until the next write // (or until the connection closes, which lands the pending one plus its own). So the count is taken @@ -74,12 +78,13 @@ public void Only_the_active_user_slot_moves() // Slot 0 is the exclusive-mode state and stays put for a shared open; slots 2.. stay at the // neutral 00 01 because no second user ever registered. - Assert.Equal(before[0..2], after[0..2]); - Assert.Equal(before[4..16], after[4..16]); - Assert.All(Enumerable.Range(2, 6), i => Assert.Equal([0x00, 0x01], after[(i * 2)..(i * 2 + 2)])); + int slotSize = Format.CommitByteSlotSize; + Assert.Equal(before[0..slotSize], after[0..slotSize]); + Assert.Equal(before[(2 * slotSize)..(8 * slotSize)], after[(2 * slotSize)..(8 * slotSize)]); + Assert.All(Enumerable.Range(2, 6), i => Assert.Equal([0x00, 0x01], after[(i * slotSize)..(i * slotSize + slotSize)])); // And the one that did move only went up. - Assert.True(Counter(path, Slot1) > (before[2] | (before[3] << 8))); + Assert.True(Counter(path, Slot1) > BinaryPrimitives.ReadUInt16LittleEndian(before.AsSpan(slotSize, slotSize))); } finally { TemporaryDatabase.Delete(path); } } @@ -129,19 +134,19 @@ public void Compacting_restarts_the_counter_but_reopening_carries_it() /// The slot's 16-bit little-endian value, read while ACE still holds the file open. private static int Counter(string path, int offset) { - byte[] pair = new byte[2]; + byte[] pair = new byte[Format.CommitByteSlotSize]; using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); stream.Seek(offset, SeekOrigin.Begin); stream.ReadExactly(pair); - return pair[0] | (pair[1] << 8); + return BinaryPrimitives.ReadUInt16LittleEndian(pair); } - /// The first eight slots (0xE00..0xE0F). + /// The first eight slots. private static byte[] Slots(string path) { - byte[] slots = new byte[16]; + byte[] slots = new byte[8 * Format.CommitByteSlotSize]; using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - stream.Seek(0xE00, SeekOrigin.Begin); + stream.Seek(Format.CommitByteTableOffset, SeekOrigin.Begin); stream.ReadExactly(slots); return slots; } @@ -159,4 +164,4 @@ private static void Scalar(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteScalar(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/ComplexDeleteCascadeProbeTest.cs b/test/LibRed.Core.AccessTests/ComplexDeleteCascadeProbeTest.cs index 09f41f0c..be7e11ab 100644 --- a/test/LibRed.Core.AccessTests/ComplexDeleteCascadeProbeTest.cs +++ b/test/LibRed.Core.AccessTests/ComplexDeleteCascadeProbeTest.cs @@ -62,16 +62,15 @@ public void Probe_what_deleting_the_owning_record_does_to_its_values() using var db = JetDatabase.Open(path); output.WriteLine($"PROBE --- {label} ---"); - TableDef owner = db.Catalog.FindTable(Table)!; + TableDefinition owner = db.Catalog.FindTable(Table)!; ColumnDef idColumn = owner.Columns[0]; int? withValues = null; foreach (ComplexColumn c in db.Catalog.ComplexColumns.Where(c => c.OwnerTable.Name == Table)) { - ReadOnlySpan ownerPage = db.OpenTable(Table).Channel.ReadPageShared(owner.DefinitionPage).Span; ReadOnlySpan flatPage = db.OpenTable(c.FlatTable.Name).Channel.ReadPageShared(c.FlatTable.DefinitionPage).Span; - output.WriteLine($"PROBE {Table}.{c.ColumnName}: owner 0x1C={BinaryPrimitives.ReadInt32LittleEndian(ownerPage[0x1C..])}" - + $" flat 0x14={BinaryPrimitives.ReadInt32LittleEndian(flatPage[0x14..])}"); + output.WriteLine($"PROBE {Table}.{c.ColumnName}: owner 0x1C={owner.ComplexAutoNumber}" + + $" flat 0x14={BinaryPrimitives.ReadInt32LittleEndian(flatPage[db.Format.TdefLastAutoNumberOffset..])}"); ColumnDef inRow = owner.FindColumn(c.ColumnName)!; var ids = new List(); @@ -115,4 +114,4 @@ public void Probe_what_deleting_the_owning_record_does_to_its_values() progId = "(none)"; return null; } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/ComplexDuplicateValueProbeTest.cs b/test/LibRed.Core.AccessTests/ComplexDuplicateValueProbeTest.cs index 8c5de758..24f900e2 100644 --- a/test/LibRed.Core.AccessTests/ComplexDuplicateValueProbeTest.cs +++ b/test/LibRed.Core.AccessTests/ComplexDuplicateValueProbeTest.cs @@ -70,7 +70,7 @@ public void Probe_whether_a_record_may_hold_the_same_value_twice() // What actually landed, read back through LibRed. using var lib = JetDatabase.Open(path); - foreach (TableDef t in lib.Catalog.Tables.Where(t => t.Name.StartsWith("f_", StringComparison.Ordinal))) + foreach (TableDefinition t in lib.Catalog.Tables.Where(t => t.Name.StartsWith("f_", StringComparison.Ordinal))) { ColumnDef? link = t.Indexes.FirstOrDefault(i => !i.IsUnique && !i.IsPrimaryKey && i.Columns.Count == 1)?.Columns[0].Column; ColumnDef? value = t.FindColumn("Value"); @@ -127,4 +127,4 @@ private static void Set(object target, string member, object? value) => progId = "(none)"; return null; } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/CompositeIndexOrderingAccessTests.cs b/test/LibRed.Core.AccessTests/CompositeIndexOrderingAccessTests.cs index 4711dc03..1767653f 100644 --- a/test/LibRed.Core.AccessTests/CompositeIndexOrderingAccessTests.cs +++ b/test/LibRed.Core.AccessTests/CompositeIndexOrderingAccessTests.cs @@ -60,14 +60,14 @@ public void Libred_composite_key_bytes_and_traversal_match_ace() using var db = JetDatabase.Open(path); Table table = db.OpenTable("CompositeKeys"); IndexDef index = table.Definition.Indexes.Single(i => i.Name == "IX_CompositeKeys"); - var decoder = new RowDecoder(table.Definition.Columns, db.Format); + var decoder = new RowCodec(table.Definition.Columns, db.Format); int valueIndex = table.Definition.FindColumn("V")!.Index; var libredOrder = new List(); foreach ((byte[] storedKey, RowId rowId) in new IndexCursor(table.Channel, index.RootPage).RawEntries()) { object?[] row = decoder.Decode(db.ReadDataPage(rowId.Page).GetRow(rowId.Row)); - byte[] encoded = IndexKeyEncoder.Encode(index.Columns, row); + byte[] encoded = IndexKeyCodec.Encode(index.Columns, row); int value = Convert.ToInt32(row[valueIndex]); Assert.True(storedKey.AsSpan().SequenceEqual(encoded), $"V={value}: ACE={Convert.ToHexString(storedKey)} LibRed={Convert.ToHexString(encoded)}"); @@ -88,4 +88,4 @@ private static void Execute(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/CompressedTextAccessTests.cs b/test/LibRed.Core.AccessTests/CompressedTextAccessTests.cs index 48c5326f..0e606d16 100644 --- a/test/LibRed.Core.AccessTests/CompressedTextAccessTests.cs +++ b/test/LibRed.Core.AccessTests/CompressedTextAccessTests.cs @@ -79,17 +79,20 @@ public void One_memo_column_holds_compressed_and_uncompressed_values_together() } // All three storage forms, in the one column. + Formats.JetFormatBase format = TestDatabases.FormatOf(path); + int payload = format.LongValueDescriptorSize; Assert.Equal( ["0x80 compressed", "0x40 compressed", "0x00 plain", "0x80 plain"], Descriptors(path).Select(d => - $"0x{(byte)(d[3] & 0xC0):X2} {(d[12] == 0xFF && d[13] == 0xFE ? "compressed" : "plain")}")); + $"0x{(uint)Storage.LongValueStore.Read(d, format).Storage >> 24:X2} {(d[payload] == 0xFF && d[payload + 1] == 0xFE ? "compressed" : "plain")}")); // LibRed reads every form back... using (var channel = PageChannel.Open(path, readOnly: true)) { - TableDef definition = new JetCatalog(channel).FindTable("MemoProbe")!; + var catalog = new JetCatalog(channel); + TableDefinition definition = catalog.FindTable("MemoProbe")!; int column = definition.Columns.Single(c => c.Name == "C").Index; - Assert.Equal(values, new Table(channel, definition).Rows().Select(r => (string)r[column]!)); + Assert.Equal(values, new Table(channel, definition, catalog).Rows().Select(r => (string)r[column]!)); } // ...and so does ACE. @@ -109,9 +112,9 @@ public void One_memo_column_holds_compressed_and_uncompressed_values_together() private static IEnumerable Descriptors(string path) { using var channel = PageChannel.Open(path, readOnly: true); - TableDef definition = new JetCatalog(channel).FindTable("MemoProbe")!; + TableDefinition definition = new JetCatalog(channel).FindTable("MemoProbe")!; ColumnDef column = definition.Columns.Single(c => c.Name == "C"); - var reader = new LongValueReader(channel); + var reader = new LongValueStore(channel); var result = new List(); foreach (int number in new UsageMap(channel, definition).DataPages()) @@ -121,11 +124,11 @@ private static IEnumerable Descriptors(string path) for (int row = 0; row < page.RowCount; row++) { if (page.Rows[row].IsDeleted) continue; - foreach (var raw in RowDecoder.LongValueDescriptors(definition.Columns, channel.Format, page.GetRow(row))) + foreach (var raw in RowCodec.LongValueDescriptors(definition.Columns, channel.Format, page.GetRow(row))) { if (raw.Key != column.ColumnId) continue; byte[] payload = reader.Resolve(raw.Value); - result.Add([.. raw.Value[..12], .. payload.Length >= 2 ? payload[..2] : new byte[2]]); + result.Add([.. raw.Value[..channel.Format.LongValueDescriptorSize], .. payload.Length >= 2 ? payload[..2] : new byte[2]]); } } } @@ -134,8 +137,9 @@ private static IEnumerable Descriptors(string path) private static string Describe(byte[] descriptor) { - uint word = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(descriptor); - return $"length={word & Formats.LongValueFormat.LengthMask} flags=0x{(byte)(descriptor[3] & 0xC0):X2}"; + (int length, Storage.LongValueStore.StorageKind storage, _, _, _) = + Storage.LongValueStore.Read(descriptor, TestDatabases.FormatOf(TestDatabases.NorthwindAccdb)); + return $"length={length} storage={storage}"; } private static byte[] AceBytes(string ddl, string insert, string value, OleDbType type, string column) @@ -181,7 +185,7 @@ private static byte[] LibRedBytes(string ddl, string value, string column) private static byte[] StoredBytes(string path, string columnName) { using var channel = PageChannel.Open(path, readOnly: true); - TableDef definition = new JetCatalog(channel).FindTable("TextProbe") + TableDefinition definition = new JetCatalog(channel).FindTable("TextProbe") ?? new JetCatalog(channel).FindTable("MemoProbe")!; ColumnDef column = definition.Columns.Single(c => c.Name == columnName); @@ -195,14 +199,14 @@ private static byte[] StoredBytes(string path, string columnName) byte[] bytes = page.GetRow(row).ToArray(); if (column.Type == JetDataType.Memo) { - foreach (var raw in RowDecoder.LongValueDescriptors(definition.Columns, channel.Format, bytes)) + foreach (var raw in RowCodec.LongValueDescriptors(definition.Columns, channel.Format, bytes)) if (raw.Key == column.ColumnId) - return raw.Value[..12]; + return raw.Value[..channel.Format.LongValueDescriptorSize]; throw new InvalidOperationException("No long-value descriptor for the memo column."); } - return RowLayout.Parse(bytes, 2, hasVar: true).VarChunk(column.VariableIndex).ToArray(); + return RowCodec.Layout.Parse(bytes, channel.Format, hasVar: true).VarChunk(column.VariableIndex).ToArray(); } } throw new InvalidOperationException("No live row found."); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/ContractionProbeTest.cs b/test/LibRed.Core.AccessTests/ContractionProbeTest.cs index a1c4bb7b..cacf450c 100644 --- a/test/LibRed.Core.AccessTests/ContractionProbeTest.cs +++ b/test/LibRed.Core.AccessTests/ContractionProbeTest.cs @@ -312,7 +312,7 @@ public void Probe_presentation_form_keys() "ا", "ب", "َ", "ُ", "ِ", "ّ"]); string path = TemporaryDatabase.CreatePath("presentation-v1-"); - DatabaseCreator.CreateEmpty(path, collation: Collation.General); + JetDatabase.Create(path, collation: Collation.General); try { Dictionary keys = AceKeys(path, [.. samples]); @@ -324,7 +324,7 @@ public void Probe_presentation_form_keys() { if (!keys.TryGetValue(sample, out string? ace)) continue; string ours; - try { ours = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [sample])); } + try { ours = Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [sample])); } catch (NotSupportedException) { ours = "(refused)"; } if (ours == ace) continue; output.WriteLine($" {Describe(sample),-10} ACE {ace,-26} ours {ours}"); @@ -447,4 +447,4 @@ private static void Exec(System.Data.OleDb.OleDbConnection connection, string sq command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/ControlCharacterKeyAccessTests.cs b/test/LibRed.Core.AccessTests/ControlCharacterKeyAccessTests.cs index fd002e9c..7628e33a 100644 --- a/test/LibRed.Core.AccessTests/ControlCharacterKeyAccessTests.cs +++ b/test/LibRed.Core.AccessTests/ControlCharacterKeyAccessTests.cs @@ -34,7 +34,7 @@ private static void AssertKeysMatchAce(Collation collation) var column = new ColumnDef { Name = "T", Type = JetDataType.Text, Index = 0, Collation = collation }; var wrong = ace - .Where(k => Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [k.Key])) != k.Value) + .Where(k => Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [k.Key])) != k.Value) .Select(k => $"{string.Join("+", k.Key.Select(ch => $"U+{(int)ch:X4}"))}: ACE {k.Value}") .ToList(); Assert.True(wrong.Count == 0, string.Join("; ", wrong)); @@ -45,7 +45,7 @@ private static Dictionary AceKeys(Collation collation, string[] string path = TemporaryDatabase.CreatePath("control-keys-", ".accdb"); try { - DatabaseCreator.CreateEmpty(path, collation: collation); + JetDatabase.Create(path, collation: collation); using (OleDbConnection ace = AceTestDatabase.Open(path)) { Exec(ace, "CREATE TABLE K (Id LONG, T TEXT(10))"); @@ -81,4 +81,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/CreatedDatabaseCollationAccessTests.cs b/test/LibRed.Core.AccessTests/CreatedDatabaseCollationAccessTests.cs index 6f1c6060..a3157805 100644 --- a/test/LibRed.Core.AccessTests/CreatedDatabaseCollationAccessTests.cs +++ b/test/LibRed.Core.AccessTests/CreatedDatabaseCollationAccessTests.cs @@ -20,6 +20,60 @@ namespace LibRed.Core.Tests; [Collection(AceCollection.Name)] public class CreatedDatabaseCollationAccessTests(ITestOutputHelper output) { + // These exact collation values were read from the former 40 locale fixtures. + // Keep broad character coverage here without multiplying it across all creation cases. + private static readonly HashSet FullSampleCollations = + [ + new(CollatingOrder.General, 0, 0), // Northwind + new(CollatingOrder.SpanishModern, 0, 0), // SpanishModern + new(CollatingOrder.German, 0, 1), // GermanPhoneBook + new(CollatingOrder.Polish, 0, 0), // Polish + new(CollatingOrder.Romanian, 0, 0), // RomanianLegacy + new(CollatingOrder.Turkish, 0, 0), // Turkish + new(CollatingOrder.Georgian, 0, 1), // GeorgianModern + new(CollatingOrder.Indic, 1, 0), // Indic + new(CollatingOrder.Spanish, 0, 0), // SpanishTraditional + new(CollatingOrder.Czech, 0, 0), // Czech + new(CollatingOrder.Croatian, 0, 0), // CroatianLegacy + new(CollatingOrder.Norwegian, 0, 0), // NorwegianDanish + new(CollatingOrder.Hungarian, 0, 0), // Hungarian + new(CollatingOrder.Estonian, 0, 0), // Estonian + new(CollatingOrder.Icelandic, 0, 0), // Icelandic + new(CollatingOrder.Latvian, 0, 0), // Latvian + new(CollatingOrder.Lithuanian, 0, 0), // Lithuanian + new(CollatingOrder.Slovenian, 0, 0), // Slovenian + new(CollatingOrder.SwedishFinnish, 0, 0), // SwedishFinnish + new(CollatingOrder.Slovak, 0, 0), // Slovak + new(CollatingOrder.Vietnamese, 0, 0), // Vietnamese + new(CollatingOrder.Hungarian, 0, 1), // HungarianTechnical + new(CollatingOrder.Ukrainian, 0, 0), // Ukrainian + new(CollatingOrder.Macedonian, 0, 0), // Macedonian + new(CollatingOrder.Croatian, 1, 0), // Croatian + new(CollatingOrder.Bosnian, 1, 0), // Bosnian + new(CollatingOrder.Serbian, 1, 0), // Serbian + new(CollatingOrder.Thai, 0, 0), // Thai + new(CollatingOrder.ChineseSimplified, 1, 0), // ChinesePronunciation + new(CollatingOrder.ChineseSimplified, 0, 0), // ChinesePronunciationLegacy + new(CollatingOrder.ChineseSimplified, 1, 2), // ChineseStrokeCount + new(CollatingOrder.ChineseSimplified, 0, 2), // ChineseStrokeCountLegacy + new(CollatingOrder.ChineseTraditional, 1, 3), // ChineseTradBopomofo + new(CollatingOrder.ChineseTraditional, 0, 3), // ChineseTradBopomofoLegacy + new(CollatingOrder.ChineseTraditional, 1, 0), // ChineseTradStrokeCount + new(CollatingOrder.ChineseTraditional, 0, 0), // ChineseTradStrokeCountLegacy + new(CollatingOrder.Japanese, 1, 0), // Japanese + new(CollatingOrder.Japanese, 0, 0), // JapaneseLegacy + new(CollatingOrder.Japanese, 1, 4), // JapaneseRadicalStrokeCount + new(CollatingOrder.Korean, 0, 0), // Korean + ]; + + private static readonly string[] SmokeSamples = + [ + "apple", "café", "coté", "côte", "Ångström", "co-op", "O'Brien", + "ñ", "č", "ž", "lj", "dž", "ch", "ll", "ı", "İ", "å", "ø", "ß", + "เก", "ไทย", "Ω", "б", "א", + "一", "漢字", "人々", "カタカナ", "がくせい", "한국", "韓國", @"C:\", + ]; + /// Every collation LibRed claims to encode, found by asking rather than by keeping a list that /// could fall out of step with JetLocaleTailoring. public static TheoryData EncodableCollations() @@ -41,23 +95,13 @@ public void LibRed_creates_a_database_that_ACE_indexes(int order, byte version, string path = TemporaryDatabase.CreatePath($"created-{order}-{version}-{sortId}-"); try { - DatabaseCreator.CreateEmpty(path, collation: collation); + JetDatabase.Create(path, collation: collation); // The order has to survive the round trip, or everything below is measuring the wrong thing. using (var db = JetDatabase.Open(path)) Assert.Equal(collation, db.Collation); - // Words that exercise the tailorings across the set: accented Latin, the digraphs, a word-sort - // ignorable, Thai's leading vowel, and the CJK tables — ideographs, an iteration mark, hanja beside - // Hangul, and the backslash two of them reweigh. Any order encodes all of them; what differs is the - // keys. - string[] samples = - [ - "apple", "café", "coté", "côte", "Ångström", "co-op", "O'Brien", - "ñ", "č", "ž", "lj", "dž", "ch", "ll", "ı", "İ", "å", "ø", "ß", - "เก", "ไทย", "Ω", "б", "א", - "一", "漢字", "人々", "カタカナ", "がくせい", "한국", "韓國", @"C:\", - ]; + string[] samples = FullSampleCollations.Contains(collation) ? Samples() : SmokeSamples; Dictionary ace = AceKeys(path, samples); Assert.NotEmpty(ace); @@ -71,24 +115,107 @@ public void LibRed_creates_a_database_that_ACE_indexes(int order, byte version, foreach (string text in samples) { if (!ace.TryGetValue(text, out string? stored)) continue; // ACE refused the value - string ours = Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [text])); - if (ours != stored) mismatches.Add($" {text,-12} ACE {stored,-30} LibRed {ours}"); + string ours = Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [text])); + if (ours != stored) mismatches.Add($" {Describe(text),-16} ACE {stored,-30} LibRed {ours}"); } output.WriteLine($"{collation.Order} v{version}" + (sortId == 0 ? "" : $" sort id {sortId}") + $": ACE indexed {ace.Count} of {samples.Length} values into a LibRed-created " + $"database, {mismatches.Count} disagreeing"); - foreach (string line in mismatches) output.WriteLine(line); + foreach (string line in mismatches.Take(40)) output.WriteLine(line); Assert.Empty(mismatches); } finally { TemporaryDatabase.Delete(path); } } + /// Printable ASCII, all of Latin-1 and Latin Extended-A, and a few words — so a tailoring is + /// tested well beyond the handful of characters it actually overrides. + private static string[] Samples() + { + var samples = new List(SmokeSamples); + for (char c = ' '; c <= '~'; c++) samples.Add(c.ToString()); + for (char c = ' '; c <= 'ſ'; c++) samples.Add(c.ToString()); + // Every further block JetTextCollationBlocks covers — Greek, Cyrillic, Hebrew, Arabic, the Latin + // extensions, punctuation, currency and the fullwidth forms — so the whole measured range stays + // guarded rather than only the range a tailoring happens to mention. + (int First, int Last)[] blocks = + [ + (0x0180, 0x024F), (0x02B0, 0x02FF), (0x0370, 0x052F), (0x0590, 0x06FF), + (0x0E01, 0x0E5B), // Thai: consonants, vowels, tone marks and digits + (0x1E00, 0x1EFF), (0x2000, 0x206F), (0x20A0, 0x20BF), (0x2100, 0x218F), (0xFF01, 0xFF65), + (0x3040, 0x30FF), (0xFF66, 0xFF9F), // kana: hiragana, katakana, halfwidth katakana + ]; + foreach ((int first, int last) in blocks) + for (int c = first; c <= last; c++) + if (!char.IsControl((char)c) && !char.IsSurrogate((char)c)) + samples.Add(((char)c).ToString()); + + // The CJK blocks, where the CJK orders put their weights: the symbols, Bopomofo, jamo and enclosed forms + // whole, and a stride through the large ones — every 7th ideograph, every 11th syllable — which crosses + // every page of their tables while keeping the suite to seconds. The whole BMP is what the generator + // measures; this keeps what it wrote honest. + { + (int First, int Last, int Stride)[] cjk = + [ + (0x1100, 0x11FF, 1), (0x3000, 0x303F, 1), (0x3100, 0x312F, 1), (0x3130, 0x318F, 1), + (0x3190, 0x31BF, 1), (0x3200, 0x33FF, 1), (0x3400, 0x4DBF, 7), (0x4E00, 0x9FFF, 7), + (0xAC00, 0xD7A3, 11), (0xF900, 0xFAFF, 1), + ]; + foreach ((int first, int last, int stride) in cjk) + for (int c = first; c <= last; c += stride) + samples.Add(((char)c).ToString()); + } + + samples.AddRange([ + "apple", "Apple", "APPLE", "cafe", "café", "Ångström", "O'Brien", "Anne-Marie", "co-op", "coop", + "Łódź", "Kraków", "İstanbul", "Isparta", "ırmak", "Ğğ", "München", "Grüße", "Bär", "Baer", + "România", "Timișoara", "Iași", "señor", "senor", "mañana", + // Digraphs, the strings that must NOT contract, and the doubled forms. + "ch", "cch", "chh", "ll", "lll", "llll", "cs", "dz", "dzs", "gy", "ly", "ny", "sz", "ty", "zs", + "ccs", "ddz", "ggy", "lly", "nny", "ssz", "tty", "zzs", "gyy", "hc", "dzz", + "lj", "nj", "dž", "ddž", "llj", "nnj", "aa", "aaa", "aab", "baa", "Aa", "AA", + // Thai: each leading vowel before a consonant, the reverse order (which must NOT contract), the + // vowel with nothing to attach to, and words where the contraction meets tone marks. + "เก", "แก", "โก", "ใก", "ไก", "กเ", "กแ", "กโ", "กใ", "กไ", "เ", "เเ", "เ ", " เ", + "เกา", "เก้า", "ไก่", "ไทย", "แดง", "โกรธ", "ใหม่", "ประเทศไทย", "ภาษาไทย", "สวัสดี", "เรียน", + // An ignorable AFTER a two-byte primary: the inline position counts weights, not bytes, and only + // a non-Latin or symbol character ahead of it can tell those two rules apart. + "£-", "©-", "½-", "£A-", "A£-", "Ω-", "б-", "£'", "Ω'A", "€-B", + // Kana: voicing, small forms and their packing, mixed scripts, and a kana beside an ignorable — + // which changes the inline section's introducer. + "あい", "ぁ", "あぁ", "ぁぁ", "ああぁ", "ぁああ", "あいう", + "かが", "ぱば", "アイ", "アイ", "あア", "あA", "Aあ", "あé", "あ-", "-あ", "あ'", + "ニホンゴ", "にほんご", "ニホンゴ", "ちょっと", "キャッシュ", + // The prolonged mark: alone, with nothing to lengthen, and after every kind of kana. + "ー", "ーあ", "あー", "あいー", "あーい", "ああー", "あああー", "あーー", "あーあー", + "ぁー", "がー", "アー", "アー", "コーヒー", "コーヒー", "サーバー", + // Iteration marks: a copy of what came before with the mark's own secondary — after a kana (joining + // the kana section as a repeat), a voiced or small one, an ideograph, a Latin letter, an accented one, + // an inline record, a combining accent; chained, after the long vowel mark, before it, and with + // nothing to copy (at the start, after an expansion, after a mark that found nothing). + "かゝ", "かゞ", "がゝ", "がゞ", "ぱゞ", "ゃゝ", "カヽ", "カヾ", "かヽ", "かゝゝ", "かーゝ", "かゝー", + "カヾ", "ガゝ", "か〱", "か〲", "か々", "か々ゝ", "かー々", "ヴゞ", "㋐ゝ", + "人々", "人々々", "人々ー", "人ゝ", "人ー", "人ー", "人〱", "人〻", "a々", "é々", "é々", "aー", + "'々", "人-々", "½々", "\t々", "ß々", "æ々", "々", "々々", "ゝゝ", "ー々", "々人", "ꀀꀕ", "ꀀꀕꀕ", + "々が", "ーが", "ゝが", "ゞé", "〱é", "ーが", "ß々é", "々々é", "ー々é", + // A Han character counts twice in version 1's inline section, a one-byte primary once. + "人-", "人人-", "人-a", "人'", "ະ-", "aະ-", "가-", "ᄀ-", + // Words through the CJK tables, beside Latin and kana: a tailored weight next to General's, and the + // context rules running over tailored weights (Korean moves the kana a long vowel mark copies). + "中文", "中国", "中國", "漢字", "日本語", "時々", "한글", "대한민국", "韓國", "ㄅㄆㄇ", "\\", + "―", "a中", "中a", "a한", "한a", "カー", "カタカナ", "中-文", "한-글", "가", + "東京タワー", "ソウル", "北京", + "chico", "llama", "coche", "calle", "chata", "hodina", "cukr", + "ljubav", "njegov", "džem", "meggy", "asszony", "nagy", "cukor", "csak", + ]); + return [.. samples.Distinct()]; + } + private static Dictionary AceKeys(string path, string[] samples) { using (var connection = AceTestDatabase.Open(path)) { - Exec(connection, "CREATE TABLE Probe (K TEXT(50), V LONG)"); + Exec(connection, "CREATE TABLE Probe (K TEXT(100), V LONG)"); Exec(connection, "CREATE INDEX IX_Probe ON Probe (K)"); for (int i = 0; i < samples.Length; i++) { @@ -113,10 +240,13 @@ private static Dictionary AceKeys(string path, string[] samples) return keys; } + private static string Describe(string s) => + s.All(c => c is >= ' ' and <= '~') ? $"\"{s}\"" : string.Concat(s.Select(c => $"U+{(int)c:X4}")); + private static void Exec(System.Data.OleDb.OleDbConnection connection, string sql) { using var command = connection.CreateCommand(); command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/CreatedJet4DatabaseAccessTests.cs b/test/LibRed.Core.AccessTests/CreatedJet4DatabaseAccessTests.cs index cf0bd0f9..5256e729 100644 --- a/test/LibRed.Core.AccessTests/CreatedJet4DatabaseAccessTests.cs +++ b/test/LibRed.Core.AccessTests/CreatedJet4DatabaseAccessTests.cs @@ -17,7 +17,7 @@ public void Ace_reads_and_writes_a_jet4_database_libred_created() .Replace(".accdb", ".mdb", StringComparison.OrdinalIgnoreCase); try { - DatabaseCreator.CreateEmpty(path, version: 0x01); + JetDatabase.Create(path, version: 0x01); using (var db = JetDatabase.Open(path, readOnly: false)) { db.CreateTable("People", [ @@ -56,4 +56,4 @@ public void Ace_reads_and_writes_a_jet4_database_libred_created() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/DatabaseEncryptionTests.cs b/test/LibRed.Core.AccessTests/DatabaseEncryptionTests.cs index 0c027a66..218afea5 100644 --- a/test/LibRed.Core.AccessTests/DatabaseEncryptionTests.cs +++ b/test/LibRed.Core.AccessTests/DatabaseEncryptionTests.cs @@ -264,7 +264,7 @@ public void Malformed_encryption_info_length_is_rejected_without_writing(int des DatabaseEncryption.SetPassword(db, "pw", AccessEncryption.OfficeStandardAes); byte[] malformed = File.ReadAllBytes(path); System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian( - malformed.AsSpan(0x299, 2), checked((ushort)descriptorLength)); + malformed.AsSpan(TestDatabases.FormatOf(Plain).EncryptionInfoLengthOffset, 2), checked((ushort)descriptorLength)); File.WriteAllBytes(path, malformed); Exception? error = Record.Exception(() => @@ -321,4 +321,4 @@ public void Invalid_schemes_are_rejected() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/DateTime2KeyEncodingTests.cs b/test/LibRed.Core.AccessTests/DateTime2KeyEncodingTests.cs index 8983a9ba..de418879 100644 --- a/test/LibRed.Core.AccessTests/DateTime2KeyEncodingTests.cs +++ b/test/LibRed.Core.AccessTests/DateTime2KeyEncodingTests.cs @@ -49,7 +49,8 @@ private static void AssertKeysMatchAccess(string indexDdl) string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "libred-dt2key-"); try { - SetVersionByte(path, 0x06); + using (var raised = JetDatabase.Open(path, readOnly: false)) + raised.EnsureFormatAtLeast(Formats.JetVersion.Version17_2019); using (OleDbConnection connection = AceTestDatabase.Open(path)) { @@ -64,7 +65,7 @@ private static void AssertKeysMatchAccess(string indexDdl) var def = table.Definition; IndexDef index = def.Indexes.Single(i => i.Columns.Any(c => c.Column.Name == "K")); int kIdx = def.FindColumn("K")!.Index; - var decoder = new RowDecoder(def.Columns, db.Format); + var decoder = new RowCodec(def.Columns, db.Format); int checkedKeys = 0; foreach ((byte[] accessKey, RowId rowId) in new IndexCursor(table.Channel, index.RootPage).RawEntries()) @@ -73,7 +74,7 @@ private static void AssertKeysMatchAccess(string indexDdl) var values = new object?[def.Columns.Count]; values[kIdx] = value; - byte[] ours = IndexKeyEncoder.Encode(index.Columns, values); + byte[] ours = IndexKeyCodec.Encode(index.Columns, values); Assert.True(accessKey.AsSpan().SequenceEqual(ours), $"{value:O}: access={Convert.ToHexString(accessKey)} ours={Convert.ToHexString(ours)}"); @@ -91,12 +92,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } - - /// Raises a copied file to the ACE 17 format. Page 0 offset 0x14 is the whole upgrade. - private static void SetVersionByte(string path, byte version) - { - using var stream = new FileStream(path, FileMode.Open, FileAccess.Write); - stream.Seek(0x14, SeekOrigin.Begin); - stream.WriteByte(version); - } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/DecimalKeyEncodingTests.cs b/test/LibRed.Core.AccessTests/DecimalKeyEncodingTests.cs index c93d9519..e11ff618 100644 --- a/test/LibRed.Core.AccessTests/DecimalKeyEncodingTests.cs +++ b/test/LibRed.Core.AccessTests/DecimalKeyEncodingTests.cs @@ -49,7 +49,7 @@ private static void AssertKeysMatchAccess(string ddl, string indexPredicate) var def = table.Definition; IndexDef index = def.Indexes.Single(i => i.Columns.Any(c => c.Column.Name == "K")); int kIdx = def.FindColumn("K")!.Index; - var decoder = new RowDecoder(def.Columns, db.Format); + var decoder = new RowCodec(def.Columns, db.Format); int checkedKeys = 0; foreach (var (accessKey, rowId) in new IndexCursor(table.Channel, index.RootPage).RawEntries()) @@ -58,7 +58,7 @@ private static void AssertKeysMatchAccess(string ddl, string indexPredicate) var values = new object?[def.Columns.Count]; values[kIdx] = d; - byte[] ours = IndexKeyEncoder.Encode(index.Columns, values); + byte[] ours = IndexKeyCodec.Encode(index.Columns, values); Assert.True(accessKey.AsSpan().SequenceEqual(ours), $"{d}: access={Convert.ToHexString(accessKey)} ours={Convert.ToHexString(ours)}"); @@ -109,14 +109,13 @@ public void A_negative_zero_keeps_its_sign_and_its_key_through_a_libred_rewrite( (byte[] accessKey, _) = new IndexCursor(table.Channel, index.RootPage).RawEntries().Single(); Assert.True(decimal.IsNegative((decimal)values[kIdx]!), "ACE stored the sign"); - Assert.Equal(Convert.ToHexString(accessKey), Convert.ToHexString(IndexKeyEncoder.Encode(index.Columns, values))); + Assert.Equal(Convert.ToHexString(accessKey), Convert.ToHexString(IndexKeyCodec.Encode(index.Columns, values))); var updated = (object?[])values.Clone(); updated[vIdx] = 2; table.Update(rowId, updated); Assert.True(decimal.IsNegative((decimal)table.GetRow(rowId)![kIdx]!), "the rewrite kept the sign"); - table.RemoveIndexEntry(index, updated, rowId); table.Delete(rowId); } @@ -127,4 +126,4 @@ public void A_negative_zero_keeps_its_sign_and_its_key_through_a_libred_rewrite( } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/DeleteAccessTests.cs b/test/LibRed.Core.AccessTests/DeleteAccessTests.cs index 6bb86977..62312695 100644 --- a/test/LibRed.Core.AccessTests/DeleteAccessTests.cs +++ b/test/LibRed.Core.AccessTests/DeleteAccessTests.cs @@ -34,7 +34,6 @@ public void Access_reads_a_libred_deleted_row_as_gone() var pk = table.Definition.Indexes.First(i => i.IsPrimaryKey); (RowId id, object?[] values) = table.Rows().WithIds().First(x => Convert.ToInt32(x.Values[idIdx]) == 3); - table.RemoveIndexEntry(pk, values, id); table.Delete(id); } @@ -79,8 +78,6 @@ public void A_delete_that_empties_pages_gives_back_what_ace_gives_back() foreach ((RowId id, object?[] values) in table.Rows().WithIds().ToList()) { if (Convert.ToInt32(values[0]) >= 700) continue; - foreach (IndexDef index in table.Definition.RealIndexes) - table.RemoveIndexEntry(index, values, id); table.Delete(id); } } @@ -112,8 +109,6 @@ public void Deleting_every_row_keeps_only_the_tables_first_page() owned = [.. new UsageMap(database.Channel, table.Definition).DataPages()]; foreach ((RowId id, object?[] values) in table.Rows().WithIds().ToList()) { - foreach (IndexDef index in table.Definition.RealIndexes) - table.RemoveIndexEntry(index, values, id); table.Delete(id); } } @@ -133,4 +128,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/DerivedTableViewAccessTests.cs b/test/LibRed.Core.AccessTests/DerivedTableViewAccessTests.cs index b29df26b..8d4798b6 100644 --- a/test/LibRed.Core.AccessTests/DerivedTableViewAccessTests.cs +++ b/test/LibRed.Core.AccessTests/DerivedTableViewAccessTests.cs @@ -85,13 +85,13 @@ private static (List Attr, List Expr, List Name1, List string.Equals(r[oName] as string, viewName, StringComparison.OrdinalIgnoreCase))[oId] is int i ? i : 0; int qObj = Col(mq, "ObjectId"), qAttr = Col(mq, "Attribute"), qExpr = Col(mq, "Expression"), qN1 = Col(mq, "Name1"), qN2 = Col(mq, "Name2"); (List, List, List, List) acc = ([], [], [], []); - foreach (var r in new Table(ch, mq).Rows()) + foreach (var r in new Table(ch, mq, cat).Rows()) { if (Convert.ToInt32(r[qObj]) != id) continue; acc.Item1.Add(Convert.ToByte(r[qAttr])); @@ -102,6 +102,6 @@ private static (List Attr, List Expr, List Name1, List + private static int Col(TableDefinition t, string name) => t.Columns.First(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase)).Index; -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/DropColumnConstraintAccessTests.cs b/test/LibRed.Core.AccessTests/DropColumnConstraintAccessTests.cs index 2cf0c927..a87e33c7 100644 --- a/test/LibRed.Core.AccessTests/DropColumnConstraintAccessTests.cs +++ b/test/LibRed.Core.AccessTests/DropColumnConstraintAccessTests.cs @@ -4,7 +4,7 @@ namespace LibRed.Core.Tests; // Ground truth for our DROP COLUMN guard: ACE REJECTS dropping a column that is part of an index/key or a -// relationship — it never cascades, you must drop the dependent first. (Our TableCreator.DropColumn mirrors +// relationship — it never cascades, you must drop the dependent first. (Our SchemaEditor.DropColumn mirrors // this: it throws for an indexed/keyed column and for a column participating in a relationship.) [Collection(AceCollection.Name)] public class DropColumnConstraintAccessTests @@ -38,4 +38,4 @@ public void Access_rejects_dropping_an_indexed_or_related_column() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/DropTableParityAccessTests.cs b/test/LibRed.Core.AccessTests/DropTableParityAccessTests.cs index fdd492b1..ca414fbd 100644 --- a/test/LibRed.Core.AccessTests/DropTableParityAccessTests.cs +++ b/test/LibRed.Core.AccessTests/DropTableParityAccessTests.cs @@ -13,7 +13,7 @@ namespace LibRed.Core.Tests; [Collection(AceCollection.Name)] public class DropTableParityAccessTests(ITestOutputHelper output) : TempDatabaseTest { - private const int PageSize = 4096; + private static readonly Formats.JetFormatBase Format = TestDatabases.FormatOf(TestDatabases.NorthwindAccdb); [Theory] [InlineData("primary key", "CREATE TABLE Doomed (Id LONG CONSTRAINT pk PRIMARY KEY, M MEMO)", 40, 20_000)] @@ -78,14 +78,14 @@ public void Libred_sizes_the_released_map_as_ace_does_across_sessions() [Fact(Explicit = true)] public void Libred_converts_the_released_map_without_a_bitmap_page_for_an_empty_range() { - const int Range2 = 2 * 32_736; + int range2 = 2 * Format.UsageMapPagesPerBitmapPage; string start = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "droppar-start-"); using (OleDbConnection connection = AceTestDatabase.Open(start)) FillWide(connection, "A", 700); using (OleDbConnection connection = AceTestDatabase.Open(start)) FillWide(connection, "C", 1); using (OleDbConnection connection = AceTestDatabase.Open(start)) FillWide(connection, "B", 1); - for (int rows = 1; new FileInfo(start).Length / PageSize < Range2 + 40;) + for (int rows = 1; new FileInfo(start).Length / Format.PageSize < range2 + 40;) { - int add = (int)Math.Clamp(Range2 + 40 - new FileInfo(start).Length / PageSize, 64, 32_000); + int add = (int)Math.Clamp(range2 + 40 - new FileInfo(start).Length / Format.PageSize, 64, 32_000); using (OleDbConnection connection = AceTestDatabase.Open(start)) AppendWide(connection, "B", rows, add); rows += add; } @@ -105,7 +105,7 @@ public void Libred_converts_the_released_map_without_a_bitmap_page_for_an_empty_ Assert.True(db.DropTable("C")); } - output.WriteLine($"comparing {new FileInfo(ace).Length / PageSize} pages"); + output.WriteLine($"comparing {new FileInfo(ace).Length / Format.PageSize} pages"); string difference = Difference(ace, libred); output.WriteLine(difference); Assert.Equal("", difference); @@ -171,7 +171,7 @@ private void AssertDropsMatchAce(string label, string start, params string[] tab using (var db = JetDatabase.Open(libred, readOnly: false)) Assert.True(db.DropTable(table)); - output.WriteLine($"{label}: comparing {new FileInfo(ace).Length / PageSize} pages"); + output.WriteLine($"{label}: comparing {new FileInfo(ace).Length / Format.PageSize} pages"); string difference = Difference(ace, libred); output.WriteLine(difference); Assert.Equal("", difference); @@ -185,25 +185,25 @@ internal static string Difference(string acePath, string libredPath) { byte[] ace = File.ReadAllBytes(acePath), libred = File.ReadAllBytes(libredPath); var differences = new StringBuilder(); - int pages = Math.Max(ace.Length, libred.Length) / PageSize, lines = 0; + int pages = Math.Max(ace.Length, libred.Length) / Format.PageSize, lines = 0; for (int page = 1; page < pages && lines < 200; page++) { - int at = page * PageSize; - bool inAce = at + PageSize <= ace.Length, inLibRed = at + PageSize <= libred.Length; + int at = page * Format.PageSize; + bool inAce = at + Format.PageSize <= ace.Length, inLibRed = at + Format.PageSize <= libred.Length; if (!inAce || !inLibRed) { differences.AppendLine($"page {page}: present in {(inAce ? "ACE" : "LibRed")} only"); lines++; continue; } - if (BitConverter.ToInt32(ace, at + 4) == 2) continue; + if (LibRed.Pages.DataPage.ReadOwner(ace.AsSpan(at, Format.PageSize), Format) == 2) continue; if (LibRed.Pages.PageHeader.ReadType(ace.AsSpan(at)) is LibRed.Pages.PageType.IntermediateIndexPage or LibRed.Pages.PageType.LeafIndexPage) continue; - for (int i = 0, shown = 0; i < PageSize && shown < 8; i++) + for (int i = 0, shown = 0; i < Format.PageSize && shown < 8; i++) if (ace[at + i] != libred[at + i]) { differences.AppendLine( - $"page {page} (type 0x{ace[at]:X2} owner {BitConverter.ToInt32(ace, at + 4)}) " + + $"page {page} (type 0x{ace[at]:X2} owner {(int)LibRed.Pages.DataPage.ReadOwner(ace.AsSpan(at, Format.PageSize), Format)}) " + $"+0x{i:X3}: ace={ace[at + i]:X2} libred={libred[at + i]:X2}"); shown++; lines++; @@ -219,4 +219,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandTimeout = 0; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/DropViewAccessTests.cs b/test/LibRed.Core.AccessTests/DropViewAccessTests.cs index 2dea5105..479cf56b 100644 --- a/test/LibRed.Core.AccessTests/DropViewAccessTests.cs +++ b/test/LibRed.Core.AccessTests/DropViewAccessTests.cs @@ -29,8 +29,8 @@ public void Access_reads_a_libred_view_dropped_file() { Assert.True(db.DropQueryObject("Doomed")); Assert.False(db.DropQueryObject("Nope")); // missing → false - Assert.False(db.Catalog.Views.ContainsKey("Doomed")); // gone - Assert.True(db.Catalog.Views.ContainsKey("Keep")); // survivor intact + Assert.Null(db.Catalog.FindQuery("Doomed")); // gone + Assert.NotNull(db.Catalog.FindQuery("Keep")); // survivor intact } using var conn = Open(path); @@ -43,4 +43,4 @@ public void Access_reads_a_libred_view_dropped_file() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/DuplicateIndexKeyProbeTest.cs b/test/LibRed.Core.AccessTests/DuplicateIndexKeyProbeTest.cs index 28e49052..7847e8cc 100644 --- a/test/LibRed.Core.AccessTests/DuplicateIndexKeyProbeTest.cs +++ b/test/LibRed.Core.AccessTests/DuplicateIndexKeyProbeTest.cs @@ -7,12 +7,12 @@ namespace LibRed.Core.Tests; // Reading an index where many rows share one key — ordinary for any non-unique index, and once a real bug. // -// A full-BMP sweep died at U+4000 with "entry [7, 9) cannot contain its 4-byte trailer", IndexPageReader +// A full-BMP sweep died at U+4000 with "entry [7, 9) cannot contain its 4-byte trailer", IndexTree // refusing a page ACE had written. CJK is largely ignorable in General v0, so thousands of rows shared the // identical key, and once the index outgrew a single leaf the prefix compression became severe enough to // break the reader's assumptions. 100 rows read fine; 500 and above read NOTHING. // -// The cause was that the shared prefix covers the whole entry, trailer included — see IndexPageReader — so +// The cause was that the shared prefix covers the whole entry, trailer included — see IndexTree — so // the stored remainder can be two bytes. These cases now assert, since nothing about them is exotic. [Collection(AceCollection.Name)] public class DuplicateIndexKeyProbeTest(ITestOutputHelper output) @@ -97,7 +97,6 @@ public void Deleting_a_row_from_a_leaf_ace_compressed_into_the_row_pointer(int r key[keyIndex] = "same"; (RowId id, object?[] values) = table.SeekRowsWithIds(index, key) .First(r => Convert.ToInt32(r.Values[valueIndex]) == rows / 2); - table.RemoveIndexEntry(index, values, id); table.Delete(id); } @@ -143,21 +142,21 @@ public void Probe_the_node_page_layout() var table = db.OpenTable("Dup"); IndexDef index = table.Definition.Indexes.Single(i => i.Name == "IX_Dup"); var page = table.Channel.ReadPageShared(index.RootPage); + Formats.JetFormatBase format = db.Format; + (int prev, int next) = IndexTree.ReadSiblings(page.Span, format); output.WriteLine($"root page {index.RootPage}: type 0x{page.ReadByte(0):X2}, " + - $"owner {page.ReadInt32(0x04)}, prev {page.ReadInt32(0x0C)}, " + - $"next {page.ReadInt32(0x10)}, tail {page.ReadInt32(0x14)}, " + - $"compressed {page.ReadUInt16(0x18)}, byte 0x1A 0x{page.ReadByte(0x1A):X2}"); + $"owner {IndexTree.ReadOwner(page.Span, format)}, prev {prev}, " + + $"next {next}, tail {page.ReadInt32(format.IndexChildTailOffset)}, " + + $"compressed {IndexTree.ReadCompressedByteCount(page.Span, format)}, byte 0x1A 0x{page.ReadByte(format.IndexLevelOffset):X2}"); var ends = new List(); - for (int i = 0x1B; i < 0x1E0 && ends.Count < 24; i++) - { - byte mask = page.ReadByte(i); + ReadOnlySpan entryMask = page.Slice(format.IndexEntryMaskOffset, format.IndexEntryDataOffset - format.IndexEntryMaskOffset); + for (int i = 0; i < entryMask.Length && ends.Count < 24; i++) for (int bit = 0; bit < 8; bit++) - if ((mask & (1 << bit)) != 0) ends.Add((i - 0x1B) * 8 + bit); - } + if (BitmapBits.Get(entryMask, i * 8 + bit)) ends.Add(i * 8 + bit); output.WriteLine($"first entry ends: {string.Join(", ", ends)}"); - output.WriteLine($"entry data 0x1E0..+64: {Convert.ToHexString(page.Slice(0x1E0, 64))}"); + output.WriteLine($"entry data 0x1E0..+64: {Convert.ToHexString(page.Slice(format.IndexEntryDataOffset, 64))}"); } finally { TemporaryDatabase.Delete(path); } } @@ -168,4 +167,4 @@ private static void Exec(System.Data.OleDb.OleDbConnection connection, string sq command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/FixedFlagHonouredAccessTests.cs b/test/LibRed.Core.AccessTests/FixedFlagHonouredAccessTests.cs index b594f259..3674582d 100644 --- a/test/LibRed.Core.AccessTests/FixedFlagHonouredAccessTests.cs +++ b/test/LibRed.Core.AccessTests/FixedFlagHonouredAccessTests.cs @@ -68,7 +68,7 @@ public void Ace_reads_a_value_from_whichever_region_the_descriptor_names( }; string path = TemporaryDatabase.CreatePath("fixedflag-"); - LibRed.Storage.DatabaseCreator.CreateEmpty( + LibRed.JetDatabase.Create( path, version: type == JetDataType.Int64 ? (byte)0x05 : (byte)0x02); using (var database = JetDatabase.Open(path, readOnly: false)) { @@ -92,4 +92,4 @@ [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), Assert.Equal("before", reader.GetString(1)); Assert.Equal(42, reader.GetInt32(2)); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/FixedWidthOverflowAccessTests.cs b/test/LibRed.Core.AccessTests/FixedWidthOverflowAccessTests.cs index dde900c4..7db1664f 100644 --- a/test/LibRed.Core.AccessTests/FixedWidthOverflowAccessTests.cs +++ b/test/LibRed.Core.AccessTests/FixedWidthOverflowAccessTests.cs @@ -28,7 +28,7 @@ public void ACE_stores_char_and_binary_as_fixed_length_columns() { CreateProbeTable(path); using var db = JetDatabase.Open(path); - TableDef probe = db.OpenTable("Probe").Definition; + TableDefinition probe = db.OpenTable("Probe").Definition; Assert.True(probe.FindColumn("C")!.IsFixedLength); // CHAR(3) Assert.False(probe.FindColumn("V")!.IsFixedLength); // TEXT(3) @@ -83,10 +83,10 @@ public void LibRed_refuses_the_same_value_the_same_way(bool fixedLength) private static void CreateProbeTable(string path) { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var connection = AceTestDatabase.Open(path); using OleDbCommand create = connection.CreateCommand(); create.CommandText = "CREATE TABLE Probe (C CHAR(3), V TEXT(3), B BINARY(3), W VARBINARY(3))"; create.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/FormatVersionRaiseAccessTests.cs b/test/LibRed.Core.AccessTests/FormatVersionRaiseAccessTests.cs index ed0b5df5..70b79c81 100644 --- a/test/LibRed.Core.AccessTests/FormatVersionRaiseAccessTests.cs +++ b/test/LibRed.Core.AccessTests/FormatVersionRaiseAccessTests.cs @@ -24,7 +24,7 @@ public void Raising_a_jet_mdb_to_an_ACE_version_is_refused() string path = TemporaryDatabase.CreatePath("raise-mdb-", ".mdb"); try { - DatabaseCreator.CreateEmpty(path, version: 0x01); // a real Access 2000 .mdb + JetDatabase.Create(path, version: 0x01); // a real Access 2000 .mdb using (var channel = PageChannel.Open(path, readOnly: false)) { @@ -59,8 +59,9 @@ public void Raising_a_2010_format_accdb_rewrites_page_zero_as_ACE_does( string libPath = TemporaryDatabase.CreatePath("raise-lib-"); try { - DatabaseCreator.CreateEmpty(basePath, version: 0x03); - Assert.Equal(0x01, PageZero(basePath, JetFormatBase.MinorVersionOffset)); + JetDatabase.Create(basePath, version: 0x03); + int minorVersionOffset = JetFormatBase.FromVersionByte(0x03).MinorVersionOffset; + Assert.Equal(0x01, PageZero(basePath, minorVersionOffset)); File.Copy(basePath, acePath, overwrite: true); File.Copy(basePath, libPath, overwrite: true); @@ -72,8 +73,10 @@ public void Raising_a_2010_format_accdb_rewrites_page_zero_as_ACE_does( byte[] ace = PageZeroBytes(acePath), lib = PageZeroBytes(libPath); Assert.Equal(expectedVersion, ace[JetFormatBase.VersionOffset]); - Assert.Equal(0x00, ace[JetFormatBase.MinorVersionOffset]); - var differences = Enumerable.Range(0, CommitByteTableStart) + Assert.Equal(0x00, ace[minorVersionOffset]); + // The commit-byte table is left out: every write moves a slot in it. + int commitByteTable = JetFormatBase.FromVersionByte(expectedVersion).CommitByteTableOffset; + var differences = Enumerable.Range(0, commitByteTable) .Where(i => ace[i] != lib[i]) .Select(i => $"0x{i:X3} ace={ace[i]:X2} lib={lib[i]:X2}") .ToList(); @@ -97,9 +100,6 @@ public void Raising_a_2010_format_accdb_rewrites_page_zero_as_ACE_does( private static byte PageZero(string path, int offset) => PageZeroBytes(path)[offset]; - /// Page 0 starts its user commit-byte table here; every write moves a slot in it. - private const int CommitByteTableStart = 0xE00; - private static byte[] PageZeroBytes(string path) { using var channel = PageChannel.Open(path, readOnly: true); @@ -112,4 +112,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/GeneralV1CollationAccessTests.cs b/test/LibRed.Core.AccessTests/GeneralV1CollationAccessTests.cs index 1cc94417..ef731cdb 100644 --- a/test/LibRed.Core.AccessTests/GeneralV1CollationAccessTests.cs +++ b/test/LibRed.Core.AccessTests/GeneralV1CollationAccessTests.cs @@ -9,7 +9,7 @@ namespace LibRed.Core.Tests; // from ACE; this re-derives them from a live database, so a change in ACE (or a wrong assumption about which // weight table it uses) is caught rather than absorbed. // -// The v1 database is one LibRed creates itself (DatabaseCreator.CreateEmpty with Collation.General) — which +// The v1 database is one LibRed creates itself (JetDatabase.Create with Collation.General) — which // is also the point: nothing else here can make one. DAO always writes v0, ignoring the application setting, // and Access only honours "New database sort order" through its own UI. So this doubles as the test that // LibRed's create-with-collation produces a file ACE accepts as a General database. @@ -26,7 +26,7 @@ public class GeneralV1CollationAccessTests private static string CreateV1Database(string prefix) { string path = TemporaryDatabase.CreatePath(prefix); - DatabaseCreator.CreateEmpty(path, collation: Collation.General); + JetDatabase.Create(path, collation: Collation.General); return path; } @@ -72,7 +72,7 @@ public void Libred_reproduces_ace_index_keys_in_a_v1_database() Assert.Equal( (value, Convert.ToHexString(stored)), - (value, Convert.ToHexString(IndexKeyEncoder.Encode(index.Columns, aligned)))); + (value, Convert.ToHexString(IndexKeyCodec.Encode(index.Columns, aligned)))); checkedKeys++; } @@ -88,7 +88,7 @@ public void Ace_reads_an_index_libred_wrote_in_a_v1_database() { var column = new ColumnDef { Name = "K", Type = JetDataType.Text, Index = 0, Collation = Collation.General }; string[] unique = Samples - .GroupBy(v => Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [v]))) + .GroupBy(v => Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [v]))) .Select(g => g.First()) .ToArray(); @@ -162,4 +162,4 @@ private static void Exec(System.Data.OleDb.OleDbConnection connection, string sq command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/GlobalMapPointerAccessTests.cs b/test/LibRed.Core.AccessTests/GlobalMapPointerAccessTests.cs index 740e2c4a..6441624b 100644 --- a/test/LibRed.Core.AccessTests/GlobalMapPointerAccessTests.cs +++ b/test/LibRed.Core.AccessTests/GlobalMapPointerAccessTests.cs @@ -1,34 +1,40 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.Catalog; using LibRed.Formats; +using LibRed.IO; +using LibRed.Pages; using LibRed.Storage; using Xunit; namespace LibRed.Core.Tests; // LibRed allocates through page 0's global map pointers (0x18 free, 0x1C released), as ACE does. Two files ACE -// itself reads correctly — maps moved off page 1, and pages sitting in the released map — must stay files ACE -// reads correctly after LibRed has allocated into them. +// itself reads correctly — maps moved to another page, and pages sitting in the released map — must stay files ACE +// reads correctly after LibRed has allocated into them. Every test finds the maps through those pointers. [Collection(AceCollection.Name)] public class GlobalMapPointerAccessTests : TempDatabaseTest { - private const int NorthwindPages = 353; + private static readonly JetFormatBase Format = TestDatabases.FormatOf(TestDatabases.NorthwindAccdb); + private static readonly int NorthwindPages = (int)(new FileInfo(TestDatabases.NorthwindAccdb).Length / Format.PageSize); [Fact] - public void Ace_reads_a_table_libred_filled_through_maps_moved_off_page_one() + public void Ace_reads_a_table_libred_filled_through_maps_moved_to_another_page() { string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "globalptr-ace-moved-"); - byte[] page1 = ReadPage(path, 1); - byte[] moved = (byte[])page1.Clone(); - SetMapBit(moved, row: 0, page: NorthwindPages, set: false); - AppendPage(path, moved); - WritePointer(path, JetFormatBase.FreePagesMapPointerOffset, row: 0, page: NorthwindPages); - WritePointer(path, JetFormatBase.ReleasedPagesMapPointerOffset, row: 1, page: NorthwindPages); + (int Row, int Page) free, released; + using (var db = JetDatabase.Open(path)) + (free, released) = (db.DefinitionPage.FreePagesMap, db.DefinitionPage.ReleasedPagesMap); + Assert.Equal(free.Page, released.Page); // the copy moves both maps together + byte[] original = TestDatabases.ReadPage(path, free.Page); + byte[] moved = (byte[])original.Clone(); + TestDatabases.SetMapBit(moved, Format, free.Row, page: NorthwindPages, set: false); + TestDatabases.AppendPage(path, moved); + TestDatabases.WriteMapPointer(path, Format.FreePagesMapPointerOffset, free.Row, NorthwindPages); + TestDatabases.WriteMapPointer(path, Format.ReleasedPagesMapPointerOffset, released.Row, NorthwindPages); FillWithLibRed(path, rows: 1500); - Assert.Equal(page1, ReadPage(path, 1)); // LibRed never touched the stale maps + Assert.Equal(original, TestDatabases.ReadPage(path, free.Page)); // LibRed never touched the stale maps AssertAceReads(path, rows: 1500); } @@ -36,19 +42,22 @@ public void Ace_reads_a_table_libred_filled_through_maps_moved_off_page_one() public void Ace_reads_a_table_libred_filled_around_released_pages() { string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "globalptr-ace-released-"); - byte[] page1 = ReadPage(path, 1); + (int Row, int Page) free, released; + using (var db = JetDatabase.Open(path)) + (free, released) = (db.DefinitionPage.FreePagesMap, db.DefinitionPage.ReleasedPagesMap); + byte[] holder = TestDatabases.ReadPage(path, released.Page); foreach (int p in new[] { 310, 329, NorthwindPages, NorthwindPages + 1 }) - SetMapBit(page1, row: 1, page: p, set: true); - WritePage(path, 1, page1); + TestDatabases.SetMapBit(holder, Format, released.Row, page: p, set: true); + TestDatabases.WritePage(path, released.Page, holder); FillWithLibRed(path, rows: 1500); // Never allocated while open; the close merged them into the free map and cleared the released map. - byte[] after = ReadPage(path, 1); + byte[] freeAfter = TestDatabases.ReadPage(path, free.Page), releasedAfter = TestDatabases.ReadPage(path, released.Page); foreach (int p in new[] { 310, 329, NorthwindPages, NorthwindPages + 1 }) { - Assert.False(MapBit(after, 1, p), $"page {p} should no longer be released"); - Assert.True(MapBit(after, 0, p), $"page {p} should be free"); + Assert.False(TestDatabases.MapBit(releasedAfter, Format, released.Row, p), $"page {p} should no longer be released"); + Assert.True(TestDatabases.MapBit(freeAfter, Format, free.Row, p), $"page {p} should be free"); } AssertAceReads(path, rows: 1500); } @@ -80,11 +89,15 @@ public void Ace_grows_the_free_map_to_cover_the_file_it_frees_pages_in() } } - int pages = (int)(new FileInfo(path).Length / 4096); - byte[] holder = ReadPage(path, 1); - int offset = BinaryPrimitives.ReadUInt16LittleEndian(holder.AsSpan(14)) & 0x1FFF; - int length = 4096 - offset; // row 0 is the last record on the page, so its record runs to the page end - int start = BinaryPrimitives.ReadInt32LittleEndian(holder.AsSpan(offset + 1)); + int pages = (int)(new FileInfo(path).Length / Format.PageSize); + (int Row, int Page) free; + using (var db = JetDatabase.Open(path)) + free = db.DefinitionPage.FreePagesMap; + byte[] holder = TestDatabases.ReadPage(path, free.Page); + var holderPage = new DataPage(); + holderPage.Read(new PageBuffer(holder, free.Page), Format); + int offset = holderPage.Rows[free.Row].Offset, length = holderPage.Rows[free.Row].Length; + int start = UsageMap.StartPage(holder.AsSpan(offset), Format); // Then free pages above the window: shortening a memo releases its long-value pages at once. using (OleDbConnection connection = AceTestDatabase.Open(path)) @@ -94,17 +107,18 @@ public void Ace_grows_the_free_map_to_cover_the_file_it_frees_pages_in() update.ExecuteNonQuery(); } - byte[] after = ReadPage(path, 1); + byte[] after = TestDatabases.ReadPage(path, free.Page); + int window = Format.UsageMapInlineBitmapSize * 8; int freeAbove512 = 0; - for (int p = 512; p < pages; p++) if (MapBit(after, 0, p)) freeAbove512++; + for (int p = window; p < pages; p++) if (TestDatabases.MapBit(after, Format, free.Row, p)) freeAbove512++; // Measured: 700 memo rows made a 1,761-page file, whose free map is a 229-byte inline record starting at // page 0 — coverage 1,792 pages — and the UPDATE marked 43 pages free above 511. So ACE's map covers // every page it might have to free, which is why a page outside the map's coverage is a malformed file // rather than an ordinary state (PageAllocator.Free). - Assert.True(pages > 512, $"expected the file to pass the 512-page window; it is {pages} pages"); + Assert.True(pages > window, $"expected the file to pass the 512-page window; it is {pages} pages"); Assert.Equal(0, start); - Assert.True(length >= 5 + (pages + 7) / 8, + Assert.True(length >= Format.UsageMapInlineHeaderSize + BitmapBits.ByteCount(pages), $"free map record is {length} bytes, too short for {pages} pages"); Assert.True(freeAbove512 > 0, "expected the pages the UPDATE freed to be marked free above page 512"); } @@ -135,7 +149,10 @@ public void Ace_keeps_released_pages_off_disk_until_the_session_closes() } } - byte[] before = ReadPage(path, 1); + int holder; + using (var db = JetDatabase.Open(path)) + holder = db.DefinitionPage.FreePagesMap.Page; + byte[] before = TestDatabases.ReadPage(path, holder); byte[] duringDrop; using (OleDbConnection connection = AceTestDatabase.Open(path)) { @@ -144,9 +161,9 @@ public void Ace_keeps_released_pages_off_disk_until_the_session_closes() drop.CommandText = "DROP TABLE Doomed"; drop.ExecuteNonQuery(); } - duringDrop = ReadPage(path, 1); // the session is still open + duringDrop = TestDatabases.ReadPage(path, holder); // the session is still open } - byte[] after = ReadPage(path, 1); + byte[] after = TestDatabases.ReadPage(path, holder); Assert.Equal(before, duringDrop); // nothing on disk yet — the pages are the session's own business Assert.NotEqual(before, after); // and the close puts them in the free map @@ -174,56 +191,4 @@ private static void AssertAceReads(string path, int rows) Assert.Equal(rows, Convert.ToInt32(reader.GetValue(0))); Assert.Equal((long)rows * (rows - 1) / 2, Convert.ToInt64(reader.GetValue(1))); } - - private static void WritePointer(string path, int offset, int row, int page) - { - ReadOnlySpan mask = JetFormatBase.PageZeroHeaderMask; - byte[] value = BitConverter.GetBytes((uint)(page << 8 | row)); - for (int i = 0; i < 4; i++) value[i] ^= mask[offset - JetFormatBase.PageZeroHeaderMaskStart + i]; - using var s = new FileStream(path, FileMode.Open, FileAccess.ReadWrite); - s.Position = offset; - s.Write(value); - } - - private static byte[] ReadPage(string path, int page) - { - using var s = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - var bytes = new byte[4096]; - s.Position = page * 4096L; - s.ReadExactly(bytes); - return bytes; - } - - private static void WritePage(string path, int page, byte[] bytes) - { - using var s = new FileStream(path, FileMode.Open, FileAccess.ReadWrite); - s.Position = page * 4096L; - s.Write(bytes); - } - - private static void AppendPage(string path, byte[] bytes) - { - using var s = new FileStream(path, FileMode.Append, FileAccess.Write); - s.Write(bytes); - } - - private static (int Byte, int Bit) Locate(byte[] holder, int row, int page) - { - int offset = BinaryPrimitives.ReadUInt16LittleEndian(holder.AsSpan(14 + row * 2)) & 0x1FFF; - int bit = page - BinaryPrimitives.ReadInt32LittleEndian(holder.AsSpan(offset + 1)); - return (offset + 5 + bit / 8, bit % 8); - } - - private static void SetMapBit(byte[] holder, int row, int page, bool set) - { - (int b, int bit) = Locate(holder, row, page); - if (set) holder[b] |= (byte)(1 << bit); - else holder[b] &= (byte)~(1 << bit); - } - - private static bool MapBit(byte[] holder, int row, int page) - { - (int b, int bit) = Locate(holder, row, page); - return (holder[b] & (1 << bit)) != 0; - } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/GuidColumnStorageAccessTests.cs b/test/LibRed.Core.AccessTests/GuidColumnStorageAccessTests.cs index c311fa38..6504cec7 100644 --- a/test/LibRed.Core.AccessTests/GuidColumnStorageAccessTests.cs +++ b/test/LibRed.Core.AccessTests/GuidColumnStorageAccessTests.cs @@ -34,7 +34,7 @@ public void Ace_declares_every_guid_column_variable_length(int count) } using var database = JetDatabase.Open(path, readOnly: true); - TableDef table = database.Catalog.FindTable("W")!; + TableDefinition table = database.Catalog.FindTable("W")!; Assert.All(table.Columns.Where(c => c.Name.StartsWith('G')), column => { Assert.False(column.IsFixedLength); @@ -73,7 +73,7 @@ public void A_wide_guid_table_declares_but_its_rows_still_face_the_record_cap(in } // The exception, and the reason "GUID is variable" must not be applied everywhere: ACE's OWN system - // tables declare it fixed, which is what DatabaseCreator reproduces when LibRed synthesises a database. + // tables declare it fixed, which is what JetDatabase reproduces when LibRed synthesises a database. // Checked against real ACE-created files, not against anything LibRed wrote. [Theory] [InlineData("Ace16Types.accdb")] @@ -117,4 +117,4 @@ [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), Assert.Equal("before", reader.GetString(1)); Assert.Equal(42, reader.GetInt32(2)); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/GuidKeyEncodingTests.cs b/test/LibRed.Core.AccessTests/GuidKeyEncodingTests.cs index e30fa17a..16c9c12d 100644 --- a/test/LibRed.Core.AccessTests/GuidKeyEncodingTests.cs +++ b/test/LibRed.Core.AccessTests/GuidKeyEncodingTests.cs @@ -47,7 +47,7 @@ public void Encoded_guid_keys_match_access_byte_for_byte() var def = table.Definition; IndexDef pk = def.Indexes.Single(i => i.IsPrimaryKey); int kIdx = def.FindColumn("K")!.Index; - var decoder = new RowDecoder(def.Columns, db.Format); + var decoder = new RowCodec(def.Columns, db.Format); int checkd = 0; foreach (var (accessKey, rowId) in new IndexCursor(table.Channel, pk.RootPage).RawEntries()) @@ -56,13 +56,13 @@ public void Encoded_guid_keys_match_access_byte_for_byte() var values = new object?[def.Columns.Count]; values[kIdx] = g; - byte[] ours = IndexKeyEncoder.Encode(pk.Columns, values); + byte[] ours = IndexKeyCodec.Encode(pk.Columns, values); Assert.True(accessKey.AsSpan().SequenceEqual(ours), $"'{g}': access={Convert.ToHexString(accessKey)} ours={Convert.ToHexString(ours)}"); // The decoder is the inverse. - Assert.Equal(g, (Guid)IndexKeyDecoder.Decode(pk.Columns, accessKey)[0]!); + Assert.Equal(g, (Guid)IndexKeyCodec.Decode(pk.Columns, accessKey)[0]!); checkd++; } Assert.Equal(Guids.Length, checkd); @@ -100,7 +100,7 @@ public void Encoded_descending_guid_keys_match_access_byte_for_byte() IndexDef ixd = def.Indexes.First(i => !i.IsPrimaryKey); Assert.False(ixd.Columns[0].Ascending); // it really is descending int kIdx = def.FindColumn("K")!.Index; - var decoder = new RowDecoder(def.Columns, db.Format); + var decoder = new RowCodec(def.Columns, db.Format); int checkd = 0; foreach (var (accessKey, rowId) in new IndexCursor(table.Channel, ixd.RootPage).RawEntries()) @@ -109,11 +109,11 @@ public void Encoded_descending_guid_keys_match_access_byte_for_byte() var values = new object?[def.Columns.Count]; values[kIdx] = g; - byte[] ours = IndexKeyEncoder.Encode(ixd.Columns, values); + byte[] ours = IndexKeyCodec.Encode(ixd.Columns, values); Assert.True(accessKey.AsSpan().SequenceEqual(ours), $"'{g}': access={Convert.ToHexString(accessKey)} ours={Convert.ToHexString(ours)}"); - Assert.Equal(g, (Guid)IndexKeyDecoder.Decode(ixd.Columns, accessKey)[0]!); + Assert.Equal(g, (Guid)IndexKeyCodec.Decode(ixd.Columns, accessKey)[0]!); checkd++; } Assert.Equal(Guids.Length, checkd); @@ -155,4 +155,4 @@ public void Access_reads_a_libred_created_guid_key_table() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/IndexColumnCountAccessTests.cs b/test/LibRed.Core.AccessTests/IndexColumnCountAccessTests.cs index b6673bd5..aa4edad5 100644 --- a/test/LibRed.Core.AccessTests/IndexColumnCountAccessTests.cs +++ b/test/LibRed.Core.AccessTests/IndexColumnCountAccessTests.cs @@ -6,10 +6,10 @@ namespace LibRed.Core.Tests; // An index spans at most 10 columns, and the incremental CREATE INDEX path has to say so. // -// The index-data block carries exactly IndexBlockFormat.MaxColumns slots, with no count field and no -// continuation, so an eleventh cannot be represented. TdefBuilder rejects that when a table is created with -// its indexes; TableCreator.InsertIndex is the other way in - CREATE INDEX and ADD FOREIGN KEY on an -// existing table - and used to check nothing. BuildIndexDataBlock filled ten slots and marked the rest +// The index-data block carries exactly JetFormatBase.IndexDataMaxColumns slots, with no count field and no +// continuation, so an eleventh cannot be represented. TableDefinition rejects that when a table is created with +// its indexes; SchemaEditor.InsertIndex is the other way in - CREATE INDEX and ADD FOREIGN KEY on an +// existing table - and used to check nothing. The data-block builder filled ten slots and marked the rest // unused, so LibRed accepted an 11-column index and stored a 10-column one. // // That is a nastier failure than the other limit overruns found alongside it. Exceeding the 32-index cap @@ -79,4 +79,4 @@ public void LibRed_matches_that_on_the_incremental_path(int indexed, bool accept Assert.Equal(indexed, database.Catalog.FindTable("IdxProbe")!.Indexes .Single(i => i.Name == "IxWide").Columns.Count); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/IndexKeyTruncationAccessTests.cs b/test/LibRed.Core.AccessTests/IndexKeyTruncationAccessTests.cs index 9592f48a..0806f5a3 100644 --- a/test/LibRed.Core.AccessTests/IndexKeyTruncationAccessTests.cs +++ b/test/LibRed.Core.AccessTests/IndexKeyTruncationAccessTests.cs @@ -83,7 +83,7 @@ public void A_key_whose_dropped_bytes_hold_a_word_sort_record_matches_ACE(char m private static string Build(string prefix, string columns, string indexColumns, Func row) { string path = TemporaryDatabase.CreatePath(prefix); - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var connection = AceTestDatabase.Open(path); Exec(connection, $"CREATE TABLE Probe ({columns})"); Exec(connection, $"CREATE INDEX IX ON Probe {indexColumns}"); @@ -116,7 +116,7 @@ private int CompareKeys(string path, string[] keyColumns) if (!rows.TryGetValue(rowId, out object?[]? values)) continue; compared++; string ace = Convert.ToHexString(stored); - string ours = Convert.ToHexString(IndexKeyEncoder.Encode(columns, values)); + string ours = Convert.ToHexString(IndexKeyCodec.Encode(columns, values)); if (ace == ours) continue; mismatched++; output.WriteLine($"ACE {ace}"); @@ -132,4 +132,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/IndexMaintenanceAccessTests.cs b/test/LibRed.Core.AccessTests/IndexMaintenanceAccessTests.cs index 4c9b519b..60da1b41 100644 --- a/test/LibRed.Core.AccessTests/IndexMaintenanceAccessTests.cs +++ b/test/LibRed.Core.AccessTests/IndexMaintenanceAccessTests.cs @@ -100,15 +100,11 @@ private static void Move(Table table, IndexDef pk, IndexDef ixK, int id, int new newValues[keyIndex] = newKey; newValues[textIndex] = text; table.Update(rowId, newValues); - table.MoveIndexEntry(pk, oldValues, newValues, rowId); - table.MoveIndexEntry(ixK, oldValues, newValues, rowId); } private static void Delete(Table table, IndexDef pk, IndexDef ixK, int id) { (RowId rowId, object?[] values) = table.SeekRowsWithIds(pk, [id]).Single(); - table.RemoveIndexEntry(pk, values, rowId); - table.RemoveIndexEntry(ixK, values, rowId); table.Delete(rowId); } @@ -118,7 +114,7 @@ private static void AssertStoredKeysMatchRows(Table table, IndexDef index) foreach ((byte[] stored, RowId rowId) in new IndexCursor(table.Channel, index.RootPage).RawEntries()) { object?[] row = Assert.IsType(table.GetRow(rowId)); - Assert.Equal(stored, IndexKeyEncoder.Encode(index.Columns, row)); + Assert.Equal(stored, IndexKeyCodec.Encode(index.Columns, row)); entries++; } Assert.Equal(RowCount - 1, entries); @@ -140,4 +136,4 @@ private static void Execute(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/IndexOrderingAccessTests.cs b/test/LibRed.Core.AccessTests/IndexOrderingAccessTests.cs index 6a6a86ee..15df99d3 100644 --- a/test/LibRed.Core.AccessTests/IndexOrderingAccessTests.cs +++ b/test/LibRed.Core.AccessTests/IndexOrderingAccessTests.cs @@ -57,11 +57,11 @@ public void Empty_binary_key_matches_ace_start_marker_only(bool ascending, strin (byte[] stored, RowId rowId) = Assert.Single(new IndexCursor(table.Channel, index.RootPage).RawEntries()); Assert.Equal(expectedHex, Convert.ToHexString(stored)); - object?[] row = new RowDecoder(table.Definition.Columns, db.Format) + object?[] row = new RowCodec(table.Definition.Columns, db.Format) .Decode(db.ReadDataPage(rowId.Page).GetRow(rowId.Row)); var aligned = new object?[table.Definition.Columns.Count]; aligned[table.Definition.FindColumn("K")!.Index] = row[table.Definition.FindColumn("K")!.Index]; - Assert.Equal(stored, IndexKeyEncoder.Encode(index.Columns, aligned)); + Assert.Equal(stored, IndexKeyCodec.Encode(index.Columns, aligned)); } finally { TemporaryDatabase.Delete(path); } } @@ -168,7 +168,7 @@ private static void AssertFamily(string storeType, OleDbType parameterType, obje IndexDef index = table.Definition.Indexes.Single(i => i.Name == "IX_BoundaryKeys"); int keyIndex = table.Definition.FindColumn("K")!.Index; int valueIndex = table.Definition.FindColumn("V")!.Index; - var decoder = new RowDecoder(table.Definition.Columns, db.Format); + var decoder = new RowCodec(table.Definition.Columns, db.Format); var libredOrder = new List(); foreach ((byte[] storedKey, RowId rowId) in new IndexCursor(table.Channel, index.RootPage).RawEntries()) @@ -176,7 +176,7 @@ private static void AssertFamily(string storeType, OleDbType parameterType, obje object?[] row = decoder.Decode(db.ReadDataPage(rowId.Page).GetRow(rowId.Row)); var aligned = new object?[table.Definition.Columns.Count]; aligned[keyIndex] = row[keyIndex]; - byte[] encoded = IndexKeyEncoder.Encode(index.Columns, aligned); + byte[] encoded = IndexKeyCodec.Encode(index.Columns, aligned); Assert.True(storedKey.AsSpan().SequenceEqual(encoded), $"V={row[valueIndex]}, K={Describe(row[keyIndex])}: ACE={Convert.ToHexString(storedKey)} LibRed={Convert.ToHexString(encoded)}"); libredOrder.Add(Convert.ToInt32(row[valueIndex])); @@ -201,4 +201,4 @@ private static void Execute(OleDbConnection connection, string sql) byte[] bytes => Convert.ToHexString(bytes), _ => value.ToString() ?? "NULL", }; -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/IndexSplitAccessTests.cs b/test/LibRed.Core.AccessTests/IndexSplitAccessTests.cs index c01007fd..b580fedb 100644 --- a/test/LibRed.Core.AccessTests/IndexSplitAccessTests.cs +++ b/test/LibRed.Core.AccessTests/IndexSplitAccessTests.cs @@ -1,6 +1,7 @@ using System.Data.OleDb; using LibRed; using LibRed.Catalog; +using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -145,8 +146,6 @@ private static void LibRedEmptyLowLeaf(string path) foreach ((RowId id, object?[] values) in table.Rows().WithIds().ToList()) { if (Convert.ToInt32(values[0]) >= 700) continue; - foreach (IndexDef index in table.Definition.RealIndexes) - table.RemoveIndexEntry(index, values, id); table.Delete(id); } } @@ -157,7 +156,7 @@ private static void LibRedEmptyLowLeaf(string path) private static void AssertChainIsIntact(string path, string table) { using var channel = LibRed.IO.PageChannel.Open(path, readOnly: true); - TableDef definition = new LibRed.Catalog.JetCatalog(channel).FindTable(table)!; + TableDefinition definition = new LibRed.Catalog.JetCatalog(channel).FindTable(table)!; IndexDef def = definition.Indexes.First(i => i.IsPrimaryKey); var fromRoot = new HashSet(); @@ -167,17 +166,17 @@ private static void AssertChainIsIntact(string path, string table) { int number = pending.Pop(); LibRed.IO.PageBuffer buffer = channel.ReadPage(number); - if (buffer.ReadByte(0) == 0x04) { fromRoot.Add(number); continue; } - int tail = buffer.ReadInt32(0x14); + if (PageHeader.ReadType(buffer.Span) == PageType.LeafIndexPage) { fromRoot.Add(number); continue; } + int tail = buffer.ReadInt32(channel.Format.IndexChildTailOffset); if (tail > 0) pending.Push(tail); - foreach ((byte[] _, int child) in LibRed.Storage.IndexPageReader.DecodeEntries( - LibRed.Storage.IndexPageReader.Read(channel, number, null))) + foreach ((byte[] _, int child) in LibRed.Storage.IndexTree.DecodeEntries( + LibRed.Storage.IndexTree.Read(channel, number, null))) pending.Push(child); } - int leftmost = fromRoot.Single(p => channel.ReadPage(p).ReadInt32(0x0C) == 0); + int leftmost = fromRoot.Single(p => LibRed.Storage.IndexTree.ReadSiblings(channel.ReadPage(p).Span, channel.Format).Previous == 0); var chain = new List(); - for (int at = leftmost; at != 0; at = channel.ReadPage(at).ReadInt32(0x10)) chain.Add(at); + for (int at = leftmost; at != 0; at = LibRed.Storage.IndexTree.ReadSiblings(channel.ReadPage(at).Span, channel.Format).Next) chain.Add(at); Assert.Equal(fromRoot.OrderBy(p => p), chain.OrderBy(p => p)); } @@ -194,7 +193,7 @@ private static void Exec(OleDbConnection connection, string sql) private static List Describe(string path, string table) { using var channel = LibRed.IO.PageChannel.Open(path, readOnly: true); - TableDef definition = new LibRed.Catalog.JetCatalog(channel).FindTable(table)!; + TableDefinition definition = new LibRed.Catalog.JetCatalog(channel).FindTable(table)!; IndexDef def = definition.Indexes.First(i => i.IsPrimaryKey); var lines = new List(); @@ -207,15 +206,17 @@ private static List Describe(string path, string table) if (!seen.Add(number)) continue; LibRed.IO.PageBuffer buffer = channel.ReadPage(number); byte type = buffer.ReadByte(0); - int prev = buffer.ReadInt32(0x0C), next = buffer.ReadInt32(0x10), tail = buffer.ReadInt32(0x14); + Formats.JetFormatBase format = channel.Format; + (int prev, int next) = LibRed.Storage.IndexTree.ReadSiblings(buffer.Span, format); + int tail = buffer.ReadInt32(format.IndexChildTailOffset); int entries = 0; - for (int at = 0x1B; at < 0x1E0; at++) entries += System.Numerics.BitOperations.PopCount(buffer.ReadByte(at)); + for (int at = format.IndexEntryMaskOffset; at < format.IndexEntryDataOffset; at++) entries += System.Numerics.BitOperations.PopCount(buffer.ReadByte(at)); lines.Add($"page {number} type=0x{type:X2} entries={entries} prev={prev} next={next} tail={tail}"); - if (type != 0x03) continue; // 0x03 = node, 0x04 = leaf + if (PageHeader.ReadType(buffer.Span) != PageType.IntermediateIndexPage) continue; // 0x03 = node, 0x04 = leaf if (tail > 0) pending.Push(tail); - foreach ((byte[] _, int child) in LibRed.Storage.IndexPageReader.DecodeEntries( - LibRed.Storage.IndexPageReader.Read(channel, number, null))) + foreach ((byte[] _, int child) in LibRed.Storage.IndexTree.DecodeEntries( + LibRed.Storage.IndexTree.Read(channel, number, null))) pending.Push(child); } lines.Sort(StringComparer.Ordinal); @@ -271,4 +272,4 @@ [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true), } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/IndexUsageMapTests.cs b/test/LibRed.Core.AccessTests/IndexUsageMapTests.cs index ad8e91b1..2b7b59b8 100644 --- a/test/LibRed.Core.AccessTests/IndexUsageMapTests.cs +++ b/test/LibRed.Core.AccessTests/IndexUsageMapTests.cs @@ -1,4 +1,3 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed; using LibRed.Catalog; @@ -28,48 +27,18 @@ private static SortedSet IndexPagesOwnedByTable(string path, Table table, J for (int p = 1; p < pageCount; p++) { ReadOnlySpan span = table.Channel.ReadPage(p).Span; - if (span[0] is not (0x03 or 0x04)) continue; - if (BinaryPrimitives.ReadInt32LittleEndian(span.Slice(0x04, 4)) == table.Definition.DefinitionPage) + if (PageHeader.ReadType(span) is not (PageType.IntermediateIndexPage or PageType.LeafIndexPage)) continue; + if (IndexTree.ReadOwner(span, format) == table.Definition.DefinitionPage) pages.Add(p); } return pages; } - /// The union of every index's own usage map, walking the TDEF to each 52-byte index block. - private static SortedSet UnionOfIndexMaps(Table table, JetFormatBase format) + /// The union of every index's own usage map. + private static SortedSet UnionOfIndexMaps(Table table) { - var tdef = table.Channel.ReadPage(table.Definition.DefinitionPage); - int dataCount = tdef.ReadInt32(format.TdefIndexCountOffset); - int colCount = tdef.ReadUInt16(format.TdefColumnCountOffset); - - int pos = format.TdefRealIndexBlockOffset + dataCount * format.RealIndexEntrySize - + colCount * format.ColumnDescriptorSize; - for (int i = 0; i < colCount; i++) pos += 2 + tdef.ReadUInt16(pos); - - var union = new SortedSet(); - for (int i = 0; i < dataCount; i++) - { - int block = pos + i * 52; - int mapRow = tdef.ReadByte(block + 0x22); - int mapPage = tdef.ReadInt24(block + 0x23); - var holder = new DataPage(); - holder.Read(table.Channel.ReadPage(mapPage), format); - union.UnionWith(BitsOf(holder.GetRow(mapRow))); - } - return union; - } - - private static IEnumerable BitsOf(ReadOnlySpan record) - { - // Only inline maps arise here (an index B-tree well under 2 GB); a reference map would need expanding. - if (record[0] != 0x00) throw new InvalidOperationException($"Unexpected usage-map type 0x{record[0]:X2}."); - int start = BinaryPrimitives.ReadInt32LittleEndian(record.Slice(1, 4)); - var pages = new List(); - for (int k = 5; k < record.Length; k++) - for (int b = 0; b < 8; b++) - if ((record[k] & (1 << b)) != 0) - pages.Add(start + (k - 5) * 8 + b); - return pages; + var maps = new UsageMap(table.Channel, table.Definition); + return [.. table.Definition.Indexes.SelectMany(i => maps.PagesInMap(i.UsageMap.Row, i.UsageMap.Page))]; } [Fact] @@ -94,7 +63,7 @@ public void An_index_usage_map_covers_every_btree_page_after_splits() } SortedSet actual = IndexPagesOwnedByTable(path, table, db.Format); - SortedSet mapped = UnionOfIndexMaps(table, db.Format); + SortedSet mapped = UnionOfIndexMaps(table); Assert.True(actual.Count > 2, "expected the B-trees to have split beyond their creation roots"); Assert.Equal(actual, mapped); // no page missing, none spurious @@ -123,7 +92,7 @@ public void Index_map_coverage_matches_what_access_itself_marks() var table = db.OpenTable("T"); // Access's own map must cover exactly the index pages it wrote — the invariant LibRed reproduces. - Assert.Equal(IndexPagesOwnedByTable(path, table, db.Format), UnionOfIndexMaps(table, db.Format)); + Assert.Equal(IndexPagesOwnedByTable(path, table, db.Format), UnionOfIndexMaps(table)); } finally { TemporaryDatabase.Delete(path); } } @@ -154,7 +123,7 @@ public void Dropping_a_multi_page_index_releases_what_ace_releases() // The index has to span more than its root, or the test measures nothing. using (var built = JetDatabase.Open(start)) - Assert.True(UnionOfIndexMaps(built.OpenTable("T"), built.Format).Count > 3, + Assert.True(UnionOfIndexMaps(built.OpenTable("T")).Count > 3, "expected the indexes to have split beyond their creation roots"); string ace = TemporaryDatabase.CopyPath(start, "idxmap-dropix-ace-"); @@ -199,7 +168,7 @@ public void Dropping_a_relationship_releases_its_indexs_tree_as_ace_does() } using (var built = JetDatabase.Open(start)) - Assert.True(UnionOfIndexMaps(built.OpenTable("C"), built.Format).Count > 3, + Assert.True(UnionOfIndexMaps(built.OpenTable("C")).Count > 3, "expected the indexes to have split beyond their creation roots"); string ace = TemporaryDatabase.CopyPath(start, "idxmap-dropfk-ace-"); @@ -292,4 +261,4 @@ public void A_new_index_after_a_drop_is_written_as_ace_writes_it( output.WriteLine(difference); Assert.Equal("", difference); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/JetPasswordSidAccessTests.cs b/test/LibRed.Core.AccessTests/JetPasswordSidAccessTests.cs index b703f895..a1d40c17 100644 --- a/test/LibRed.Core.AccessTests/JetPasswordSidAccessTests.cs +++ b/test/LibRed.Core.AccessTests/JetPasswordSidAccessTests.cs @@ -41,7 +41,7 @@ public void A_jet_password_remasks_a_libred_databases_sids_as_access_does() string source = TemporaryDatabase.CreatePath("jetpw_libred_", ".mdb"); try { - DatabaseCreator.CreateEmpty(source, version: 0x01); + JetDatabase.Create(source, version: (byte)Formats.JetVersion.Version4); Compare(engine!, source); } finally { TemporaryDatabase.Delete(source); } @@ -64,7 +64,9 @@ private static void Compare(object engine, string source) List access = StoredSids(byAccess), ours = StoredSids(byLibRed); Assert.NotEqual(StoredSids(source, null)[0], access[0]); // Access did re-mask them Assert.Equal(access, ours); - Assert.Equal(File.ReadAllBytes(byAccess)[0x42..0x6A], File.ReadAllBytes(byLibRed)[0x42..0x6A]); + Formats.JetFormatBase format = TestDatabases.FormatOf(byAccess); + Range password = format.PasswordOffset..(format.PasswordOffset + format.PasswordSize); + Assert.Equal(File.ReadAllBytes(byAccess)[password], File.ReadAllBytes(byLibRed)[password]); } finally { @@ -90,4 +92,4 @@ private static List StoredSids(string path, string? password = Password) private static object? Invoke(object target, string member, params object?[] args) => target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/KeyChecksumProbeTest.cs b/test/LibRed.Core.AccessTests/KeyChecksumProbeTest.cs index fdc90e4d..8685e5e8 100644 --- a/test/LibRed.Core.AccessTests/KeyChecksumProbeTest.cs +++ b/test/LibRed.Core.AccessTests/KeyChecksumProbeTest.cs @@ -1,5 +1,6 @@ using LibRed; using LibRed.Catalog; +using LibRed.Formats; using LibRed.Storage; using Xunit; @@ -40,7 +41,7 @@ public void Probe_truncated_key_structure() { if (!ace.TryGetValue(text, out string? stored)) { output.WriteLine($" {Label(text)}: not stored"); continue; } byte[] aceKey = Convert.FromHexString(stored); - byte[] full = IndexKeyEncoder.EncodeWithoutLengthLimit([(column, true)], [text]); + byte[] full = IndexKeyCodec.EncodeWithoutLengthLimit([(column, true)], [text]); int shared = 0; while (shared < aceKey.Length && shared < full.Length && aceKey[shared] == full[shared]) shared++; @@ -83,8 +84,8 @@ public void Probe_identify_the_checksum() { if (!ace.TryGetValue(text, out string? stored)) continue; byte[] aceKey = Convert.FromHexString(stored); - if (aceKey.Length != 510) continue; // not truncated: no checksum to learn from - dataset.Add((IndexKeyEncoder.EncodeWithoutLengthLimit([(column, true)], [text]), aceKey)); + if (aceKey.Length != IndexKeyCodec.MaxKeyBytes) continue; // not truncated: no checksum to learn from + dataset.Add((IndexKeyCodec.EncodeWithoutLengthLimit([(column, true)], [text]), aceKey)); } } finally { if (created is not null) TemporaryDatabase.Delete(created); } @@ -94,18 +95,18 @@ public void Probe_identify_the_checksum() Assert.NotEmpty(dataset); // Distinct checksums confirm the samples actually exercise the function rather than repeating one value. - int distinct = dataset.Select(d => Convert.ToHexString(d.Ace[508..])).Distinct().Count(); + int distinct = dataset.Select(d => Convert.ToHexString(d.Ace[IndexKeyCodec.KeptKeyBytes..])).Distinct().Count(); output.WriteLine($"{distinct} distinct checksums among them"); (string Name, Func Slice)[] inputs = [ - ("discarded", (full, _) => full[508..]), - ("discarded less terminator", (full, _) => full[508..^1]), + ("discarded", (full, _) => full[IndexKeyCodec.KeptKeyBytes..]), + ("discarded less terminator", (full, _) => full[IndexKeyCodec.KeptKeyBytes..^1]), ("whole key", (full, _) => full), ("whole key less start flag", (full, _) => full[1..]), - ("kept prefix", (full, _) => full[..508]), - ("kept prefix less start flag", (full, _) => full[1..508]), - ("discarded reversed", (full, _) => full[508..].Reverse().ToArray()), + ("kept prefix", (full, _) => full[..IndexKeyCodec.KeptKeyBytes]), + ("kept prefix less start flag", (full, _) => full[1..IndexKeyCodec.KeptKeyBytes]), + ("discarded reversed", (full, _) => full[IndexKeyCodec.KeptKeyBytes..].Reverse().ToArray()), ]; var hits = new List(); @@ -116,8 +117,8 @@ public void Probe_identify_the_checksum() bool all = dataset.All(d => { ushort expected = bigEndian - ? (ushort)((d.Ace[508] << 8) | d.Ace[509]) - : (ushort)((d.Ace[509] << 8) | d.Ace[508]); + ? (ushort)((d.Ace[IndexKeyCodec.KeptKeyBytes] << 8) | d.Ace[IndexKeyCodec.KeptKeyBytes + 1]) + : (ushort)((d.Ace[IndexKeyCodec.KeptKeyBytes + 1] << 8) | d.Ace[IndexKeyCodec.KeptKeyBytes]); return fn(slice(d.Full, d)) == expected; }); if (all) hits.Add($"{fnName} over {inputName} ({(bigEndian ? "big" : "little")}-endian)"); @@ -128,8 +129,8 @@ public void Probe_identify_the_checksum() // Whatever the answer is, record the raw pairs so the next attempt need not re-measure them. foreach ((byte[] full, byte[] aceKey) in dataset.Take(12)) - output.WriteLine($" cut={Convert.ToHexString(aceKey[508..])} " + - $"discarded[{full.Length - 508}]={Convert.ToHexString(full[508..])[..Math.Min(48, (full.Length - 508) * 2)]}"); + output.WriteLine($" cut={Convert.ToHexString(aceKey[IndexKeyCodec.KeptKeyBytes..])} " + + $"discarded[{full.Length - IndexKeyCodec.KeptKeyBytes}]={Convert.ToHexString(full[IndexKeyCodec.KeptKeyBytes..])[..Math.Min(48, (full.Length - IndexKeyCodec.KeptKeyBytes) * 2)]}"); } /// @@ -542,7 +543,7 @@ public void Probe_inline_position_overflow() { int at = text.IndexOf('-'); if (!ace.TryGetValue(text, out string? stored)) { output.WriteLine($" hyphen@{at,3}: not stored"); continue; } - string ours = Convert.ToHexString(IndexKeyEncoder.EncodeWithoutLengthLimit([(column, true)], [text])); + string ours = Convert.ToHexString(IndexKeyCodec.EncodeWithoutLengthLimit([(column, true)], [text])); output.WriteLine( $" hyphen@{at,3} (0x07+4x{at} = 0x{0x07 + 4 * at:X3}): ACE …{stored[^12..]} " + (ours == stored ? "ours SAME" : $"ours …{ours[^12..]}")); @@ -586,7 +587,7 @@ public void Probe_secondary_slot_of_a_han_character() { if (!ace.TryGetValue(text, out string? stored)) { output.WriteLine($" {Describe(text),-20} not stored"); continue; } string ours; - try { ours = Convert.ToHexString(IndexKeyEncoder.EncodeWithoutLengthLimit([(column, true)], [text])); } + try { ours = Convert.ToHexString(IndexKeyCodec.EncodeWithoutLengthLimit([(column, true)], [text])); } catch (NotSupportedException e) { ours = $"(refused: {e.Message[..Math.Min(30, e.Message.Length)]})"; } output.WriteLine($" {Describe(text),-20} ACE {stored,-34} {(ours == stored ? "SAME" : $"ours {ours}")}"); } @@ -708,9 +709,10 @@ private static ushort ApplyLinear(ushort[] table, ushort value) { if (!ace.TryGetValue(text, out string? stored)) continue; byte[] aceKey = Convert.FromHexString(stored); - if (aceKey.Length != 510) continue; - byte[] full = IndexKeyEncoder.EncodeWithoutLengthLimit([(column, true)], [text]); - yield return (full[508..], (ushort)((aceKey[508] << 8) | aceKey[509])); + if (aceKey.Length != IndexKeyCodec.MaxKeyBytes) continue; + byte[] full = IndexKeyCodec.EncodeWithoutLengthLimit([(column, true)], [text]); + yield return (full[IndexKeyCodec.KeptKeyBytes..], + (ushort)((aceKey[IndexKeyCodec.KeptKeyBytes] << 8) | aceKey[IndexKeyCodec.KeptKeyBytes + 1])); } } finally { if (created is not null) TemporaryDatabase.Delete(created); } @@ -906,7 +908,7 @@ private static (string Source, string? Created, ColumnDef Column) Fixture(byte v if (v1) { created = TemporaryDatabase.CreatePath("general-v1-chk-"); - DatabaseCreator.CreateEmpty(created, collation: Collation.General); + JetDatabase.Create(created, collation: Collation.General); } return (created ?? TestDatabases.NorthwindAccdb, created, new ColumnDef @@ -956,4 +958,4 @@ private static void Exec(System.Data.OleDb.OleDbConnection connection, string sq command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/Latin1SymbolCollationAccessTests.cs b/test/LibRed.Core.AccessTests/Latin1SymbolCollationAccessTests.cs index f3ab852d..05fc3862 100644 --- a/test/LibRed.Core.AccessTests/Latin1SymbolCollationAccessTests.cs +++ b/test/LibRed.Core.AccessTests/Latin1SymbolCollationAccessTests.cs @@ -56,7 +56,7 @@ public void Libred_reproduces_ace_index_keys_for_every_latin1_symbol() Assert.Equal( (value, Convert.ToHexString(stored)), - (value, Convert.ToHexString(IndexKeyEncoder.Encode(index.Columns, aligned)))); + (value, Convert.ToHexString(IndexKeyCodec.Encode(index.Columns, aligned)))); checkedKeys++; } @@ -78,8 +78,8 @@ public void A_superscript_digit_encodes_identically_to_its_base_digit(char super Name = "t", Type = JetDataType.Text, Index = 0, Collation = Collation.GeneralLegacy, }; Assert.Equal( - Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [digit.ToString()])), - Convert.ToHexString(IndexKeyEncoder.Encode([(column, true)], [superscript.ToString()]))); + Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [digit.ToString()])), + Convert.ToHexString(IndexKeyCodec.Encode([(column, true)], [superscript.ToString()]))); } private static void Exec(System.Data.OleDb.OleDbConnection connection, string sql) @@ -88,4 +88,4 @@ private static void Exec(System.Data.OleDb.OleDbConnection connection, string sq command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.AccessTests/LibRed.Core.AccessTests.csproj b/test/LibRed.Core.AccessTests/LibRed.Core.AccessTests.csproj index a6a357b7..dfc92f7c 100644 --- a/test/LibRed.Core.AccessTests/LibRed.Core.AccessTests.csproj +++ b/test/LibRed.Core.AccessTests/LibRed.Core.AccessTests.csproj @@ -41,8 +41,7 @@ - - + - - + diff --git a/test/LibRed.Core.Tests/LongValueCorruptionTests.cs b/test/LibRed.Core.Tests/LongValueCorruptionTests.cs index 13416a06..e6ce8adb 100644 --- a/test/LibRed.Core.Tests/LongValueCorruptionTests.cs +++ b/test/LibRed.Core.Tests/LongValueCorruptionTests.cs @@ -1,5 +1,7 @@ using System.Buffers.Binary; using LibRed; +using LibRed.Formats; +using LibRed.IO; using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -12,16 +14,15 @@ public class LongValueCorruptionTests public void Rejects_a_short_descriptor() { using var fixture = new Fixture(); - Assert.Throws(() => fixture.Reader.Resolve(new byte[11])); + Assert.Throws(() => + fixture.Reader.Resolve(new byte[fixture.Table.Channel.Format.LongValueDescriptorSize - 1])); } [Fact] public void Rejects_a_truncated_inline_value() { using var fixture = new Fixture(); - var descriptor = new byte[12]; - descriptor[0] = 1; - descriptor[3] = 0x80; + byte[] descriptor = LongValueStore.Descriptor(fixture.Table.Channel.Format, 1, LongValueStore.StorageKind.Inline); Assert.Throws(() => fixture.Reader.Resolve(descriptor)); } @@ -34,14 +35,15 @@ public void Rejects_a_truncated_inline_value() public void Rejects_a_malformed_chained_value(string corruption) { using var fixture = new Fixture(); - LongValueResult value = fixture.Writer.Write(new byte[5000]); + (byte[] Descriptor, IReadOnlyList OwnedPages, int FreePage) value = fixture.Writer.Write(new byte[5000]); byte[] descriptor = value.Descriptor.ToArray(); int first = value.OwnedPages[0]; switch (corruption) { case "outside-file": - WritePagePointer(descriptor.AsSpan(4, 4), fixture.Table.Channel.PageCount + 1); + PageBuffer.WriteRecordPointer(descriptor, fixture.Table.Channel.Format.LongValueDescriptorPointerOffset, + 0, fixture.Table.Channel.PageCount + 1); break; case "wrong-owner": byte[] wrongOwner = fixture.Table.Channel.ReadPage(first).Span.ToArray(); @@ -52,9 +54,8 @@ public void Rejects_a_malformed_chained_value(string corruption) break; case "short-chunk": byte[] shortChunk = fixture.Table.Channel.ReadPage(first).Span.ToArray(); - BinaryPrimitives.WriteUInt16LittleEndian( - shortChunk.AsSpan(fixture.Table.Channel.Format.DataRowDirectoryOffset, 2), - (ushort)(fixture.Table.Channel.PageSize - 3)); + DataPage.WriteSlot(shortChunk, fixture.Table.Channel.Format, 0, + fixture.Table.Channel.PageSize - (PageBuffer.RecordPointerSize - 1), RowSlotFlags.None); fixture.Table.Channel.WritePage(first, shortChunk); break; case "early-end": @@ -72,13 +73,13 @@ public void Rejects_a_malformed_chained_value(string corruption) public void Valid_inline_single_and_chained_values_round_trip() { using var fixture = new Fixture(); - byte[] inline = [3, 0, 0, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3]; + byte[] inline = [.. LongValueStore.Descriptor(fixture.Table.Channel.Format, 3, LongValueStore.StorageKind.Inline), 1, 2, 3]; Assert.Equal(new byte[] { 1, 2, 3 }, fixture.Reader.Resolve(inline)); foreach (int size in new[] { 100, 5000 }) { byte[] payload = Enumerable.Range(0, size).Select(i => (byte)i).ToArray(); - LongValueResult stored = fixture.Writer.Write(payload); + (byte[] Descriptor, IReadOnlyList OwnedPages, int FreePage) stored = fixture.Writer.Write(payload); Assert.Equal(payload, fixture.Reader.Resolve(stored.Descriptor)); } } @@ -88,19 +89,11 @@ private static void RewriteNextPointer(Fixture fixture, int pageNumber, int next byte[] page = fixture.Table.Channel.ReadPage(pageNumber).Span.ToArray(); var parsed = new DataPage(); parsed.Read(fixture.Table.Channel.ReadPage(pageNumber), fixture.Table.Channel.Format); - RowSlot slot = parsed.Rows[0]; - WritePagePointer(page.AsSpan(slot.Offset, 4), nextPage); + DataPage.RowSlot slot = parsed.Rows[0]; + PageBuffer.WriteRecordPointer(page, slot.Offset, 0, nextPage); fixture.Table.Channel.WritePage(pageNumber, page); } - private static void WritePagePointer(Span pointer, int page) - { - pointer[0] = 0; - pointer[1] = (byte)page; - pointer[2] = (byte)(page >> 8); - pointer[3] = (byte)(page >> 16); - } - private sealed class Fixture : IDisposable { private readonly string _path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "lval-corrupt-"); @@ -110,13 +103,13 @@ public Fixture() { _database = JetDatabase.Open(_path, readOnly: false); Table = _database.OpenTable("Categories"); - Writer = new LongValueWriter(Table.Channel); - Reader = new LongValueReader(Table.Channel); + Writer = new LongValueStore(Table.Channel); + Reader = new LongValueStore(Table.Channel); } public Table Table { get; } - public LongValueWriter Writer { get; } - public LongValueReader Reader { get; } + public LongValueStore Writer { get; } + public LongValueStore Reader { get; } public void Dispose() { @@ -124,4 +117,4 @@ public void Dispose() TemporaryDatabase.Delete(_path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/MultiPageDefinitionTests.cs b/test/LibRed.Core.Tests/MultiPageDefinitionTests.cs index d3be3683..ce49b9a7 100644 --- a/test/LibRed.Core.Tests/MultiPageDefinitionTests.cs +++ b/test/LibRed.Core.Tests/MultiPageDefinitionTests.cs @@ -1,6 +1,7 @@ using LibRed; using LibRed.Catalog; using LibRed.IO; +using LibRed.Pages; using Xunit; namespace LibRed.Core.Tests; @@ -29,9 +30,9 @@ public void Index_that_overflows_the_tdef_page_spills_to_a_continuation_and_roun using (var ch = PageChannel.Open(path, readOnly: true)) { var def = new JetCatalog(ch).FindTable("Wide")!; - var buf = ch.ReadPage(def.DefinitionPage); - Assert.True(buf.ReadInt32(0x08) > ch.Format.PageSize); // definition exceeds one page - Assert.NotEqual(0, buf.ReadInt32(ch.Format.TdefNextPageOffset)); // a continuation page exists + (PageBuffer definition, IReadOnlyList continuations) = TableDefinition.ReadChain(ch, def.DefinitionPage); + Assert.True(definition.Length > ch.Format.PageSize); // definition exceeds one page + Assert.NotEmpty(continuations); // a continuation page exists } using (var db = JetDatabase.Open(path)) @@ -70,7 +71,8 @@ public void A_definition_near_a_page_boundary_spills_its_reserve_and_reads_back( using (var ch = PageChannel.Open(path, readOnly: true)) { var chainFree = new List(); - for (int page = new JetCatalog(ch).FindTable("L")!.DefinitionPage; page != 0; page = ch.ReadPage(page).ReadInt32(ch.Format.TdefNextPageOffset)) + int first = new JetCatalog(ch).FindTable("L")!.DefinitionPage; + foreach (int page in (int[])[first, .. TableDefinition.ReadChain(ch, first).ContinuationPages]) chainFree.Add(ch.ReadPage(page).ReadUInt16(ch.Format.TdefFreeSpaceOffset)); Assert.Equal(free, chainFree); } @@ -83,4 +85,4 @@ public void A_definition_near_a_page_boundary_spills_its_reserve_and_reads_back( } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/MultiPageTableDefinitionTests.cs b/test/LibRed.Core.Tests/MultiPageTableDefinitionTests.cs index af30c2ee..33d8e44b 100644 --- a/test/LibRed.Core.Tests/MultiPageTableDefinitionTests.cs +++ b/test/LibRed.Core.Tests/MultiPageTableDefinitionTests.cs @@ -61,8 +61,7 @@ public void Rejects_an_invalid_continuation_chain_before_assembly(string corrupt var channel = table.Channel; int firstPage = table.Definition.DefinitionPage; byte[] first = channel.ReadPage(firstPage).Span.ToArray(); - int continuation = BinaryPrimitives.ReadInt32LittleEndian( - first.AsSpan(db.Format.TdefNextPageOffset, 4)); + int continuation = db.ReadTableDefinition(firstPage).NextDefinitionPage; Assert.True(continuation > 0); switch (corruption) @@ -108,4 +107,4 @@ public void Rejects_an_invalid_continuation_chain_before_assembly(string corrupt } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/NameMapTests.cs b/test/LibRed.Core.Tests/NameMapTests.cs index bd49aa30..5ffb88a1 100644 --- a/test/LibRed.Core.Tests/NameMapTests.cs +++ b/test/LibRed.Core.Tests/NameMapTests.cs @@ -1,5 +1,6 @@ using LibRed; using LibRed.Catalog; +using LibRed.Formats; using Xunit; namespace LibRed.Core.Tests; @@ -67,7 +68,7 @@ public void A_null_guid_record_does_not_end_a_property_map() { NameMap map = new() { - Records = [new NameMapRecord(Guid.Empty, 0, "Unresolved"), new NameMapRecord(Table1, 0, "Table1")], + Records = [new NameMap.Entry(Guid.Empty, 0, "Unresolved"), new NameMap.Entry(Table1, 0, "Table1")], }; NameMap read = NameMap.ReadProperty(map.WriteProperty()); @@ -107,7 +108,7 @@ public void A_database_s_name_map_row_can_be_read_and_updated() string path = TemporaryDatabase.CopyPath(Fixture, "namemap-row-"); try { - NameMapRow row; + NameMap.CatalogRow row; using (var db = JetDatabase.Open(path, readOnly: false)) { row = Assert.Single(db.ReadNameMaps()); @@ -126,7 +127,7 @@ public void A_database_s_name_map_row_can_be_read_and_updated() using (var db = JetDatabase.Open(path, readOnly: true)) { - NameMapRow after = Assert.Single(db.ReadNameMaps()); + NameMap.CatalogRow after = Assert.Single(db.ReadNameMaps()); Assert.Equal("Renamed", after.Name); Assert.Equal(row.Id, after.Id); Assert.Equal("Renamed", after.Map!.Records[0].Name); @@ -145,25 +146,25 @@ public void A_table_s_name_map_property_can_be_read_updated_and_removed_without_ NameMap original; using (var db = JetDatabase.Open(path, readOnly: false)) { - original = Assert.IsType(db.ReadNameMapProperty("Table1", 1)); + original = Assert.IsType(db.ReadNameMapProperty("Table1", ObjectType.Table)); Assert.Equal("Table1", original.Records[0].Name); NameMap renamed = original with { Records = [original.Records[0] with { Name = "Renamed" }, .. original.Records.Skip(1)], }; - db.UpdateNameMapProperty("Table1", 1, renamed); - Assert.Throws(() => db.ReadNameMapProperty("NoSuchTable", 1)); + db.UpdateNameMapProperty("Table1", ObjectType.Table, renamed); + Assert.Throws(() => db.ReadNameMapProperty("NoSuchTable", ObjectType.Table)); } using (var db = JetDatabase.Open(path, readOnly: false)) { - NameMap after = Assert.IsType(db.ReadNameMapProperty("Table1", 1)); + NameMap after = Assert.IsType(db.ReadNameMapProperty("Table1", ObjectType.Table)); Assert.Equal("Renamed", after.Records[0].Name); Assert.Equal(original.Version, after.Version); Assert.NotNull(TableProperty(db, "GUID")); - db.UpdateNameMapProperty("Table1", 1, null); - Assert.Null(db.ReadNameMapProperty("Table1", 1)); + db.UpdateNameMapProperty("Table1", ObjectType.Table, null); + Assert.Null(db.ReadNameMapProperty("Table1", ObjectType.Table)); Assert.NotNull(TableProperty(db, "GUID")); } } @@ -180,15 +181,15 @@ public void A_table_s_name_map_property_can_be_read_updated_and_removed_without_ } // Records hold their slot as an array, so record equality compares it by reference. - private sealed class NameMapRecordComparer : IEqualityComparer + private sealed class NameMapRecordComparer : IEqualityComparer { public static readonly NameMapRecordComparer Instance = new(); - public bool Equals(NameMapRecord? x, NameMapRecord? y) => + public bool Equals(NameMap.Entry? x, NameMap.Entry? y) => x is not null && y is not null && x with { Slot = NoSlot } == y with { Slot = NoSlot } && x.Slot.AsSpan().SequenceEqual(y.Slot); private static readonly byte[] NoSlot = []; - public int GetHashCode(NameMapRecord obj) => obj.ItemGuid.GetHashCode(); + public int GetHashCode(NameMap.Entry obj) => obj.ItemGuid.GetHashCode(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/OfficeStandardEncryptionTests.cs b/test/LibRed.Core.Tests/OfficeStandardEncryptionTests.cs index ff64e46b..a3229fae 100644 --- a/test/LibRed.Core.Tests/OfficeStandardEncryptionTests.cs +++ b/test/LibRed.Core.Tests/OfficeStandardEncryptionTests.cs @@ -1,5 +1,6 @@ using System.Buffers.Binary; using LibRed.Crypto; +using LibRed.Formats; using Xunit; namespace LibRed.Core.Tests; @@ -10,6 +11,8 @@ namespace LibRed.Core.Tests; // just the binary EncryptionInfo descriptor exercises the whole key-derivation + verifier path without the DB. public class OfficeStandardEncryptionTests { + private static readonly JetFormatBase Format = JetFormatBase.FromVersionByte(0x02); // ACE 12 + // db2007-oldenc.accdb — RC4-40, password "Test123" private const uint AlgRc4 = 0x6801; private static readonly byte[] Rc4Salt = Convert.FromHexString("78da7d5196c71492eed3b4471a479449"); @@ -24,25 +27,25 @@ public class OfficeStandardEncryptionTests [Fact] public void Rc4_authenticates_correct_password() => - Assert.NotNull(OfficeStandardEncryption.TryCreate(BuildPage0(AlgRc4, 0x04, 40, Rc4Salt, Rc4EncVerifier, Rc4EncVerifierHash), 0x12345678, "Test123")); + Assert.NotNull(OfficeStandardEncryption.TryCreate(BuildPage0(AlgRc4, 0x04, 40, Rc4Salt, Rc4EncVerifier, Rc4EncVerifierHash), 0x12345678, "Test123", Format)); [Fact] public void Aes_authenticates_correct_password() => - Assert.NotNull(OfficeStandardEncryption.TryCreate(BuildPage0(AlgAes256, 0x0C, 256, AesSalt, AesEncVerifier, AesEncVerifierHash), 0x12345678, "password")); + Assert.NotNull(OfficeStandardEncryption.TryCreate(BuildPage0(AlgAes256, 0x0C, 256, AesSalt, AesEncVerifier, AesEncVerifierHash), 0x12345678, "password", Format)); [Fact] public void Wrong_password_throws() => Assert.Throws(() => - OfficeStandardEncryption.TryCreate(BuildPage0(AlgAes256, 0x0C, 256, AesSalt, AesEncVerifier, AesEncVerifierHash), 0x12345678, "wrong")); + OfficeStandardEncryption.TryCreate(BuildPage0(AlgAes256, 0x0C, 256, AesSalt, AesEncVerifier, AesEncVerifierHash), 0x12345678, "wrong", Format)); [Fact] public void Missing_password_throws() => Assert.Throws(() => - OfficeStandardEncryption.TryCreate(BuildPage0(AlgRc4, 0x04, 40, Rc4Salt, Rc4EncVerifier, Rc4EncVerifierHash), 0x12345678, null)); + OfficeStandardEncryption.TryCreate(BuildPage0(AlgRc4, 0x04, 40, Rc4Salt, Rc4EncVerifier, Rc4EncVerifierHash), 0x12345678, null, Format)); [Fact] public void Unencrypted_returns_null() => - Assert.Null(OfficeStandardEncryption.TryCreate(new byte[4096], databaseKey: 0, password: "x")); + Assert.Null(OfficeStandardEncryption.TryCreate(new byte[Format.PageSize], databaseKey: 0, password: "x", Format)); [Theory] [InlineData(0)] @@ -52,7 +55,7 @@ public void Verifier_hash_size_must_exactly_match_the_declared_hash(int verifier { Assert.Throws(() => OfficeStandardEncryption.TryCreate( BuildPage0(AlgRc4, 0x04, 40, Rc4Salt, Rc4EncVerifier, Rc4EncVerifierHash, verifierHashSize), - 0x12345678, "Test123")); + 0x12345678, "Test123", Format)); } [Theory] @@ -65,7 +68,7 @@ public void Rc4_key_size_must_be_a_supported_whole_byte_count(int keyBits) { Assert.Throws(() => OfficeStandardEncryption.TryCreate( BuildPage0(AlgRc4, 0x04, keyBits, Rc4Salt, Rc4EncVerifier, Rc4EncVerifierHash), - 0x12345678, "Test123")); + 0x12345678, "Test123", Format)); } [Fact] @@ -73,7 +76,7 @@ public void Aes_key_size_must_be_a_supported_exact_size() { Assert.Throws(() => OfficeStandardEncryption.TryCreate( BuildPage0(AlgAes256, 0x0C, 257, AesSalt, AesEncVerifier, AesEncVerifierHash), - 0x12345678, "password")); + 0x12345678, "password", Format)); } [Theory] @@ -82,10 +85,10 @@ public void Aes_key_size_must_be_a_supported_exact_size() public void Encrypt_then_decrypt_round_trips(bool rc4) { var codec = rc4 - ? OfficeStandardEncryption.TryCreate(BuildPage0(AlgRc4, 0x04, 40, Rc4Salt, Rc4EncVerifier, Rc4EncVerifierHash), 0x12345678, "Test123")! - : OfficeStandardEncryption.TryCreate(BuildPage0(AlgAes256, 0x0C, 256, AesSalt, AesEncVerifier, AesEncVerifierHash), 0x12345678, "password")!; + ? OfficeStandardEncryption.TryCreate(BuildPage0(AlgRc4, 0x04, 40, Rc4Salt, Rc4EncVerifier, Rc4EncVerifierHash), 0x12345678, "Test123", Format)! + : OfficeStandardEncryption.TryCreate(BuildPage0(AlgAes256, 0x0C, 256, AesSalt, AesEncVerifier, AesEncVerifierHash), 0x12345678, "password", Format)!; - var page = new byte[4096]; + var page = new byte[Format.PageSize]; new Random(5).NextBytes(page); var original = (byte[])page.Clone(); codec.EncryptPage(4, page); @@ -101,8 +104,8 @@ private static byte[] BuildPage0( uint algId, uint flags, int keyBits, byte[] salt, byte[] encVerifier, byte[] encVerifierHash, int verifierHashSize = 20) { - var page = new byte[4096]; - const int ei = 0x29B; + var page = new byte[Format.PageSize]; + int ei = Format.EncryptionInfoOffset; const int headerSize = 32; void U16(int o, ushort v) => BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(o), v); void U32(int o, uint v) => BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(o), v); @@ -122,7 +125,7 @@ private static byte[] BuildPage0( U32(v + 4 + salt.Length + 16, (uint)verifierHashSize); // VerifierHashSize (SHA1 normally 20) encVerifierHash.CopyTo(page, v + 4 + salt.Length + 16 + 4); int descriptorLength = v + 4 + salt.Length + 16 + 4 + encVerifierHash.Length - ei; - U16(0x299, checked((ushort)descriptorLength)); + U16(Format.EncryptionInfoLengthOffset, checked((ushort)descriptorLength)); return page; } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/OfficeStandardVariantReadTests.cs b/test/LibRed.Core.Tests/OfficeStandardVariantReadTests.cs index 9e509367..0b499ee6 100644 --- a/test/LibRed.Core.Tests/OfficeStandardVariantReadTests.cs +++ b/test/LibRed.Core.Tests/OfficeStandardVariantReadTests.cs @@ -1,6 +1,7 @@ using System.Buffers.Binary; using LibRed; using LibRed.Crypto; +using LibRed.Formats; using Xunit; namespace LibRed.Core.Tests; @@ -8,9 +9,9 @@ namespace LibRed.Core.Tests; /// Fixture-free rejection coverage for unsupported Office-Standard descriptor variants. public class OfficeStandardVariantReadTests { - private const int DescriptorOffset = 0x29B; - private const int AlgorithmOffset = DescriptorOffset + 12 + 8; - private const int HashOffset = DescriptorOffset + 12 + 12; + private static readonly JetFormatBase Format = TestDatabases.FormatOf(TestDatabases.WideTableAccdb); + private static readonly int AlgorithmOffset = Format.EncryptionInfoOffset + 12 + 8; + private static readonly int HashOffset = Format.EncryptionInfoOffset + 12 + 12; [Theory] [InlineData(0x6603u)] // 3DES-168 @@ -56,7 +57,7 @@ public void A_zero_length_descriptor_is_read_as_unencrypted_like_ace() string path = CreateEncryptedCopy(); try { - MutateUInt16(path, 0x299, 0); + MutateUInt16(path, Format.EncryptionInfoLengthOffset, 0); var error = Assert.Throws( () => JetDatabase.Open(path, readOnly: true, password: "Test123")); Assert.Contains("unsupported scheme", error.Message, StringComparison.OrdinalIgnoreCase); @@ -85,4 +86,4 @@ private static void MutateUInt16(string path, int offset, ushort value) BinaryPrimitives.WriteUInt16LittleEndian(file.AsSpan(offset, 2), value); File.WriteAllBytes(path, file); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/PageAndRowCorruptionTests.cs b/test/LibRed.Core.Tests/PageAndRowCorruptionTests.cs index ff3075a1..0bebc525 100644 --- a/test/LibRed.Core.Tests/PageAndRowCorruptionTests.cs +++ b/test/LibRed.Core.Tests/PageAndRowCorruptionTests.cs @@ -41,14 +41,14 @@ public void Malformed_data_page_slots_are_rejected_as_corruption(string corrupti BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowCountOffset, 2), ushort.MaxValue); break; case "row-overlaps-directory": - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowDirectoryOffset, 2), - (ushort)(Format.DataRowDirectoryOffset + 2)); + DataPage.WriteSlot(page, Format, 0, + Format.DataRowDirectoryOffset + Format.DataRowDirectoryEntrySize, RowSlotFlags.None); break; case "row-offset-past-page": - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowDirectoryOffset, 2), 5000); + DataPage.WriteSlot(page, Format, 0, 5000, RowSlotFlags.None); break; case "ascending-row-offsets": - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowDirectoryOffset + 2, 2), 4050); + DataPage.WriteSlot(page, Format, 1, 4050, RowSlotFlags.None); break; } @@ -68,13 +68,12 @@ public void Direct_row_seek_applies_the_same_slot_validation() public void Zero_length_deleted_overflow_tombstone_remains_a_valid_slot_shape() { byte[] page = NewDataPage(rowCount: 2, firstOffset: 4000, secondOffset: 4000); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowDirectoryOffset + 2, 2), - (ushort)(4000 | 0x8000 | 0x4000)); + DataPage.WriteSlot(page, Format, 1, 4000, RowSlotFlags.Deleted | RowSlotFlags.Overflow); var dataPage = new DataPage(); dataPage.Read(new PageBuffer(page, 7), Format); - Assert.Equal(new RowSlot(4000, 0, IsDeleted: true, HasOverflow: true), dataPage.Rows[1]); + Assert.Equal(new DataPage.RowSlot(4000, 0, IsDeleted: true, HasOverflow: true), dataPage.Rows[1]); } [Fact] @@ -83,7 +82,7 @@ public void All_fixed_row_does_not_parse_fixed_bytes_as_a_variable_trailer() ColumnDef column = FixedColumn(JetDataType.Int32, length: 4); byte[] row = [1, 0, 0xFF, 0xFF, 0xFF, 0x7F, 1]; - object?[] values = new RowDecoder([column], Format).Decode(row); + object?[] values = new RowCodec([column], Format).Decode(row); Assert.Equal(int.MaxValue, values[0]); } @@ -119,7 +118,7 @@ public void Malformed_rows_are_rejected_as_corruption(string corruption) }; } - Assert.Throws(() => new RowDecoder([column], Format).Decode(row)); + Assert.Throws(() => new RowCodec([column], Format).Decode(row)); } [Fact] @@ -128,7 +127,7 @@ public void Column_added_after_an_old_row_decodes_as_null_when_its_bitmap_bit_do ColumnDef column = FixedColumn(JetDataType.Int32, length: 4, columnId: 8); byte[] oldRow = [1, 0, 0]; - object?[] values = new RowDecoder([column], Format).Decode(oldRow); + object?[] values = new RowCodec([column], Format).Decode(oldRow); Assert.Null(values[0]); } @@ -143,7 +142,7 @@ public void First_variable_column_added_after_an_old_all_fixed_row_decodes_as_nu }; byte[] oldRow = [1, 0, 0]; - object?[] values = new RowDecoder([column], Format).Decode(oldRow); + object?[] values = new RowCodec([column], Format).Decode(oldRow); Assert.Null(values[0]); } @@ -153,8 +152,8 @@ private static byte[] NewDataPage(int rowCount, int firstOffset, int secondOffse var page = new byte[Format.PageSize]; PageHeader.WriteType(page, PageType.DataPage); BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowCountOffset, 2), (ushort)rowCount); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowDirectoryOffset, 2), (ushort)firstOffset); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.DataRowDirectoryOffset + 2, 2), (ushort)secondOffset); + DataPage.WriteSlot(page, Format, 0, firstOffset, RowSlotFlags.None); + DataPage.WriteSlot(page, Format, 1, secondOffset, RowSlotFlags.None); return page; } @@ -169,4 +168,4 @@ private static JetFormatBase OpenFormat() using var db = JetDatabase.Open(TestDatabases.NorthwindAccdb); return db.Format; } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/PageCacheTests.cs b/test/LibRed.Core.Tests/PageCacheTests.cs index dc93a47f..c804798b 100644 --- a/test/LibRed.Core.Tests/PageCacheTests.cs +++ b/test/LibRed.Core.Tests/PageCacheTests.cs @@ -72,7 +72,8 @@ public void Two_paths_share_a_pool_exactly_when_they_name_one_file() // And the same on disk once both have closed: one file carries the write, two files do not. byte[] onDisk = File.ReadAllBytes(variant); - Assert.Equal(sameFile, onDisk.AsSpan(page * 4096, 4096).SequenceEqual(mutated)); + int pageSize = TestDatabases.FormatOf(variant).PageSize; + Assert.Equal(sameFile, onDisk.AsSpan(page * pageSize, pageSize).SequenceEqual(mutated)); } finally { @@ -126,4 +127,4 @@ public void Reopening_after_the_last_channel_closes_reads_committed_bytes_from_d } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/PageChannelTests.cs b/test/LibRed.Core.Tests/PageChannelTests.cs index 6d683257..ba46b4b3 100644 --- a/test/LibRed.Core.Tests/PageChannelTests.cs +++ b/test/LibRed.Core.Tests/PageChannelTests.cs @@ -49,7 +49,7 @@ public void RollbackTransaction_restores_modified_pages_and_drops_allocated_ones channel.WritePage(1, page); // ...and allocate a couple of new ones. - channel.AllocatePage(); + channel.Allocator.Append(); channel.WritePage(channel.PageCount, new byte[channel.PageSize]); Assert.True(channel.PageCount > pagesBefore); Assert.True(channel.InTransaction); @@ -210,4 +210,4 @@ public void A_dependency_is_held_once_per_key_and_a_savepoint_rollback_frees_its } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/PageChannelWriteTests.cs b/test/LibRed.Core.Tests/PageChannelWriteTests.cs index 097eec8a..9e86ca5a 100644 --- a/test/LibRed.Core.Tests/PageChannelWriteTests.cs +++ b/test/LibRed.Core.Tests/PageChannelWriteTests.cs @@ -87,7 +87,7 @@ public void AllocatePage_grows_the_file_by_one_zeroed_page() using (var channel = PageChannel.Open(path, readOnly: false)) { countBefore = channel.PageCount; - allocated = channel.AllocatePage(); + allocated = channel.Allocator.Append(); Assert.Equal(countBefore, allocated); Assert.Equal(countBefore + 1, channel.PageCount); } @@ -112,4 +112,4 @@ public void Read_only_channel_refuses_writes() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs b/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs index c6d2cf1f..7b49995b 100644 --- a/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs +++ b/test/LibRed.Core.Tests/PropertyBlobRoundTripTests.cs @@ -6,7 +6,7 @@ namespace LibRed.Core.Tests; // The LvProp property blob must round-trip EVERY property faithfully — including ones LibRed does not model // (a numeric DecimalPlaces, a designer ValidationRule/Format) — because an ALTER that edits a table's defaults -// or nullability rewrites the whole blob (TableCreator.MutateLvPropForColumn does Read -> Write). Property.RawValue +// or nullability rewrites the whole blob (SchemaEditor.MutateLvPropForColumn does Read -> Write). Property.RawValue // and Flags preserve each entry's exact stored value bytes and flag byte, so an unmodelled property survives // rather than being mangled or silently converted into a definition-protected property. public class PropertyBlobRoundTripTests @@ -301,4 +301,4 @@ public void A_flag_byte_and_an_index_block_round_trip_and_stay_the_indexs() Assert.Equal("CustID", Assert.Single(renamed, p => p.Name == PropertyBlob.DefaultValueProperty).Owner); Assert.Equal("CustomerID", Assert.Single(renamed, p => p.Block == IndexBlock).Owner); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/PublicTableWriteTests.cs b/test/LibRed.Core.Tests/PublicTableWriteTests.cs new file mode 100644 index 00000000..f8c1e29d --- /dev/null +++ b/test/LibRed.Core.Tests/PublicTableWriteTests.cs @@ -0,0 +1,42 @@ +using LibRed.Catalog; +using LibRed.Storage; +using Xunit; + +namespace LibRed.Core.Tests; + +public class PublicTableWriteTests +{ + [Fact] + public void Updates_and_deletes_keep_indexes_consistent_even_with_an_incomplete_change_mask() + { + string path = Path.Combine(Path.GetTempPath(), $"public-table-{Guid.NewGuid():N}.accdb"); + try + { + JetDatabase.Create(path); + using var db = JetDatabase.Open(path, readOnly: false); + db.CreateTable("T", [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true)], primaryKey: ["Id"]); + Table table = db.OpenTable("T"); + IndexDef index = table.Definition.Indexes.Single(i => i.IsPrimaryKey); + table.Insert([1]); + table.Insert([2]); + RowId id = table.Rows().WithIds().First(r => Equals(r.Values[0], 1)).Id; + + db.CreateTable("Other", [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true)], primaryKey: ["Id"]); + Table other = db.OpenTable("Other"); + Assert.Throws(() => other.Update(id, [9])); + Assert.Throws(() => other.Delete(id)); + + Assert.Throws(() => table.Update(id, [2], new HashSet())); + Assert.Single(table.SeekRows(index, [1])); + Assert.Single(table.SeekRows(index, [2])); + + table.Update(id, [3], new HashSet()); + Assert.Empty(table.SeekRows(index, [1])); + Assert.Single(table.SeekRows(index, [3])); + table.Delete(id); + Assert.Empty(table.SeekRows(index, [3])); + Assert.Single(table.Rows()); + } + finally { TemporaryDatabase.Delete(path); } + } +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/ReleaseAtCloseTests.cs b/test/LibRed.Core.Tests/ReleaseAtCloseTests.cs index 32f9a908..55f3d6db 100644 --- a/test/LibRed.Core.Tests/ReleaseAtCloseTests.cs +++ b/test/LibRed.Core.Tests/ReleaseAtCloseTests.cs @@ -1,6 +1,7 @@ -using System.Buffers.Binary; using LibRed.Catalog; +using LibRed.Formats; using LibRed.IO; +using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -9,10 +10,12 @@ namespace LibRed.Core.Tests; // ACE holds the pages a session frees — a deleted row's long values, a dropped index, a dropped table — until the // session closes, then returns them and anything in the global released-pages map to the global free map and // clears the released map, lengthening it to cover the highest page released (docs/format/page-05-usage-maps.md -// §9.1). The long value an UPDATE replaces is the exception: its pages are free at once. Northwind's maps are page 1 -// rows 0 (free: 310 and 329 inside the file) and 1 (released, empty). +// §9.1). The long value an UPDATE replaces is the exception: its pages are free at once. Both maps are found through +// page 0's pointers; Northwind's free map has 310 and 329 inside the file, and its released map is empty. public class ReleaseAtCloseTests { + private static readonly JetFormatBase Format = TestDatabases.FormatOf(TestDatabases.NorthwindAccdb); + [Fact] public void Released_pages_stay_unreusable_until_close_then_become_free() { @@ -22,12 +25,12 @@ public void Released_pages_stay_unreusable_until_close_then_become_free() using (var db = JetDatabase.Open(path, readOnly: false)) { PageChannel channel = db.OpenTable("MSysObjects").Channel; - var allocator = new PageAllocator(channel); + var allocator = channel.Allocator; allocator.Release(300); allocator.Release(301); - Assert.False(MapBit(channel, 0, 300)); - Assert.False(MapBit(channel, 0, 301)); + Assert.False(FreeMapBit(channel,300)); + Assert.False(FreeMapBit(channel,301)); Assert.Equal(310, allocator.Allocate()); // the free pages, never the released ones Assert.Equal(329, allocator.Allocate()); Assert.Equal(353, allocator.Allocate()); @@ -36,9 +39,9 @@ public void Released_pages_stay_unreusable_until_close_then_become_free() using (var db = JetDatabase.Open(path)) { PageChannel channel = db.OpenTable("MSysObjects").Channel; - Assert.True(MapBit(channel, 0, 300)); - Assert.True(MapBit(channel, 0, 301)); - Assert.False(MapBit(channel, 0, 310)); + Assert.True(FreeMapBit(channel,300)); + Assert.True(FreeMapBit(channel,301)); + Assert.False(FreeMapBit(channel,310)); Assert.All(ReleasedBits(channel), b => Assert.Equal(0, b)); } } @@ -54,7 +57,7 @@ public void A_rolled_back_release_frees_nothing_and_a_savepoint_rollback_keeps_t using (var db = JetDatabase.Open(path, readOnly: false)) { PageChannel channel = db.OpenTable("MSysObjects").Channel; - var allocator = new PageAllocator(channel); + var allocator = channel.Allocator; channel.BeginTransaction(); allocator.Release(300); @@ -75,10 +78,10 @@ public void A_rolled_back_release_frees_nothing_and_a_savepoint_rollback_keeps_t using (var db = JetDatabase.Open(path)) { PageChannel channel = db.OpenTable("MSysObjects").Channel; - Assert.False(MapBit(channel, 0, 300)); - Assert.True(MapBit(channel, 0, 301)); - Assert.False(MapBit(channel, 0, 302)); - Assert.False(MapBit(channel, 0, 303)); + Assert.False(FreeMapBit(channel,300)); + Assert.True(FreeMapBit(channel,301)); + Assert.False(FreeMapBit(channel,302)); + Assert.False(FreeMapBit(channel,303)); } } finally { TemporaryDatabase.Delete(path); } @@ -94,7 +97,7 @@ public void The_close_lengthens_the_released_map_to_cover_the_highest_page_relea using (var db = JetDatabase.Open(path, readOnly: false)) { PageChannel channel = db.OpenTable("MSysObjects").Channel; - var allocator = new PageAllocator(channel); + var allocator = channel.Allocator; before = ReleasedBits(channel).Length; for (int i = 0; i < 400; i++) allocator.Allocate(); highest = channel.PageCount - 1; @@ -105,11 +108,12 @@ public void The_close_lengthens_the_released_map_to_cover_the_highest_page_relea { PageChannel channel = db.OpenTable("MSysObjects").Channel; // 5-byte header, then the bitmap to the highest page in whole 4-byte words. - int expected = ((highest / 8 + 1) + 3) / 4 * 4; + int growth = Format.UsageMapInlineGrowthSize; + int expected = (BitmapBits.ByteCount(highest + 1) + growth - 1) / growth * growth; Assert.True(expected > before); Assert.Equal(expected, ReleasedBits(channel).Length); Assert.All(ReleasedBits(channel), b => Assert.Equal(0, b)); - Assert.True(MapBit(channel, 0, highest)); + Assert.True(FreeMapBit(channel,highest)); } } finally { TemporaryDatabase.Delete(path); } @@ -121,12 +125,21 @@ public void Pages_already_in_the_released_map_are_merged_by_a_close_that_wrote_b string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "release-merge-"); try { - byte[] page1 = ReadPage(path, 1); - SetReleasedBit(page1, 300); - WritePage(path, 1, page1); + int holderPage; + byte[] holder; + using (var channel = PageChannel.Open(path, readOnly: true)) + { + (_, holderPage, holder, DataPage.RowSlot slot) = TestDatabases.GlobalMap(channel, Format.ReleasedPagesMapPointerOffset); + Span record = InlineMap(holder, slot); + int bit = 300 - UsageMap.StartPage(record, Format); + Span bits = UsageMap.InlineBits(record, Format); + Assert.InRange(bit, 0, bits.Length * 8 - 1); + BitmapBits.Set(bits, bit, true); + } + TestDatabases.WritePage(path, holderPage, holder); using (JetDatabase.Open(path, readOnly: false)) { } - Assert.Equal(page1, ReadPage(path, 1)); // an idle writable close changes nothing + Assert.Equal(holder, TestDatabases.ReadPage(path, holderPage)); // an idle writable close changes nothing using (var db = JetDatabase.Open(path, readOnly: false)) db.CreateTable("Wrote", [new("Id", JetDataType.Int32, 4, IsFixedLength: true)]); @@ -134,7 +147,7 @@ public void Pages_already_in_the_released_map_are_merged_by_a_close_that_wrote_b using (var db = JetDatabase.Open(path)) { PageChannel channel = db.OpenTable("MSysObjects").Channel; - Assert.True(MapBit(channel, 0, 300)); + Assert.True(FreeMapBit(channel,300)); Assert.All(ReleasedBits(channel), b => Assert.Equal(0, b)); } } @@ -158,7 +171,7 @@ public void Dropping_an_index_holds_its_root_but_a_memo_update_frees_the_old_cha int root = db.Catalog.FindTable("Paths")!.Indexes.Single().RootPage; db.DropIndex("Paths", db.Catalog.FindTable("Paths")!.Indexes.Single().Name); - Assert.False(MapBit(channel, 0, root)); + Assert.False(FreeMapBit(channel,root)); table = db.OpenTable("Paths"); (RowId id, object?[] values) = table.Rows().WithIds().Single(); @@ -179,51 +192,27 @@ public void Dropping_an_index_holds_its_root_but_a_memo_update_frees_the_old_cha // ---------------------------------------------------------------- helpers - private static (int Offset, int Length) Record(ReadOnlySpan page1, int row) + /// A global map's record on its holder page, which these tests expect in inline form. + private static Span InlineMap(byte[] holder, DataPage.RowSlot slot) { - int offset = BinaryPrimitives.ReadUInt16LittleEndian(page1[(14 + row * 2)..]) & 0x1FFF; - int end = row == 0 ? page1.Length : BinaryPrimitives.ReadUInt16LittleEndian(page1[(14 + (row - 1) * 2)..]) & 0x1FFF; - Assert.Equal(0x00, page1[offset]); // inline - return (offset, end - offset); + Span record = holder.AsSpan(slot.Offset, slot.Length); + Assert.Equal(UsageMapType.Inline, UsageMap.RecordType(record)); + return record; } - private static bool MapBit(PageChannel channel, int row, int page) + private static bool FreeMapBit(PageChannel channel, int page) { - byte[] page1 = channel.ReadPage(1).Span.ToArray(); - (int offset, int length) = Record(page1, row); - int bit = page - BinaryPrimitives.ReadInt32LittleEndian(page1.AsSpan(offset + 1)); - if (bit < 0 || bit / 8 >= length - 5) return false; - return (page1[offset + 5 + bit / 8] & (1 << (bit % 8))) != 0; + (_, _, byte[] holder, DataPage.RowSlot slot) = TestDatabases.GlobalMap(channel, Format.FreePagesMapPointerOffset); + Span record = InlineMap(holder, slot); + int bit = page - UsageMap.StartPage(record, Format); + Span bits = UsageMap.InlineBits(record, Format); + if (bit < 0 || bit / 8 >= bits.Length) return false; + return BitmapBits.Get(bits, bit); } private static byte[] ReleasedBits(PageChannel channel) { - byte[] page1 = channel.ReadPage(1).Span.ToArray(); - (int offset, int length) = Record(page1, 1); - return page1.AsSpan(offset + 5, length - 5).ToArray(); - } - - private static void SetReleasedBit(byte[] page1, int page) - { - (int offset, int length) = Record(page1, 1); - int bit = page - BinaryPrimitives.ReadInt32LittleEndian(page1.AsSpan(offset + 1)); - Assert.InRange(bit, 0, (length - 5) * 8 - 1); - page1[offset + 5 + bit / 8] |= (byte)(1 << (bit % 8)); - } - - private static byte[] ReadPage(string path, int page) - { - using var s = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - var bytes = new byte[4096]; - s.Position = page * 4096L; - s.ReadExactly(bytes); - return bytes; - } - - private static void WritePage(string path, int page, byte[] bytes) - { - using var s = new FileStream(path, FileMode.Open, FileAccess.ReadWrite); - s.Position = page * 4096L; - s.Write(bytes); + (_, _, byte[] holder, DataPage.RowSlot slot) = TestDatabases.GlobalMap(channel, Format.ReleasedPagesMapPointerOffset); + return UsageMap.InlineBits(InlineMap(holder, slot), Format).ToArray(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/RelocationSlotWidthTests.cs b/test/LibRed.Core.Tests/RelocationSlotWidthTests.cs index 1cf4d871..1823bbf7 100644 --- a/test/LibRed.Core.Tests/RelocationSlotWidthTests.cs +++ b/test/LibRed.Core.Tests/RelocationSlotWidthTests.cs @@ -46,19 +46,18 @@ [new ColumnSpec("Id", JetDataType.Int32, 4, IsFixedLength: true, IsNullable: fal private static (int Page, int Index, int Offset, int Length) FirstOverflowSlot(Table table) { PageChannel channel = table.Channel; - int dir = channel.Format.DataRowDirectoryOffset; + JetFormatBase format = channel.Format; foreach (int pageNumber in table.UsageMap.DataPages()) { PageBuffer page = channel.ReadPage(pageNumber); - int rowCount = page.ReadUInt16(channel.Format.DataRowCountOffset); + int rowCount = DataPage.ReadRowCount(page.Span, format); int prevEnd = page.Length; for (int i = 0; i < rowCount; i++) { - int raw = page.ReadUInt16(dir + i * 2); - int offset = raw & RowPointer.OffsetMask; + (int offset, RowSlotFlags flags) = DataPage.ReadSlot(page.Span, format, i); int length = prevEnd - offset; prevEnd = offset; - if ((raw & RowPointer.DeletedFlag) == 0 && (raw & RowPointer.OverflowFlag) != 0) + if ((flags & (RowSlotFlags.Deleted | RowSlotFlags.Overflow)) == RowSlotFlags.Overflow) return (pageNumber, i, offset, length); } } @@ -76,27 +75,26 @@ public void LibRed_writes_relocation_slots_exactly_four_bytes_wide() using var db = JetDatabase.Open(path, readOnly: true); Table table = db.OpenTable("R"); PageChannel channel = table.Channel; - int dir = channel.Format.DataRowDirectoryOffset; + JetFormatBase format = channel.Format; var widths = new List(); foreach (int pageNumber in table.UsageMap.DataPages()) { PageBuffer page = channel.ReadPage(pageNumber); - int rowCount = page.ReadUInt16(channel.Format.DataRowCountOffset); + int rowCount = DataPage.ReadRowCount(page.Span, format); int prevEnd = page.Length; for (int i = 0; i < rowCount; i++) { - int raw = page.ReadUInt16(dir + i * 2); - int offset = raw & RowPointer.OffsetMask; + (int offset, RowSlotFlags flags) = DataPage.ReadSlot(page.Span, format, i); int length = prevEnd - offset; prevEnd = offset; - if ((raw & RowPointer.DeletedFlag) == 0 && (raw & RowPointer.OverflowFlag) != 0) + if ((flags & (RowSlotFlags.Deleted | RowSlotFlags.Overflow)) == RowSlotFlags.Overflow) widths.Add(length); } } Assert.NotEmpty(widths); - Assert.All(widths, w => Assert.Equal(4, w)); + Assert.All(widths, w => Assert.Equal(PageBuffer.RecordPointerSize, w)); } finally { TemporaryDatabase.Delete(path); } } @@ -113,22 +111,22 @@ public void A_source_wider_than_the_pointer_resolves_to_the_same_row() Table table = db.OpenTable("R"); PageChannel channel = table.Channel; (int pageNumber, _, int offset, int length) = FirstOverflowSlot(table); - Assert.Equal(4, length); + Assert.Equal(PageBuffer.RecordPointerSize, length); - byte[] pointer = channel.ReadPage(pageNumber).Slice(offset, 4).ToArray(); + byte[] pointer = channel.ReadPage(pageNumber).Slice(offset, PageBuffer.RecordPointerSize).ToArray(); - byte[] trimmed = RowRelocationReader.Resolve( + byte[] trimmed = DataPage.ResolveRelocation( channel, table.Definition.DefinitionPage, - new RowSlot(offset, 4, IsDeleted: false, HasOverflow: true), pointer).Bytes.ToArray(); + new DataPage.RowSlot(offset, PageBuffer.RecordPointerSize, IsDeleted: false, HasOverflow: true), pointer).Bytes.ToArray(); // The Northwind shape: the pointer followed by 51 bytes of the row as it was before it moved. byte[] wide = new byte[55]; pointer.CopyTo(wide, 0); - for (int i = 4; i < wide.Length; i++) wide[i] = (byte)(i * 7); + for (int i = PageBuffer.RecordPointerSize; i < wide.Length; i++) wide[i] = (byte)(i * 7); - byte[] fromWide = RowRelocationReader.Resolve( + byte[] fromWide = DataPage.ResolveRelocation( channel, table.Definition.DefinitionPage, - new RowSlot(offset, wide.Length, IsDeleted: false, HasOverflow: true), wide).Bytes.ToArray(); + new DataPage.RowSlot(offset, wide.Length, IsDeleted: false, HasOverflow: true), wide).Bytes.ToArray(); Assert.Equal(trimmed, fromWide); } @@ -147,9 +145,10 @@ public void A_source_shorter_than_the_pointer_is_still_rejected() Table table = db.OpenTable("R"); (_, _, int offset, _) = FirstOverflowSlot(table); - var error = Assert.Throws(() => RowRelocationReader.Resolve( + var error = Assert.Throws(() => DataPage.ResolveRelocation( table.Channel, table.Definition.DefinitionPage, - new RowSlot(offset, 3, IsDeleted: false, HasOverflow: true), new byte[3])); + new DataPage.RowSlot(offset, PageBuffer.RecordPointerSize - 1, IsDeleted: false, HasOverflow: true), + new byte[PageBuffer.RecordPointerSize - 1])); Assert.Contains("4-byte pointer", error.Message); } finally { TemporaryDatabase.Delete(path); } @@ -166,4 +165,4 @@ public void The_northwind_system_storage_table_can_be_read() Table table = db.OpenTable("MSysAccessStorage"); Assert.NotEmpty(table.Rows().ToList()); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/RowCodecGappedIdTests.cs b/test/LibRed.Core.Tests/RowCodecGappedIdTests.cs index 65113df7..226f2da7 100644 --- a/test/LibRed.Core.Tests/RowCodecGappedIdTests.cs +++ b/test/LibRed.Core.Tests/RowCodecGappedIdTests.cs @@ -1,4 +1,3 @@ -using System.Buffers.Binary; using LibRed; using LibRed.Catalog; using LibRed.Storage; @@ -28,14 +27,14 @@ public void Count_and_null_bitmap_span_the_dead_ids_whose_bits_stay_clear() // Three live columns, id 1 is a DEAD gap (as a burned type-change would leave: ids 0, 2, 3). var cols = Int32Cols(("A", 0, 0), ("B", 2, 4), ("C", 3, 8)); - byte[] row = new RowEncoder(cols, db.Format).Encode([10, 20, 30]); + byte[] row = new RowCodec(cols, db.Format).Encode([10, 20, 30]); // Leading count = max id + 1 = 4 (NOT the live count 3). - Assert.Equal(4, BinaryPrimitives.ReadUInt16LittleEndian(row)); + Assert.Equal(4, RowCodec.Layout.ReadColumnCount(row, db.Format)); // 1-byte null bitmap: live ids 0,2,3 present, the dead id 1 clear → 0x0D. Assert.Equal(0x0D, row[^1]); // Round-trips (the decoder sizes the bitmap from the stored count, not the live count). - Assert.Equal(new object?[] { 10, 20, 30 }, new RowDecoder(cols, db.Format).Decode(row)); + Assert.Equal(new object?[] { 10, 20, 30 }, new RowCodec(cols, db.Format).Decode(row)); } [Fact] @@ -46,10 +45,10 @@ public void A_null_column_past_the_live_count_still_reads_null() // Two live columns with ids 0 and 3 (ids 1,2 dead); B (id 3) is null — its bit is beyond the live // count of 2, which is exactly the case that read back null before the fix. var cols = Int32Cols(("A", 0, 0), ("B", 3, 4)); - byte[] row = new RowEncoder(cols, db.Format).Encode([7, null]); + byte[] row = new RowCodec(cols, db.Format).Encode([7, null]); - Assert.Equal(4, BinaryPrimitives.ReadUInt16LittleEndian(row)); // count spans id 3 + Assert.Equal(4, RowCodec.Layout.ReadColumnCount(row, db.Format)); // count spans id 3 Assert.Equal(0x01, row[^1]); // A(0) present; dead 1,2 and null B(3) clear - Assert.Equal(new object?[] { 7, null }, new RowDecoder(cols, db.Format).Decode(row)); + Assert.Equal(new object?[] { 7, null }, new RowCodec(cols, db.Format).Decode(row)); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/RowDecoderTests.cs b/test/LibRed.Core.Tests/RowDecoderTests.cs index b31a2385..260883c4 100644 --- a/test/LibRed.Core.Tests/RowDecoderTests.cs +++ b/test/LibRed.Core.Tests/RowDecoderTests.cs @@ -13,7 +13,7 @@ public class RowDecoderTests // MSysObjects has a Memo column of its own — LvProp — so decoding one of its rows needs the pages // that value lives on. Without the reader it came back as its 12-byte descriptor, which these // assertions never look at and so never noticed. - var decoder = new RowDecoder(columns, db.Format, new LongValueReader(db.OpenTable("MSysObjects").Channel)); + var decoder = new RowCodec(columns, db.Format, longValues: new LongValueStore(db.OpenTable("MSysObjects").Channel)); var page = db.ReadDataPage(dataPage); var rows = new List(); @@ -59,4 +59,4 @@ public void Null_columns_decode_as_null() int connectIdx = columns.First(c => c.Name == "Connect").Index; Assert.Contains(rows, r => r[connectIdx] is null); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/RowEncoderTests.cs b/test/LibRed.Core.Tests/RowEncoderTests.cs index 93a25f9b..dbabf594 100644 --- a/test/LibRed.Core.Tests/RowEncoderTests.cs +++ b/test/LibRed.Core.Tests/RowEncoderTests.cs @@ -16,8 +16,8 @@ public void Encode_then_decode_round_trips_real_rows(string tableName) var table = db.OpenTable(tableName); var columns = table.Definition.Columns; - var encoder = new RowEncoder(columns, db.Format); - var decoder = new RowDecoder(columns, db.Format); + var encoder = new RowCodec(columns, db.Format); + var decoder = new RowCodec(columns, db.Format); int rows = 0; foreach (object?[] original in table.Rows()) @@ -41,8 +41,8 @@ public void Null_variable_columns_round_trip() var columns = table.Definition.Columns; int region = columns.Single(c => c.Name == "Region").Index; - var encoder = new RowEncoder(columns, db.Format); - var decoder = new RowDecoder(columns, db.Format); + var encoder = new RowCodec(columns, db.Format); + var decoder = new RowCodec(columns, db.Format); bool sawNull = false; foreach (object?[] original in table.Rows()) @@ -52,4 +52,4 @@ public void Null_variable_columns_round_trip() } Assert.True(sawNull, "expected at least one null Region"); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/RowRelocationCorruptionTests.cs b/test/LibRed.Core.Tests/RowRelocationCorruptionTests.cs index f8e409c9..2088698f 100644 --- a/test/LibRed.Core.Tests/RowRelocationCorruptionTests.cs +++ b/test/LibRed.Core.Tests/RowRelocationCorruptionTests.cs @@ -1,6 +1,9 @@ using System.Buffers.Binary; using LibRed; using LibRed.Catalog; +using LibRed.Formats; +using LibRed.IO; +using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -8,12 +11,6 @@ namespace LibRed.Core.Tests; public class RowRelocationCorruptionTests { - private const int PageSize = 4096; - private const int RowDirectoryOffset = 0x0E; - private const int OffsetMask = 0x1FFF; - private const int DeletedFlag = 0x8000; - private const int OverflowFlag = 0x4000; - [Theory] [InlineData("short-source")] [InlineData("page-outside-file")] @@ -95,49 +92,41 @@ private static (string Path, RowId Source) CreateRelocatedRow() private static void Corrupt(string path, RowId source, string corruption) { + JetFormatBase format = TestDatabases.FormatOf(path); + int pageSize = format.PageSize; byte[] file = File.ReadAllBytes(path); - Span sourcePage = file.AsSpan(source.Page * PageSize, PageSize); - int sourceEntryPos = RowDirectoryOffset + source.Row * 2; - int raw = BinaryPrimitives.ReadUInt16LittleEndian(sourcePage[sourceEntryPos..]); - int sourceOffset = raw & OffsetMask; - int sourceEnd = source.Row == 0 - ? PageSize - : BinaryPrimitives.ReadUInt16LittleEndian(sourcePage[(sourceEntryPos - 2)..]) & OffsetMask; - int pointer = BinaryPrimitives.ReadInt32LittleEndian(sourcePage[sourceOffset..]); - int targetPageNumber = pointer >> 8; - int targetRow = pointer & 0xFF; + Span sourcePage = file.AsSpan(source.Page * pageSize, pageSize); + (int sourceOffset, RowSlotFlags sourceFlags) = DataPage.ReadSlot(sourcePage, format, source.Row); + int sourceEnd = source.Row == 0 ? pageSize : DataPage.ReadSlot(sourcePage, format, source.Row - 1).Offset; + (int targetRow, int targetPageNumber) = PageBuffer.ReadRecordPointer(sourcePage, sourceOffset); switch (corruption) { case "short-source": - BinaryPrimitives.WriteUInt16LittleEndian(sourcePage[sourceEntryPos..], - (ushort)((raw & ~OffsetMask) | (sourceEnd - 3))); + DataPage.WriteSlot(sourcePage, format, source.Row, sourceEnd - 3, sourceFlags); break; case "page-outside-file": - BinaryPrimitives.WriteInt32LittleEndian(sourcePage[sourceOffset..], - ((file.Length / PageSize) + 1) << 8); + PageBuffer.WriteRecordPointer(sourcePage, sourceOffset, row: 0, file.Length / pageSize + 1); break; case "row-outside-page": - BinaryPrimitives.WriteInt32LittleEndian(sourcePage[sourceOffset..], (targetPageNumber << 8) | 0xFF); + PageBuffer.WriteRecordPointer(sourcePage, sourceOffset, row: 0xFF, targetPageNumber); break; case "target-not-hidden": case "target-is-overflow": case "target-wrong-owner": - Span targetPage = file.AsSpan(targetPageNumber * PageSize, PageSize); + Span targetPage = file.AsSpan(targetPageNumber * pageSize, pageSize); if (corruption == "target-wrong-owner") { - BinaryPrimitives.WriteUInt32LittleEndian(targetPage[4..], 2); + BinaryPrimitives.WriteUInt32LittleEndian(targetPage[format.DataOwnerOffset..], 2); break; } - int targetEntryPos = RowDirectoryOffset + targetRow * 2; - int targetRaw = BinaryPrimitives.ReadUInt16LittleEndian(targetPage[targetEntryPos..]); - targetRaw = corruption == "target-not-hidden" - ? targetRaw & ~DeletedFlag - : targetRaw | DeletedFlag | OverflowFlag; - BinaryPrimitives.WriteUInt16LittleEndian(targetPage[targetEntryPos..], (ushort)targetRaw); + (int targetOffset, RowSlotFlags targetFlags) = DataPage.ReadSlot(targetPage, format, targetRow); + DataPage.WriteSlot(targetPage, format, targetRow, targetOffset, corruption == "target-not-hidden" + ? targetFlags & ~RowSlotFlags.Deleted + : targetFlags | RowSlotFlags.Deleted | RowSlotFlags.Overflow); break; } File.WriteAllBytes(path, file); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/SortOrderProvenanceProbeTest.cs b/test/LibRed.Core.Tests/SortOrderProvenanceProbeTest.cs index f8f8cacd..a23a274b 100644 --- a/test/LibRed.Core.Tests/SortOrderProvenanceProbeTest.cs +++ b/test/LibRed.Core.Tests/SortOrderProvenanceProbeTest.cs @@ -144,7 +144,7 @@ private static bool TryPrimaries(char c, out byte[] primaries) Name = "K", Type = JetDataType.Text, Index = 0, Collation = Collation.GeneralLegacy, }; byte[] key; - try { key = IndexKeyEncoder.Encode([(column, true)], [c.ToString()]); } + try { key = IndexKeyCodec.Encode([(column, true)], [c.ToString()]); } catch (NotSupportedException) { return false; } int split = key.Length - 2; @@ -175,4 +175,4 @@ private static bool TryPrimaries(char c, out byte[] primaries) private static string Describe(char c) => c is >= ' ' and <= '~' ? $"'{c}'" : $"U+{(int)c:X4}"; -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/TableCreatorTests.cs b/test/LibRed.Core.Tests/TableCreatorTests.cs index d123be52..2d0a8c4e 100644 --- a/test/LibRed.Core.Tests/TableCreatorTests.cs +++ b/test/LibRed.Core.Tests/TableCreatorTests.cs @@ -9,24 +9,6 @@ namespace LibRed.Core.Tests; public class TableCreatorTests { - // Reads the table's inline free-pages map (TDEF 0x3B) -> the set of pages marked as having room. - private static List ReadFreePagesMap(PageChannel ch, int tdefPage) - { - const int FreePtr = 0x3B; - var tdef = ch.ReadPage(tdefPage).Span; - int row = tdef[FreePtr]; - int mapPage = tdef[FreePtr + 1] | tdef[FreePtr + 2] << 8 | tdef[FreePtr + 3] << 16; - var dp = new LibRed.Pages.DataPage(); - dp.Read(ch.ReadPage(mapPage), ch.Format); - ReadOnlySpan m = dp.GetRow(row); - int start = BinaryPrimitives.ReadInt32LittleEndian(m.Slice(1, 4)); - var pages = new List(); - for (int i = 5; i < m.Length; i++) - for (int b = 0; b < 8; b++) - if ((m[i] & (1 << b)) != 0) pages.Add(start + (i - 5) * 8 + b); - return pages; - } - [Fact] public void Multi_page_insert_marks_only_the_tail_page_free() { @@ -49,12 +31,13 @@ public void Multi_page_insert_marks_only_the_tail_page_free() using (var db = JetDatabase.Open(path)) { var table = db.OpenTable("FM"); - var owned = new UsageMap(table.Channel, table.Definition).DataPages().ToList(); + var maps = new UsageMap(table.Channel, table.Definition); + var owned = maps.DataPages().ToList(); Assert.True(owned.Count > 1, "expected multiple owned data pages"); // Access keeps only the current append tail (highest owned page) in the free-pages // map — earlier full pages are cleared as it moves past them. LibRed must match. - var free = ReadFreePagesMap(table.Channel, table.Definition.DefinitionPage); + var free = maps.FreeDataPages().ToList(); Assert.Equal([owned.Max()], free); } } @@ -80,10 +63,8 @@ public void Insert_maintains_the_unique_index_stat_and_leaves_total_at_zero() using (var db = JetDatabase.Open(path)) { var table = db.OpenTable("Stats"); - var span = table.Channel.ReadPage(table.Definition.DefinitionPage).Span; - var format = table.Channel.Format; - int total = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(format.TdefRealIndexBlockOffset, 4)); - int unique = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(format.TdefRealIndexBlockOffset + 4, 4)); + (int total, int unique) = LibRed.Catalog.TableDefinition.ReadIndexCounts( + table.Channel.ReadPage(table.Definition.DefinitionPage).Span, table.Channel.Format, ordinal: 0); // Access maintains the cumulative unique-entry count live (one distinct key per row // for a unique index) but leaves the total-entry count at 0 until compact. @@ -371,7 +352,7 @@ public void Creating_a_table_refuses_a_type_the_file_is_too_old_for() string path = TemporaryDatabase.CreatePath("versiongate-"); try { - DatabaseCreator.CreateEmpty(path, version: 0x02); // ACE 12 + JetDatabase.Create(path, version: 0x02); // ACE 12 using var db = JetDatabase.Open(path, readOnly: false); var error = Assert.Throws(() => db.CreateTable("T", @@ -390,7 +371,7 @@ public void A_duplicate_index_name_is_refused() string path = TemporaryDatabase.CreatePath("dupindex-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var db = JetDatabase.Open(path, readOnly: false); db.CreateTable("T", [Long("K"), Long("A"), Long("B")]); @@ -411,7 +392,7 @@ public void A_table_may_only_have_one_autonumber_column() string path = TemporaryDatabase.CreatePath("twocounters-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var db = JetDatabase.Open(path, readOnly: false); Assert.Throws(() => db.CreateTable("T", [Counter("A"), Counter("B")])); @@ -425,4 +406,4 @@ public void A_table_may_only_have_one_autonumber_column() private static ColumnSpec Long(string name) => new(name, JetDataType.Int32, 4, IsFixedLength: true); private static ColumnSpec Counter(string name) => new(name, JetDataType.Int32, 4, IsFixedLength: true, IsAutoNumber: true, Seed: 1, Increment: 1); -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/TableDefinitionPageTests.cs b/test/LibRed.Core.Tests/TableDefinitionPageTests.cs index 2ac340ef..bc3e9982 100644 --- a/test/LibRed.Core.Tests/TableDefinitionPageTests.cs +++ b/test/LibRed.Core.Tests/TableDefinitionPageTests.cs @@ -10,15 +10,12 @@ namespace LibRed.Core.Tests; public class TableDefinitionPageTests { - // In Jet 4 / ACE the system catalog table MSysObjects has its TDEF on page 2. - private const int MSysObjectsPage = 2; - [Fact] public void Reads_MSysObjects_table_definition() { using var db = JetDatabase.Open(TestDatabases.NorthwindAccdb); - var tdef = db.ReadTableDefinition(MSysObjectsPage); + var tdef = db.ReadTableDefinition(db.DefinitionPage.CatalogRootPage); Assert.Equal(TableType.System, tdef.TableType); Assert.Equal(17, tdef.ColumnCount); @@ -33,7 +30,7 @@ public void Decodes_MSysObjects_column_names() { using var db = JetDatabase.Open(TestDatabases.NorthwindAccdb); - var tdef = db.ReadTableDefinition(MSysObjectsPage); + var tdef = db.ReadTableDefinition(db.DefinitionPage.CatalogRootPage); var names = tdef.Columns.Select(c => c.Name).ToList(); // Known MSysObjects columns (a few stable ones, observed in the file). @@ -59,26 +56,28 @@ public void Rejects_counts_outside_jet_ace_table_geometry_before_parsing(string { using var db = JetDatabase.Open(TestDatabases.NorthwindAccdb); JetFormatBase format = db.Format; - byte[] page = TdefBuilder.Build(format, TableType.User, + byte[] page = TableDefinition.Build(format, TableType.User, [new ColumnSpec("C", JetDataType.Int32, 4, IsFixedLength: true)], Collation.GeneralLegacy).Page; switch (field) { case "columns": - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefColumnCountOffset, 2), 256); + BinaryPrimitives.WriteUInt16LittleEndian( + page.AsSpan(format.TdefColumnCountOffset, 2), (ushort)(format.MaxColumnsPerTable + 1)); break; case "variable-columns": - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefVariableColumnsOffset, 2), 256); + BinaryPrimitives.WriteUInt16LittleEndian( + page.AsSpan(format.TdefVariableColumnsOffset, 2), (ushort)(format.MaxColumnsPerTable + 1)); break; case "real-indexes": - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefIndexCountOffset, 4), 33); + BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefIndexCountOffset, 4), format.MaxIndexesPerTable + 1); break; case "logical-indexes": BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefLogicalIndexCountOffset, 4), -1); break; } - var definition = new TableDefinitionPage(); + var definition = new TableDefinition(); Assert.Throws(() => definition.Read(new PageBuffer(page, 99), format)); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/TdefBuilderTests.cs b/test/LibRed.Core.Tests/TdefBuilderTests.cs index a2c272ed..fc165474 100644 --- a/test/LibRed.Core.Tests/TdefBuilderTests.cs +++ b/test/LibRed.Core.Tests/TdefBuilderTests.cs @@ -31,9 +31,9 @@ public void Built_tdef_round_trips_through_the_reader() new("Notes", JetDataType.Text, 510, IsFixedLength: false), }; - var result = TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy); + var result = TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy); - var page = new TableDefinitionPage(); + var page = new TableDefinition(); page.Read(new PageBuffer(result.Page, 99), Format); Assert.Equal(TableType.User, page.TableType); @@ -73,9 +73,9 @@ public void Built_tdef_with_primary_key_index_round_trips() }; var indexes = new[] { new IndexSpec("PrimaryKey", ["Id"], IsPrimaryKey: true, IsUnique: true, RootPage: 42) }; - var result = TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy, indexes); + var result = TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy, indexes); - var page = new TableDefinitionPage(); + var page = new TableDefinition(); page.Read(new PageBuffer(result.Page, 7), Format); var pk = Assert.Single(page.Indexes); @@ -99,10 +99,10 @@ public void Built_tdef_columns_can_encode_and_decode_a_row() new("Id", JetDataType.Int32, 4, IsFixedLength: true), new("Name", JetDataType.Text, 510, IsFixedLength: false), }; - var result = TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy); + var result = TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy); - var encoder = new LibRed.Storage.RowEncoder(result.Columns, Format); - var decoder = new LibRed.Storage.RowDecoder(result.Columns, Format); + var encoder = new LibRed.Storage.RowCodec(result.Columns, Format); + var decoder = new LibRed.Storage.RowCodec(result.Columns, Format); object?[] row = [42, "hello"]; Assert.Equal(row, decoder.Decode(encoder.Encode(row))); @@ -115,7 +115,7 @@ public void Builder_rejects_more_than_255_columns_before_narrowing_the_count() .Select(i => new ColumnSpec($"C{i}", JetDataType.Int32, 4, IsFixedLength: true)) .ToArray(); - Assert.Throws(() => TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy)); + Assert.Throws(() => TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy)); } [Theory] @@ -125,7 +125,7 @@ public void Builder_rejects_column_fields_that_do_not_fit_the_verified_domain(in { ColumnSpec[] specs = [new("C", JetDataType.Int32, length, IsFixedLength: true, ColumnId: columnId)]; - Assert.Throws(() => TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy)); + Assert.Throws(() => TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy)); } [Fact] @@ -137,7 +137,7 @@ public void Builder_rejects_duplicate_column_ids() new("B", JetDataType.Int32, 4, IsFixedLength: true, ColumnId: 3), ]; - Assert.Throws(() => TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy)); + Assert.Throws(() => TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy)); } [Fact] @@ -145,7 +145,7 @@ public void Builder_rejects_a_column_wider_than_ace_stores() { ColumnSpec[] specs = [new("A", JetDataType.Binary, 40000, IsFixedLength: true)]; - Assert.Throws(() => TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy)); + Assert.Throws(() => TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy)); } // Every column within the per-field limit, yet the fixed region as a whole is past what a record can @@ -156,7 +156,7 @@ public void Builder_rejects_a_fixed_region_past_the_record_cap() ColumnSpec[] specs = [.. Enumerable.Range(0, 252) .Select(i => new ColumnSpec($"G{i}", JetDataType.Guid, 16, IsFixedLength: true))]; - Assert.Throws(() => TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy)); + Assert.Throws(() => TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy)); } [Fact] @@ -165,10 +165,10 @@ public void Builder_sizes_the_definition_from_the_actual_encoded_names() ColumnSpec[] columns = Enumerable.Range(0, 255) .Select(i => new ColumnSpec($"C{i:D3}" + new string('N', 60), JetDataType.Boolean, 0, IsFixedLength: true)) .ToArray(); - var result = TdefBuilder.Build(Format, TableType.User, columns, Collation.GeneralLegacy); + var result = TableDefinition.Build(Format, TableType.User, columns, Collation.GeneralLegacy); int declared = BinaryPrimitives.ReadInt32LittleEndian(result.Page.AsSpan(Format.TdefLengthOffset, 4)); - var definition = new TableDefinitionPage(); + var definition = new TableDefinition(); definition.Read(new PageBuffer(result.Page, 99), Format); Assert.Equal(255, definition.Columns.Count); Assert.Equal(declared, result.Page.Length); @@ -178,12 +178,12 @@ public void Builder_sizes_the_definition_from_the_actual_encoded_names() public void Builder_rejects_names_that_do_not_fit_their_16_bit_byte_lengths() { string tooLong = new('N', 65); - Assert.Throws(() => TdefBuilder.Build(Format, TableType.User, + Assert.Throws(() => TableDefinition.Build(Format, TableType.User, [new(tooLong, JetDataType.Int32, 4, IsFixedLength: true)], Collation.GeneralLegacy)); ColumnSpec[] columns = [new("Id", JetDataType.Int32, 4, IsFixedLength: true)]; IndexSpec[] indexes = [new(tooLong, ["Id"], true, true, RootPage: 2)]; - Assert.Throws(() => TdefBuilder.Build(Format, TableType.User, columns, Collation.GeneralLegacy, indexes)); + Assert.Throws(() => TableDefinition.Build(Format, TableType.User, columns, Collation.GeneralLegacy, indexes)); } [Theory] @@ -198,7 +198,7 @@ public void Builder_rejects_invalid_long_value_usage_map_fields( LongValueColumnSpec[] longValues = [new(columnId, usedRow, freeRow, mapPage)]; Assert.Throws(() => - TdefBuilder.Build(Format, TableType.User, columns, Collation.GeneralLegacy, longValueColumns: longValues)); + TableDefinition.Build(Format, TableType.User, columns, Collation.GeneralLegacy, longValueColumns: longValues)); } [Fact] @@ -207,9 +207,9 @@ public void Builder_rejects_duplicate_long_value_entries_and_unknown_index_colum ColumnSpec[] columns = [new("M", JetDataType.Memo, 0, IsFixedLength: false)]; LongValueColumnSpec[] duplicate = [new(0, 1, 2, 5), new(0, 1, 2, 5)]; Assert.Throws(() => - TdefBuilder.Build(Format, TableType.User, columns, Collation.GeneralLegacy, longValueColumns: duplicate)); + TableDefinition.Build(Format, TableType.User, columns, Collation.GeneralLegacy, longValueColumns: duplicate)); IndexSpec[] indexes = [new("IX", ["Missing"], false, false, RootPage: 2)]; - Assert.Throws(() => TdefBuilder.Build(Format, TableType.User, columns, Collation.GeneralLegacy, indexes)); + Assert.Throws(() => TableDefinition.Build(Format, TableType.User, columns, Collation.GeneralLegacy, indexes)); } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/TdefVariableRegionTests.cs b/test/LibRed.Core.Tests/TdefVariableRegionTests.cs index 6bc817fe..03e2d468 100644 --- a/test/LibRed.Core.Tests/TdefVariableRegionTests.cs +++ b/test/LibRed.Core.Tests/TdefVariableRegionTests.cs @@ -43,22 +43,27 @@ public void Malformed_variable_regions_are_rejected_with_a_corruption_error(stri ? [new("M", JetDataType.Memo, 0, IsFixedLength: false)] : [new("C", JetDataType.Int32, 4, IsFixedLength: true)]; - byte[] page = TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy).Page; - int columnBlock = Format.TdefRealIndexBlockOffset; + byte[] page = TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy).Page; + // Where the regions are in the sound definition, before any corruption: with no index, the long-value + // list follows the column names directly. + TableDefinition.Regions regions = TableDefinition.Regions.Of(page, Format); + int columnBlock = regions.ColumnDescriptors; int namePos = columnBlock + specs.Length * Format.ColumnDescriptorSize; - int lvalPos = SkipNames(page, namePos, specs.Length); + int lvalPos = regions.IndexNames; int declaredLength = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(Format.TdefLengthOffset, 4)); + int overlongName = Format.MaxNameBytes + sizeof(char); switch (corruption) { case "column-name-too-long": - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, 2), 130); - page.AsSpan(namePos + 2, 130).Fill((byte)'A'); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos + 132, 2), 0xFFFF); - declaredLength = namePos + 134; + BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, Format.TdefNameLengthSize), (ushort)overlongName); + page.AsSpan(namePos + Format.TdefNameLengthSize, overlongName).Fill((byte)'A'); + BinaryPrimitives.WriteUInt16LittleEndian( + page.AsSpan(namePos + Format.TdefNameLengthSize + overlongName, 2), JetFormatBase.TdefLongValueMapTerminator); + declaredLength = namePos + Format.TdefNameLengthSize + overlongName + sizeof(ushort); break; case "column-name-out-of-bounds": - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, 2), 128); + BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, Format.TdefNameLengthSize), (ushort)Format.MaxNameBytes); break; case "duplicate-column-id": BinaryPrimitives.WriteUInt16LittleEndian( @@ -66,31 +71,33 @@ public void Malformed_variable_regions_are_rejected_with_a_corruption_error(stri break; case "column-id-255": BinaryPrimitives.WriteUInt16LittleEndian( - page.AsSpan(columnBlock + Format.ColumnNumberOffset, 2), 255); + page.AsSpan(columnBlock + Format.ColumnNumberOffset, 2), (ushort)Format.MaxColumnsPerTable); break; case "unknown-column-type": page[columnBlock + Format.ColumnTypeOffset] = 0xFF; break; case "missing-lval-terminator": - declaredLength -= 2; + declaredLength -= sizeof(ushort); break; case "lval-for-non-lval-column": WriteLvalEntry(page, lvalPos, 0); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(lvalPos + 10, 2), 0xFFFF); - declaredLength += 10; + BinaryPrimitives.WriteUInt16LittleEndian( + page.AsSpan(lvalPos + Format.TdefLongValueMapEntrySize, 2), JetFormatBase.TdefLongValueMapTerminator); + declaredLength += Format.TdefLongValueMapEntrySize; break; case "duplicate-lval-column": WriteLvalEntry(page, lvalPos, 0); - WriteLvalEntry(page, lvalPos + 10, 0); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(lvalPos + 20, 2), 0xFFFF); - declaredLength += 20; + WriteLvalEntry(page, lvalPos + Format.TdefLongValueMapEntrySize, 0); + BinaryPrimitives.WriteUInt16LittleEndian( + page.AsSpan(lvalPos + 2 * Format.TdefLongValueMapEntrySize, 2), JetFormatBase.TdefLongValueMapTerminator); + declaredLength += 2 * Format.TdefLongValueMapEntrySize; break; case "odd-name-length": - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, 2), 1); + BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(namePos, Format.TdefNameLengthSize), 1); break; case "invalid-name-utf16": - page[namePos + 2] = 0x00; - page[namePos + 3] = 0xD8; // unpaired UTF-16 high surrogate + page[namePos + Format.TdefNameLengthSize] = 0x00; + page[namePos + Format.TdefNameLengthSize + 1] = 0xD8; // unpaired UTF-16 high surrogate break; case "trailing-after-lval": page[declaredLength] = 0; @@ -103,7 +110,7 @@ public void Malformed_variable_regions_are_rejected_with_a_corruption_error(stri } BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(Format.TdefLengthOffset, 4), declaredLength); - var definition = new TableDefinitionPage(); + var definition = new TableDefinition(); Assert.Throws(() => definition.Read(new PageBuffer(page.AsMemory(0, declaredLength), 99), Format)); } @@ -113,19 +120,18 @@ public void Overlong_index_name_is_rejected_before_decoding() { ColumnSpec[] specs = [new("C", JetDataType.Int32, 4, IsFixedLength: true)]; IndexSpec[] indexes = [new("I", ["C"], IsPrimaryKey: false, IsUnique: false, RootPage: 42)]; - byte[] page = TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy, indexes).Page; - - int pos = Format.TdefRealIndexBlockOffset + Format.RealIndexEntrySize - + Format.ColumnDescriptorSize; - pos = SkipNames(page, pos, 1); - int indexNamePos = pos + 52 + 28; // §3.5 data block + §3.6 logical-info block - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(indexNamePos, 2), 130); - page.AsSpan(indexNamePos + 2, 130).Fill((byte)'I'); - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(indexNamePos + 132, 2), 0xFFFF); - int declaredLength = indexNamePos + 134; + byte[] page = TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy, indexes).Page; + + int indexNamePos = TableDefinition.Regions.Of(page, Format).IndexNames; + int overlongName = Format.MaxNameBytes + sizeof(char); + BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(indexNamePos, Format.TdefNameLengthSize), (ushort)overlongName); + page.AsSpan(indexNamePos + Format.TdefNameLengthSize, overlongName).Fill((byte)'I'); + BinaryPrimitives.WriteUInt16LittleEndian( + page.AsSpan(indexNamePos + Format.TdefNameLengthSize + overlongName, 2), JetFormatBase.TdefLongValueMapTerminator); + int declaredLength = indexNamePos + Format.TdefNameLengthSize + overlongName + sizeof(ushort); BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(Format.TdefLengthOffset, 4), declaredLength); - var definition = new TableDefinitionPage(); + var definition = new TableDefinition(); Assert.Throws(() => definition.Read(new PageBuffer(page.AsMemory(0, declaredLength), 99), Format)); } @@ -142,7 +148,7 @@ public void Overlong_index_name_is_rejected_before_decoding() [InlineData("logical-index-region-overflow")] public void Malformed_header_counts_and_lengths_are_rejected_before_region_allocation(string corruption) { - byte[] page = TdefBuilder.Build(Format, TableType.User, + byte[] page = TableDefinition.Build(Format, TableType.User, [new("C", JetDataType.Int32, 4, IsFixedLength: true)], Collation.GeneralLegacy).Page; int declaredLength = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(Format.TdefLengthOffset, 4)); @@ -159,16 +165,18 @@ public void Malformed_header_counts_and_lengths_are_rejected_before_region_alloc BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(Format.TdefLengthOffset, 4), declaredLength + 1); break; case "too-many-columns": - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.TdefColumnCountOffset, 2), 256); + BinaryPrimitives.WriteUInt16LittleEndian( + page.AsSpan(Format.TdefColumnCountOffset, 2), (ushort)(Format.MaxColumnsPerTable + 1)); break; case "variable-column-high-water-overflow": - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(Format.TdefVariableColumnsOffset, 2), 256); + BinaryPrimitives.WriteUInt16LittleEndian( + page.AsSpan(Format.TdefVariableColumnsOffset, 2), (ushort)(Format.MaxColumnsPerTable + 1)); break; case "negative-index-count": BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(Format.TdefIndexCountOffset, 4), -1); break; case "too-many-indexes": - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(Format.TdefIndexCountOffset, 4), 33); + BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(Format.TdefIndexCountOffset, 4), Format.MaxIndexesPerTable + 1); break; case "negative-logical-index-count": BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(Format.TdefLogicalIndexCountOffset, 4), -1); @@ -178,7 +186,7 @@ public void Malformed_header_counts_and_lengths_are_rejected_before_region_alloc break; } - var definition = new TableDefinitionPage(); + var definition = new TableDefinition(); Assert.Throws(() => definition.Read(new PageBuffer(page.AsMemory(0, declaredLength), 99), Format)); } @@ -188,32 +196,16 @@ public void Valid_memo_usage_map_entry_remains_available() { ColumnSpec[] specs = [new("M", JetDataType.Memo, 0, IsFixedLength: false)]; LongValueColumnSpec[] maps = [new(ColumnId: 0, UsedRow: 2, FreeRow: 3, MapPage: 17)]; - byte[] page = TdefBuilder.Build(Format, TableType.User, specs, Collation.GeneralLegacy, longValueColumns: maps).Page; + byte[] page = TableDefinition.Build(Format, TableType.User, specs, Collation.GeneralLegacy, longValueColumns: maps).Page; int declaredLength = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(Format.TdefLengthOffset, 4)); - var definition = new TableDefinitionPage(); + var definition = new TableDefinition(); definition.Read(new PageBuffer(page.AsMemory(0, declaredLength), 99), Format); Assert.Equal((2, 17), definition.LongValueOwnedMaps[0]); Assert.Equal((3, 17), definition.LongValueFreeMaps[0]); } - private static int SkipNames(byte[] page, int pos, int count) - { - for (int i = 0; i < count; i++) - { - int length = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(pos, 2)); - pos += 2 + length; - } - return pos; - } - - private static void WriteLvalEntry(byte[] page, int pos, ushort columnId) - { - BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(pos, 2), columnId); - page[pos + 2] = 2; - page[pos + 3] = 17; - page[pos + 6] = 3; - page[pos + 7] = 17; - } -} + private static void WriteLvalEntry(byte[] page, int pos, ushort columnId) => + TableDefinition.LongValueMapEntry(Format, columnId, usedRow: 2, freeRow: 3, mapPage: 17).CopyTo(page, pos); +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/TransactionFailureRecoveryTests.cs b/test/LibRed.Core.Tests/TransactionFailureRecoveryTests.cs index 4e4bae2b..d97e8409 100644 --- a/test/LibRed.Core.Tests/TransactionFailureRecoveryTests.cs +++ b/test/LibRed.Core.Tests/TransactionFailureRecoveryTests.cs @@ -64,8 +64,8 @@ public void Conflicting_file_growth_can_rollback_and_retry_without_truncating_th first.BeginTransaction(); second.BeginTransaction(); - int firstPage = first.AllocatePage(); - int secondPage = second.AllocatePage(); + int firstPage = first.Allocator.Append(); + int secondPage = second.Allocator.Append(); Assert.Equal(originalCount, firstPage); Assert.Equal(firstPage, secondPage); WriteMarker(first, firstPage, 0x11); @@ -80,7 +80,7 @@ public void Conflicting_file_growth_can_rollback_and_retry_without_truncating_th Assert.Equal(0x11, second.ReadPage(firstPage).Span[100]); second.BeginTransaction(); - int retryPage = second.AllocatePage(); + int retryPage = second.Allocator.Append(); Assert.Equal(originalCount + 1, retryPage); WriteMarker(second, retryPage, 0x22); second.CommitTransaction(); @@ -102,7 +102,7 @@ public void Disposing_channel_with_outstanding_growth_discards_the_overlay() using (var channel = PageChannel.Open(path, readOnly: false)) { channel.BeginTransaction(); - int page = channel.AllocatePage(); + int page = channel.Allocator.Append(); WriteMarker(channel, page, 0x7E); Assert.Equal(before.Length / channel.PageSize + 1, channel.PageCount); } @@ -147,8 +147,8 @@ public void Failed_growth_publication_truncates_published_tail_and_same_transact using (var channel = PageChannel.Open(path, readOnly: false, locks: locks)) { channel.BeginTransaction(); - int firstPage = channel.AllocatePage(); - int secondPage = channel.AllocatePage(); + int firstPage = channel.Allocator.Append(); + int secondPage = channel.Allocator.Append(); WriteMarker(channel, firstPage, 0x31); WriteMarker(channel, secondPage, 0x32); @@ -187,4 +187,4 @@ public void EnterExclusive(int page) } public void ExitExclusive(int page) { } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/UsageMapTests.cs b/test/LibRed.Core.Tests/UsageMapTests.cs index c0ed936b..64028321 100644 --- a/test/LibRed.Core.Tests/UsageMapTests.cs +++ b/test/LibRed.Core.Tests/UsageMapTests.cs @@ -1,7 +1,8 @@ using LibRed; +using LibRed.Formats; using LibRed.IO; using LibRed.Pages; -using System.Buffers.Binary; +using LibRed.Storage; using Xunit; namespace LibRed.Core.Tests; @@ -41,18 +42,15 @@ public void Reference_usage_map_rejects_a_record_larger_than_the_format_shape() using var db = JetDatabase.Open(path, readOnly: false); var table = db.OpenTable("MSysObjects"); PageBuffer tdef = table.Channel.ReadPage(table.Definition.DefinitionPage); - int mapRow = tdef.ReadByte(db.Format.TdefOwnedPagesOffset); - int mapPage = tdef.ReadInt24(db.Format.TdefOwnedPagesOffset + 1); + (int mapRow, int mapPage) = tdef.ReadRecordPointer(db.Format.TdefOwnedPagesOffset); Assert.Equal(0, mapRow); // row 0 ends at the page boundary, making its length deterministic byte[] page = table.Channel.ReadPage(mapPage).Span.ToArray(); - int originalOffset = BinaryPrimitives.ReadUInt16LittleEndian( - page.AsSpan(db.Format.DataRowDirectoryOffset, 2)) & 0x1FFF; - int oversizedOffset = originalOffset - 4; - BinaryPrimitives.WriteUInt16LittleEndian( - page.AsSpan(db.Format.DataRowDirectoryOffset, 2), (ushort)oversizedOffset); + int originalOffset = DataPage.ReadSlot(page, db.Format, 0).Offset; + int oversizedOffset = originalOffset - sizeof(int); + DataPage.WriteSlot(page, db.Format, 0, oversizedOffset, RowSlotFlags.None); page.AsSpan(oversizedOffset, db.Format.PageSize - oversizedOffset).Clear(); - page[oversizedOffset] = 0x01; + page[oversizedOffset] = (byte)UsageMapType.Reference; table.Channel.WritePage(mapPage, page); Assert.Throws(() => table.UsageMap.DataPages().ToList()); @@ -69,13 +67,11 @@ public void Exact_empty_reference_usage_map_remains_valid() using var db = JetDatabase.Open(path, readOnly: false); var table = db.OpenTable("MSysObjects"); PageBuffer tdef = table.Channel.ReadPage(table.Definition.DefinitionPage); - int mapPage = tdef.ReadInt24(db.Format.TdefOwnedPagesOffset + 1); + int mapPage = tdef.ReadRecordPointer(db.Format.TdefOwnedPagesOffset).Page; byte[] page = table.Channel.ReadPage(mapPage).Span.ToArray(); - int offset = BinaryPrimitives.ReadUInt16LittleEndian( - page.AsSpan(db.Format.DataRowDirectoryOffset, 2)) & 0x1FFF; - Assert.Equal(69, db.Format.PageSize - offset); - page.AsSpan(offset, 69).Clear(); - page[offset] = 0x01; + int offset = DataPage.ReadSlot(page, db.Format, 0).Offset; + Assert.Equal(db.Format.UsageMapReferenceRecordSize, db.Format.PageSize - offset); + UsageMap.NewReferenceRecord(db.Format).CopyTo(page, offset); table.Channel.WritePage(mapPage, page); Assert.Empty(table.UsageMap.DataPages()); @@ -92,17 +88,16 @@ public void Reference_usage_map_rejects_a_pointer_to_a_non_bitmap_page() using var db = JetDatabase.Open(path, readOnly: false); var table = db.OpenTable("MSysObjects"); PageBuffer tdef = table.Channel.ReadPage(table.Definition.DefinitionPage); - int mapPage = tdef.ReadInt24(db.Format.TdefOwnedPagesOffset + 1); + int mapPage = tdef.ReadRecordPointer(db.Format.TdefOwnedPagesOffset).Page; byte[] page = table.Channel.ReadPage(mapPage).Span.ToArray(); - int offset = BinaryPrimitives.ReadUInt16LittleEndian( - page.AsSpan(db.Format.DataRowDirectoryOffset, 2)) & 0x1FFF; - page.AsSpan(offset, 69).Clear(); - page[offset] = 0x01; - BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(offset + 1, 4), mapPage); // data page, not 0x05 + int offset = DataPage.ReadSlot(page, db.Format, 0).Offset; + Span record = page.AsSpan(offset, db.Format.UsageMapReferenceRecordSize); + UsageMap.NewReferenceRecord(db.Format).CopyTo(record); + UsageMap.WriteReferencePointer(record, 0, db.Format, mapPage); // data page, not 0x05 table.Channel.WritePage(mapPage, page); Assert.Throws(() => table.UsageMap.DataPages().ToList()); } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Core.Tests/VersionByteDetectTests.cs b/test/LibRed.Core.Tests/VersionByteDetectTests.cs index 27d7d403..0d51043b 100644 --- a/test/LibRed.Core.Tests/VersionByteDetectTests.cs +++ b/test/LibRed.Core.Tests/VersionByteDetectTests.cs @@ -87,7 +87,7 @@ public void A_future_version_byte_survives_a_rollback() string path = TemporaryDatabase.CreatePath("future-version-"); try { - Storage.DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); byte[] file = File.ReadAllBytes(path); file[JetFormatBase.VersionOffset] = 0x07; // an ACE this build has never heard of File.WriteAllBytes(path, file); @@ -114,7 +114,7 @@ public void A_raise_by_another_handle_is_picked_up() string path = TemporaryDatabase.CreatePath("raise-other-handle-"); try { - Storage.DatabaseCreator.CreateEmpty(path, version: 0x02); // ACE 12 + JetDatabase.Create(path, version: 0x02); // ACE 12 using var watcher = JetDatabase.Open(path, readOnly: false); Assert.Equal(JetVersion.Version12_2007, watcher.Format.Version); @@ -131,4 +131,4 @@ public void A_raise_by_another_handle_is_picked_up() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.EFCore.Tests/RoundTripTests.cs b/test/LibRed.EFCore.Tests/RoundTripTests.cs index 4a4035e7..9ad59ffc 100644 --- a/test/LibRed.EFCore.Tests/RoundTripTests.cs +++ b/test/LibRed.EFCore.Tests/RoundTripTests.cs @@ -124,7 +124,7 @@ await Assert.ThrowsAsync( [Fact] public void EnsureCreated_creates_a_new_database_natively_and_round_trips() { - // A brand-new file (no copy): EnsureCreated must create the .accdb natively (DatabaseCreator), + // A brand-new file (no copy): EnsureCreated must create the .accdb natively (JetDatabase), // then create the model's schema, then the context is usable for insert + query. string path = Path.Combine(Path.GetTempPath(), $"libred-newdb-{Guid.NewGuid():N}.accdb"); var options = new DbContextOptionsBuilder() @@ -197,4 +197,4 @@ public void Find_by_key_round_trips() var c = context.Customers.Single(x => x.CustomerID == "AROUT"); Assert.Equal("Around the Horn", c.CompanyName); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/AceCoreApiShapeProbeTests.cs b/test/LibRed.Engine.AccessTests/AceCoreApiShapeProbeTests.cs index 87b43ec7..bf49f4cf 100644 --- a/test/LibRed.Engine.AccessTests/AceCoreApiShapeProbeTests.cs +++ b/test/LibRed.Engine.AccessTests/AceCoreApiShapeProbeTests.cs @@ -10,7 +10,7 @@ namespace LibRed.Engine.Tests; // column it builds has already passed AccessTypeMapper — where the width, precision and scale caps live. This // goes in the other door, the one JetVersion.cs warns about: a ColumnSpec straight to the writer. // -// TdefBuilder already validates column count, names, ids, the 510-byte field cap and the widest record, and +// TableDefinition already validates column count, names, ids, the 510-byte field cap and the widest record, and // EnsureStorable covers version-gated types. This asks what is left OVER those guards. // // The contract is NOT "every shape must be accepted" — it is that whether LibRed accepts or refuses the spec, @@ -79,7 +79,7 @@ private sealed record Shape(string Why, Action Apply, JetVersion Ve private static readonly Dictionary Shapes = new() { // ---- Precision and scale, which nothing checked until these shapes found it: the width check covers - // only the fixed 17 bytes. TdefBuilder.ValidateNumericPrecision now refuses a wrong pair, and + // only the fixed 17 bytes. TableDefinition.ValidateNumericPrecision now refuses a wrong pair, and // EffectivePrecision resolves a declared 0 (which means "unspecified") to ACE's 18. ["decimal-precision-zero"] = new("FixedPoint declaring precision 0 — no digits at all", db => Numeric(db, precision: 0, scale: 0)), @@ -157,7 +157,7 @@ private sealed record Shape(string Why, Action Apply, JetVersion Ve table.Insert([null, 2]); }), - // ---- Undocumented descriptor flags Access sets on ITS OWN catalog columns. TdefBuilder's own comment + // ---- Undocumented descriptor flags Access sets on ITS OWN catalog columns. TableDefinition's own comment // says user-table columns leave these clear; nothing stops a caller setting them. ["system-flags-on-user-column"] = new("SystemFlags 0x10/0x20 (system-catalog, security-id) on a user column", db => db.CreateTable("S", @@ -252,4 +252,4 @@ private static void Numeric(JetDatabase db, byte precision, byte scale) ]); db.OpenTable("S").Insert([1.5m]); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/AceValidityLadder.cs b/test/LibRed.Engine.AccessTests/AceValidityLadder.cs index 315b7696..5de4f20a 100644 --- a/test/LibRed.Engine.AccessTests/AceValidityLadder.cs +++ b/test/LibRed.Engine.AccessTests/AceValidityLadder.cs @@ -16,7 +16,7 @@ namespace LibRed.Engine.Tests; /// ACE inserts, updates and deletes through its own allocator and index maintenance /// LibRed reads the file back after ACE has written to it /// -/// Rung 4 earns its place: ACE writing means ACE trusting page 1's free map enough to allocate against it, and +/// Rung 4 earns its place: ACE writing means ACE trusting the global free map enough to allocate against it, and /// a file can pass 1-3 on a free map that is quietly wrong. /// /// Shared so both arms judge by one standard — a finding from either has to be comparable. @@ -163,7 +163,7 @@ private static bool ReadsAsText(OleDbConnection connection, string table) try { using var db = JetDatabase.Open(path, readOnly: true); - foreach (TableDef table in db.Catalog.UserTables) + foreach (TableDefinition table in db.Catalog.UserTables) foreach (object?[] row in db.OpenTable(table.Name).Rows()) _ = row.Length; return null; @@ -183,4 +183,4 @@ private static void Execute(OleDbConnection connection, string sql) public static string Flatten(string message) => string.Join(' ', message.Split('\n', '\r').Select(l => l.Trim()).Where(l => l.Length > 0)); -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/AceWriteValiditySweepProbeTests.cs b/test/LibRed.Engine.AccessTests/AceWriteValiditySweepProbeTests.cs index b57fc0b8..1d182f3d 100644 --- a/test/LibRed.Engine.AccessTests/AceWriteValiditySweepProbeTests.cs +++ b/test/LibRed.Engine.AccessTests/AceWriteValiditySweepProbeTests.cs @@ -116,7 +116,7 @@ public void Ace_opens_an_empty_database_at_every_format_libred_creates(JetVersio LibRedConnection.CreateDatabase($"Data Source={path}", collation: null, version: version); using var stream = File.OpenRead(path); - stream.Seek(0x14, SeekOrigin.Begin); + stream.Seek(JetFormatBase.VersionOffset, SeekOrigin.Begin); output.WriteLine($"{version}: version byte 0x{stream.ReadByte():X2}"); stream.Dispose(); @@ -150,7 +150,7 @@ public void Libred_refuses_to_create_an_ace_15_database() /// The measurement the creation guard rests on: ACE does not merely avoid the 0x04 version byte, /// it REFUSES a file carrying one, and restamping 0x14 to 0x03 makes the identical bytes open. The spec /// previously inferred "reserved" from absence; this is the stronger fact. - /// Built at 2010 and stamped by hand, since DatabaseCreator now refuses to write 0x04 — the + /// Built at 2010 and stamped by hand, since JetDatabase now refuses to write 0x04 — the /// evidence must not depend on the guard being absent. [Fact] public void Ace_refuses_the_0x04_version_byte_and_nothing_else_about_the_file() @@ -180,7 +180,7 @@ public void Ace_refuses_the_0x04_version_byte_and_nothing_else_about_the_file() private static void Stamp(string path, byte version) { using var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite); - stream.Seek(0x14, SeekOrigin.Begin); + stream.Seek(JetFormatBase.VersionOffset, SeekOrigin.Begin); stream.WriteByte(version); } @@ -632,4 +632,4 @@ private static string Summarise(string sql, Dictionary? paramet _ => value.ToString() ?? "", }; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/BigIntCreatedDatabaseAccessTests.cs b/test/LibRed.Engine.AccessTests/BigIntCreatedDatabaseAccessTests.cs index c13c210c..822cd7dc 100644 --- a/test/LibRed.Engine.AccessTests/BigIntCreatedDatabaseAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/BigIntCreatedDatabaseAccessTests.cs @@ -41,10 +41,10 @@ public void Ace_reads_bigint_values_libred_wrote_into_a_database_libred_upgraded try { LibRedConnection.CreateDatabase($"Data Source={path}"); // the ACE 12 default, which cannot hold it - Assert.Equal(0x02, VersionByte(path)); using (var db = JetDatabase.Open(path, readOnly: false)) { + Assert.Equal(JetVersion.Version12_2007, db.Format.Version); var engine = new QueryEngine(db); engine.ExecuteNonQuery("CREATE TABLE `B` (`Id` INTEGER PRIMARY KEY, `V` BIGINT NULL)"); Assert.Equal(JetVersion.Version16_2016, db.Format.Version); @@ -54,7 +54,8 @@ public void Ace_reads_bigint_values_libred_wrote_into_a_database_libred_upgraded new Dictionary { ["id"] = i, ["v"] = values[i] }); } - Assert.Equal(0x05, VersionByte(path)); + using (var reopened = JetDatabase.Open(path)) + Assert.Equal(JetVersion.Version16_2016, reopened.Format.Version); using var connection = AceTestDatabase.Open(path); using var command = connection.CreateCommand(); @@ -67,11 +68,4 @@ public void Ace_reads_bigint_values_libred_wrote_into_a_database_libred_upgraded } finally { TemporaryDatabase.Delete(path); } } - - private static byte VersionByte(string path) - { - using var stream = File.OpenRead(path); - stream.Seek(0x14, SeekOrigin.Begin); - return (byte)stream.ReadByte(); - } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/CalculatedColumnSqlAccessTests.cs b/test/LibRed.Engine.AccessTests/CalculatedColumnSqlAccessTests.cs index 0301e682..eca63a32 100644 --- a/test/LibRed.Engine.AccessTests/CalculatedColumnSqlAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/CalculatedColumnSqlAccessTests.cs @@ -19,7 +19,7 @@ public void Creates_and_adds_calculated_columns_ace_can_read() string path = TemporaryDatabase.CreatePath("calc-sql-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using (var database = JetDatabase.Open(path, readOnly: false)) { var engine = new QueryEngine(database); @@ -58,7 +58,7 @@ public void Accepts_the_sql_ef_core_emits_for_a_computed_column() string path = TemporaryDatabase.CreatePath("calc-efsql-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using (var database = JetDatabase.Open(path, readOnly: false)) { var engine = new QueryEngine(database); @@ -98,7 +98,7 @@ public void Normalises_identifier_quoting_in_a_check_constraint() string path = TemporaryDatabase.CreatePath("check-tick-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using (var database = JetDatabase.Open(path, readOnly: false)) new QueryEngine(database).ExecuteNonQuery( "CREATE TABLE `T` (`Id` integer, `Qty` integer, CONSTRAINT `ck` CHECK (`Qty` > 0))"); @@ -142,7 +142,7 @@ public void Refuses_an_index_over_a_calculated_column(string sql) string path = TemporaryDatabase.CreatePath("calc-index-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var database = JetDatabase.Open(path, readOnly: false); var engine = new QueryEngine(database); engine.ExecuteNonQuery("CREATE TABLE T (Id integer, Qty integer, C integer AS ([Qty]*2))"); @@ -164,7 +164,7 @@ public void Refuses_a_key_over_a_calculated_column(string sql) string path = TemporaryDatabase.CreatePath("calc-key-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var database = JetDatabase.Open(path, readOnly: false); var ex = Assert.ThrowsAny(() => { new QueryEngine(database).ExecuteNonQuery(sql); }); output.WriteLine($" {ex.Message}"); @@ -188,7 +188,7 @@ public void Refuses_a_calculated_column_that_cannot_mean_what_it_says(string sql string path = TemporaryDatabase.CreatePath("calc-sqlbad-"); try { - DatabaseCreator.CreateEmpty(path); + JetDatabase.Create(path); using var database = JetDatabase.Open(path, readOnly: false); var engine = new QueryEngine(database); var ex = Assert.ThrowsAny(() => { engine.ExecuteNonQuery(sql); }); @@ -197,4 +197,4 @@ public void Refuses_a_calculated_column_that_cannot_mean_what_it_says(string sql } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/CatalogRowParityAccessTests.cs b/test/LibRed.Engine.AccessTests/CatalogRowParityAccessTests.cs index 7e3cce6b..bb6f4a06 100644 --- a/test/LibRed.Engine.AccessTests/CatalogRowParityAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/CatalogRowParityAccessTests.cs @@ -119,7 +119,7 @@ private static string Describe(string sql, Action run) run(path, sql); using var database = JetDatabase.Open(path, readOnly: true); - TableDef objects = database.Catalog.FindTable("MSysObjects")!; + TableDefinition objects = database.Catalog.FindTable("MSysObjects")!; int Col(string n) => objects.Columns.Single(c => c.Name == n).Index; int name = Col("Name"), id = Col("Id"), parent = Col("ParentId"), type = Col("Type"); @@ -131,7 +131,7 @@ private static string Describe(string sql, Action run) // The owner and the permission rows ARE comparable: both engines write into a copy of one file, so // the per-file SID mask (page-00 §2.3) is the same for both and the masked account SIDs must be too. - TableDef aces = database.Catalog.FindTable("MSysACEs")!; + TableDefinition aces = database.Catalog.FindTable("MSysACEs")!; int aceObject = aces.Columns.Single(c => c.Name == "ObjectId").Index; int aceSid = aces.Columns.Single(c => c.Name == "SID").Index; int aceAcm = aces.Columns.Single(c => c.Name == "ACM").Index; @@ -155,4 +155,4 @@ private static string Describe(string sql, Action run) byte[] b => b.Length <= 8 ? Convert.ToHexString(b) : $"byte[{b.Length}]", _ => value.ToString() ?? "", }; -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/ColumnDescriptorByteParityAccessTests.cs b/test/LibRed.Engine.AccessTests/ColumnDescriptorByteParityAccessTests.cs index b5c5d443..aa831f91 100644 --- a/test/LibRed.Engine.AccessTests/ColumnDescriptorByteParityAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/ColumnDescriptorByteParityAccessTests.cs @@ -1,8 +1,4 @@ -using System.Buffers.Binary; using System.Data.OleDb; -using LibRed.Catalog; -using LibRed.Formats; -using LibRed.IO; using Xunit; namespace LibRed.Engine.Tests; @@ -15,7 +11,7 @@ namespace LibRed.Engine.Tests; // // It found two, both of them beliefs rather than oversights: // -// 0x09, the repeated column id, on EVERY column of every type. TdefBuilder wrote zero, with a comment +// 0x09, the repeated column id, on EVERY column of every type. TableDefinition wrote zero, with a comment // saying real files store zero there - read off the system tables, where they do. Every genuine user // table in every fixture carries the id, and so does everything that creates one: ACE's SQL DDL, DAO's // object model, and DAO-executed SQL. Compaction preserves whatever is there, so it is set at creation. @@ -67,26 +63,13 @@ public void Descriptor_bytes_match_ace(string declaration) private static string Copy(string prefix) => TemporaryDatabase.CopyPath( Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), prefix); - /// The descriptor bytes for column "V" — the second column of table W. A two-column table's - /// definition fits one page, so the page is sliced directly rather than stitching a TDEF chain. + /// The descriptor bytes for column "V" of table W, as read from its definition. private static byte[]? Descriptor(string path, Action create) { try { create(path); } catch { return null; } - int definitionPage; - using (var database = JetDatabase.Open(path, readOnly: true)) - { - TableDef? table = database.Catalog.FindTable("W"); - if (table is null) return null; - definitionPage = table.DefinitionPage; - } - - using var channel = PageChannel.Open(path, readOnly: true); - JetFormatBase format = channel.Format; - byte[] page = channel.ReadPage(definitionPage).Span.ToArray(); - int dataCount = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(format.TdefIndexCountOffset, 4)); - int start = format.TdefRealIndexBlockOffset + dataCount * format.RealIndexEntrySize; - return page.AsSpan(start + format.ColumnDescriptorSize, format.ColumnDescriptorSize).ToArray(); + using var database = JetDatabase.Open(path, readOnly: true); + return database.Catalog.FindTable("W")?.Columns.Single(c => c.Name == "V").RawDescriptor; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/ColumnLengthAccessTests.cs b/test/LibRed.Engine.AccessTests/ColumnLengthAccessTests.cs index 35432d18..60107cab 100644 --- a/test/LibRed.Engine.AccessTests/ColumnLengthAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/ColumnLengthAccessTests.cs @@ -4,7 +4,7 @@ namespace LibRed.Engine.Tests; // ACE rejects a value longer than a variable column's declared width — it neither stores nor clips it. -// RowEncoder.EnsureFitsDeclaredLength matches that; these are the measurements it holds to. +// RowCodec.EnsureFitsDeclaredLength matches that; these are the measurements it holds to. // // The text case uses a literal, not a parameter: parameter Size has its own clipping rule // (ParameterSizeAccessTests) and would confound the answer. @@ -54,7 +54,7 @@ public void Ace_variable_text_column_versus_an_overlong_value() Assert.True( outcome.StartsWith("rejected", StringComparison.Ordinal), $"ACE no longer rejects six characters in a TEXT(5) column - it {outcome}. " - + "RowEncoder.EnsureFitsDeclaredLength should then stop rejecting them too."); + + "RowCodec.EnsureFitsDeclaredLength should then stop rejecting them too."); // The wording LibRed's own rejection is modelled on. Assert.Contains("too small", outcome, StringComparison.OrdinalIgnoreCase); @@ -155,4 +155,4 @@ public void Ace_variable_binary_column_versus_an_overlong_value() $"ACE no longer rejects six bytes in a VARBINARY(5) column - it {overlong}, where five gave " + $"'{control}'. Text is still rejected, so the check would need splitting per column kind."); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/ColumnNumberingAccessTests.cs b/test/LibRed.Engine.AccessTests/ColumnNumberingAccessTests.cs index 3726da29..5a9e9e19 100644 --- a/test/LibRed.Engine.AccessTests/ColumnNumberingAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/ColumnNumberingAccessTests.cs @@ -146,8 +146,9 @@ private static string Describe(string path) return string.Join(" ", db.Catalog.FindTable("T")!.Columns.Select(c => { byte[] d = c.RawDescriptor!; - return $"{c.Name}:{BinaryPrimitives.ReadUInt16LittleEndian(d.AsSpan(5))}/" - + $"{BinaryPrimitives.ReadUInt16LittleEndian(d.AsSpan(7))}/{BinaryPrimitives.ReadUInt16LittleEndian(d.AsSpan(9))}"; + return $"{c.Name}:{c.ColumnId}/" + + $"{c.VariableTableIndex}/" + + $"{BinaryPrimitives.ReadUInt16LittleEndian(d.AsSpan(db.Format.ColumnSecondaryNumberOffset))}"; })); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs b/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs index 158b8e02..4023d5d1 100644 --- a/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/ColumnRetypeParityAccessTests.cs @@ -1,7 +1,9 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed; using LibRed.Catalog; +using LibRed.Formats; +using LibRed.IO; +using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -61,18 +63,23 @@ public void A_retype_leaves_the_file_ACE_leaves(string table, string alter) using (OleDbConnection c = AceTestDatabase.Open(acePath)) Exec(c, alter); OleDbConnection.ReleaseObjectPool(); + JetFormatBase format; using (var db = JetDatabase.Open(libredPath, readOnly: false)) + { + format = db.Format; new QueryEngine(db).ExecuteNonQuery(alter); + } Assert.Equal(Describe(acePath, table), Describe(libredPath, table)); byte[] ace = File.ReadAllBytes(acePath), libred = File.ReadAllBytes(libredPath); Assert.Equal(ace.Length, libred.Length); (int catalogPage, int usageMapPage, int lowestRecord) = Landmarks(acePath, table); - for (int page = 1; page < ace.Length / 4096; page++) + int pageSize = format.PageSize; + for (int page = 1; page < ace.Length / pageSize; page++) { - var differ = Enumerable.Range(0, 4096).Where(i => ace[page * 4096 + i] != libred[page * 4096 + i]).ToList(); - if (differ.Count == 0 || BinaryPrimitives.ReadInt32LittleEndian(ace.AsSpan(page * 4096 + 4)) == catalogPage) continue; + var differ = Enumerable.Range(0, pageSize).Where(i => ace[page * pageSize + i] != libred[page * pageSize + i]).ToList(); + if (differ.Count == 0 || (int)DataPage.ReadOwner(ace.AsSpan(page * pageSize, pageSize), format) == catalogPage) continue; Assert.True(page == usageMapPage && differ.All(i => i < lowestRecord), $"page {page} differs at 0x{differ[0]:X3} ({differ.Count} bytes)"); } @@ -90,7 +97,7 @@ private static string Describe(string path, string tableName) { var lines = new List(); using var db = JetDatabase.Open(path, readOnly: true); - TableDef t = db.Catalog.FindTable(tableName)!; + TableDefinition t = db.Catalog.FindTable(tableName)!; var tdef = db.ReadTableDefinition(t.DefinitionPage); lines.AddRange(t.Columns.Select(c => $"{c.Name} {c.Type} id {c.ColumnId} {Convert.ToHexString(c.RawDescriptor ?? [])}")); lines.AddRange(tdef.LongValueOwnedMaps.Select(m => $"maps {m.Key}: {m.Value} {tdef.LongValueFreeMaps[m.Key]}")); @@ -106,13 +113,14 @@ private static string Describe(string path, string tableName) private static (int CatalogPage, int UsageMapPage, int LowestRecord) Landmarks(string path, string tableName) { using var db = JetDatabase.Open(path, readOnly: true); - TableDef t = db.Catalog.FindTable(tableName)!; + TableDefinition t = db.Catalog.FindTable(tableName)!; byte[] file = File.ReadAllBytes(path); - int holder = BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(t.DefinitionPage * 4096 + db.Format.TdefOwnedPagesOffset)) >> 8; - int rows = BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(holder * 4096 + db.Format.DataRowCountOffset)); + int pageSize = db.Format.PageSize; + int holder = PageBuffer.ReadRecordPointer(file, t.DefinitionPage * pageSize + db.Format.TdefOwnedPagesOffset).Page; + int rows = DataPage.ReadRowCount(file.AsSpan(holder * pageSize, pageSize), db.Format); int lowest = Enumerable.Range(0, rows) - .Select(i => BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(holder * 4096 + db.Format.DataRowDirectoryOffset + i * 2))) - .Where(s => (s & 0x8000) == 0).Min(s => s & 0x1FFF); + .Select(i => DataPage.ReadSlot(file.AsSpan(holder * pageSize, pageSize), db.Format, i)) + .Where(s => (s.Flags & RowSlotFlags.Deleted) == 0).Min(s => s.Offset); return (db.Catalog.FindTable("MSysObjects")!.DefinitionPage, holder, lowest); } @@ -122,4 +130,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/ComplexAttachmentWriteAccessTests.cs b/test/LibRed.Engine.AccessTests/ComplexAttachmentWriteAccessTests.cs index c1baff6b..d0672812 100644 --- a/test/LibRed.Engine.AccessTests/ComplexAttachmentWriteAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/ComplexAttachmentWriteAccessTests.cs @@ -41,7 +41,7 @@ public void Ace_reads_an_attachment_libred_wrote(string fileName, bool expectCom ComplexColumn column = db.Catalog.ComplexColumns .First(c => c.OwnerTable.Name == "Table1" && c.IsAttachment); columnName = column.ColumnName; - TableDef owner = column.OwnerTable; + TableDefinition owner = column.OwnerTable; ColumnDef inRow = owner.FindColumn(column.ColumnName)!; object?[] record = db.OpenTable("Table1").Rows().First(); @@ -141,4 +141,4 @@ private static object Fields(object target, string name) => progId = "(none)"; return null; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/ComplexDeleteByteParityProbeTest.cs b/test/LibRed.Engine.AccessTests/ComplexDeleteByteParityProbeTest.cs index 32349a90..31ce22a1 100644 --- a/test/LibRed.Engine.AccessTests/ComplexDeleteByteParityProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/ComplexDeleteByteParityProbeTest.cs @@ -2,6 +2,9 @@ using System.Reflection; using LibRed; using LibRed.Catalog; +using LibRed.Formats; +using LibRed.Pages; +using LibRed.Storage; using Xunit; namespace LibRed.Engine.Tests; @@ -31,7 +34,7 @@ public void Probe_byte_parity_of_a_complex_delete() string where; using (var db = JetDatabase.Open(TemporaryDatabase.CopyPath(Source, "cx-parity-pick-"))) { - TableDef table = db.Catalog.FindTable("Table1")!; + TableDefinition table = db.Catalog.FindTable("Table1")!; ComplexColumn any = db.Catalog.ComplexColumns.First(c => c.OwnerTable.Name == "Table1"); int recordId = db.OpenTable(any.FlatTable.Name).Rows() .Select(r => Convert.ToInt32(r[any.OwnerLink.Index])).Distinct().First(); @@ -80,21 +83,28 @@ public void Probe_entry_counts_on_insert() new QueryEngine(d).ExecuteNonQuery(Insert); int page; - using (var d = JetDatabase.Open(orig)) page = d.Catalog.FindTable("Order Details")!.DefinitionPage; + JetFormatBase format; + using (var d = JetDatabase.Open(orig)) + { + page = d.Catalog.FindTable("Order Details")!.DefinitionPage; + format = d.Format; + } byte[] o = File.ReadAllBytes(orig), a = File.ReadAllBytes(aceCopy), l = File.ReadAllBytes(libredCopy); - const int PageSize = 4096; - var so = o.AsSpan(page * PageSize, PageSize); - var sa = a.AsSpan(page * PageSize, PageSize); - var sl = l.AsSpan(page * PageSize, PageSize); - - output.WriteLine($"PROBE rowCount(0x10) orig={I(so, 0x10)} ace={I(sa, 0x10)} libred={I(sl, 0x10)}"); - int realIndexes = I(so, 0x33); + int pageSize = format.PageSize; + var so = o.AsSpan(page * pageSize, pageSize); + var sa = a.AsSpan(page * pageSize, pageSize); + var sl = l.AsSpan(page * pageSize, pageSize); + + int rowCount = format.TdefRowCountOffset; + output.WriteLine($"PROBE rowCount(0x10) orig={I(so, rowCount)} ace={I(sa, rowCount)} libred={I(sl, rowCount)}"); + int realIndexes = I(so, format.TdefIndexCountOffset); for (int i = 0; i < realIndexes; i++) { - int at = 0x3F + i * 12; - output.WriteLine($"PROBE index {i} @0x{at:X2}: total orig={I(so, at)} ace={I(sa, at)} libred={I(sl, at)}" - + $" | unique orig={I(so, at + 4)} ace={I(sa, at + 4)} libred={I(sl, at + 4)}"); + var (origCounts, aceCounts, libredCounts) = (TableDefinition.ReadIndexCounts(so, format, i), + TableDefinition.ReadIndexCounts(sa, format, i), TableDefinition.ReadIndexCounts(sl, format, i)); + output.WriteLine($"PROBE index {i}: total orig={origCounts.Total} ace={aceCounts.Total} libred={libredCounts.Total}" + + $" | unique orig={origCounts.Unique} ace={aceCounts.Unique} libred={libredCounts.Unique}"); } } finally @@ -164,7 +174,7 @@ private static Dictionary RowCounts(string path) { using var db = JetDatabase.Open(path); var counts = new Dictionary(StringComparer.Ordinal); - foreach (TableDef t in db.Catalog.Tables) + foreach (TableDefinition t in db.Catalog.Tables) { try { counts[t.Name] = db.OpenTable(t.Name).Rows().Count(); } catch (Exception ex) { counts[t.Name] = -1; Console.WriteLine($"{t.Name}: {ex.Message}"); } @@ -246,28 +256,32 @@ private static Dictionary Owners(string path) { using var db = JetDatabase.Open(path); var map = new Dictionary(); - foreach (TableDef t in db.Catalog.Tables) map[t.DefinitionPage] = t.Name; + foreach (TableDefinition t in db.Catalog.Tables) map[t.DefinitionPage] = t.Name; return map; } - private static string Describe(byte[] file, int page, Dictionary owners) + private static string Describe(byte[] file, int page, Dictionary owners, JetFormatBase format) { - const int PageSize = 4096; - if ((page + 1) * PageSize > file.Length) return "(absent)"; - var span = file.AsSpan(page * PageSize, PageSize); - byte type = span[0]; + int pageSize = format.PageSize; + if ((page + 1) * pageSize > file.Length) return "(absent)"; + var span = file.AsSpan(page * pageSize, pageSize); + PageType type = PageHeader.ReadType(span); string kind = type switch { - 0x00 => "db-header", 0x01 => "data", 0x02 => "TDEF", 0x03 => "index-node", - 0x04 => "index-leaf", 0x05 => "usage-map", 0x08 => "long-value", _ => $"?0x{type:X2}", + PageType.DatabaseDefinition => "db-header", PageType.DataPage => "data", PageType.TableDefinition => "TDEF", + PageType.IntermediateIndexPage => "index-node", PageType.LeafIndexPage => "index-leaf", + PageType.PageUsageBitmap => "usage-bitmap", PageType.ReleasedTableDefinition => "released-TDEF", + PageType.ReleasedDataPage => "released-data", _ => $"?0x{(ushort)type:X4}", }; - if (type is 0x01 or 0x03 or 0x04) + if (type is PageType.DataPage or PageType.IntermediateIndexPage or PageType.LeafIndexPage) { - int tdef = BinaryPrimitives.ReadInt32LittleEndian(span[0x04..]); + int tdef = type == PageType.DataPage + ? (int)DataPage.ReadOwner(span, format) + : IndexTree.ReadOwner(span, format); string name = owners.TryGetValue(tdef, out string? t) ? t : $"tdef@{tdef}"; return $"{kind} of [{name}]"; } - if (type == 0x02 && owners.TryGetValue(page, out string? own)) return $"{kind} [{own}]"; + if (type == PageType.TableDefinition && owners.TryGetValue(page, out string? own)) return $"{kind} [{own}]"; if (span.TrimStart((byte)0).IsEmpty) return "all-zero"; return kind; } @@ -275,12 +289,13 @@ private static string Describe(byte[] file, int page, Dictionary ow private void Report(string label, byte[] o, byte[] a, byte[] n, byte[] l, Dictionary owners) { { + JetFormatBase format = JetFormatBase.Detect(new MemoryStream(o)); + int pageSize = format.PageSize; output.WriteLine($"PROBE [{label}] sizes: orig={o.Length} ace={a.Length} noise={n.Length} libred={l.Length}" - + $" (pages: orig={o.Length / 4096} ace={a.Length / 4096} libred={l.Length / 4096})"); + + $" (pages: orig={o.Length / pageSize} ace={a.Length / pageSize} libred={l.Length / pageSize})"); - const int PageSize = 4096; - HashSet acePages = ChangedPages(o, a, PageSize), noisePages = ChangedPages(o, n, PageSize), - libredPages = ChangedPages(o, l, PageSize); + HashSet acePages = ChangedPages(o, a, pageSize), noisePages = ChangedPages(o, n, pageSize), + libredPages = ChangedPages(o, l, pageSize); output.WriteLine($"PROBE pages changed: ace={acePages.Count} noise={noisePages.Count} libred={libredPages.Count}"); output.WriteLine($"PROBE ace-only (minus housekeeping): [{Join(acePages.Except(noisePages))}]"); @@ -294,39 +309,41 @@ private void Report(string label, byte[] o, byte[] a, byte[] n, byte[] l, Dictio // What every page in play actually is, on each side — a page number alone says nothing. output.WriteLine("PROBE page inventory (orig -> ace / libred):"); foreach (int page in aceReal.Union(libredPages).Order()) - output.WriteLine($"PROBE {page,5}: was {Describe(o, page, owners),-28}" - + $" ace={Describe(a, page, owners),-28} libred={Describe(l, page, owners)}"); + output.WriteLine($"PROBE {page,5}: was {Describe(o, page, owners, format),-28}" + + $" ace={Describe(a, page, owners, format),-28} libred={Describe(l, page, owners, format)}"); // For the pages both changed, are the resulting bytes identical — and where not, what exactly? foreach (int page in aceReal.Intersect(libredPages).Order()) { - var origSpan = o.AsSpan(page * PageSize, PageSize); - var aceSpan = a.AsSpan(page * PageSize, PageSize); - var libSpan = l.AsSpan(page * PageSize, PageSize); + var origSpan = o.AsSpan(page * pageSize, pageSize); + var aceSpan = a.AsSpan(page * pageSize, pageSize); + var libSpan = l.AsSpan(page * pageSize, pageSize); if (aceSpan.SequenceEqual(libSpan)) { output.WriteLine($"PROBE page {page}: IDENTICAL"); continue; } // Branch on what the page BECAME, not what it was: a recycled page changes type, and the // interesting structure is the new one. - byte type = aceSpan[0]; - output.WriteLine($"PROBE page {page}: was=0x{origSpan[0]:X2} now=0x{type:X2}" + PageType type = PageHeader.ReadType(aceSpan); + output.WriteLine($"PROBE page {page}: was=0x{origSpan[0]:X2} now=0x{aceSpan[0]:X2}" + $" ace-vs-libred differs in {Ranges(aceSpan, libSpan).Split(' ').Length} run(s)"); - if (type == 0x02) // TDEF — the per-index entry counters + if (type == PageType.TableDefinition) // TDEF — the per-index entry counters { - int realIndexes = BinaryPrimitives.ReadInt32LittleEndian(origSpan[0x33..]); - output.WriteLine($"PROBE rowCount(0x10) orig={I(origSpan, 0x10)} ace={I(aceSpan, 0x10)} libred={I(libSpan, 0x10)}"); - for (int i = 0; i < realIndexes && 0x3F + i * 12 + 8 <= PageSize; i++) + int realIndexes = I(origSpan, format.TdefIndexCountOffset); + int rowCount = format.TdefRowCountOffset; + output.WriteLine($"PROBE rowCount(0x10) orig={I(origSpan, rowCount)} ace={I(aceSpan, rowCount)} libred={I(libSpan, rowCount)}"); + for (int i = 0; i < realIndexes; i++) { - int at = 0x3F + i * 12; - if (I(origSpan, at) == I(aceSpan, at) && I(origSpan, at + 4) == I(aceSpan, at + 4) - && I(origSpan, at) == I(libSpan, at) && I(origSpan, at + 4) == I(libSpan, at + 4)) continue; - output.WriteLine($"PROBE index {i} @0x{at:X2}: total orig={I(origSpan, at)} ace={I(aceSpan, at)} libred={I(libSpan, at)}" - + $" | unique orig={I(origSpan, at + 4)} ace={I(aceSpan, at + 4)} libred={I(libSpan, at + 4)}"); + var (origCounts, aceCounts, libredCounts) = (TableDefinition.ReadIndexCounts(origSpan, format, i), + TableDefinition.ReadIndexCounts(aceSpan, format, i), TableDefinition.ReadIndexCounts(libSpan, format, i)); + if (origCounts == aceCounts && origCounts == libredCounts) continue; + output.WriteLine($"PROBE index {i}: total orig={origCounts.Total} ace={aceCounts.Total} libred={libredCounts.Total}" + + $" | unique orig={origCounts.Unique} ace={aceCounts.Unique} libred={libredCounts.Unique}"); } } - else if (type is 0x03 or 0x04) // index page — free space, and where each side starts to diverge + else if (type is PageType.IntermediateIndexPage or PageType.LeafIndexPage) // index page — free space, and where each side starts to diverge { - output.WriteLine($"PROBE freeSpace(0x02) orig={U(origSpan, 0x02)} ace={U(aceSpan, 0x02)} libred={U(libSpan, 0x02)}"); + output.WriteLine($"PROBE freeSpace(0x02) orig={IndexTree.ReadFreeSpace(origSpan, format)}" + + $" ace={IndexTree.ReadFreeSpace(aceSpan, format)} libred={IndexTree.ReadFreeSpace(libSpan, format)}"); output.WriteLine($"PROBE first diff vs orig: ace @0x{FirstDiff(origSpan, aceSpan):X3}" + $" libred @0x{FirstDiff(origSpan, libSpan):X3} ace-vs-libred @0x{FirstDiff(aceSpan, libSpan):X3}"); output.WriteLine($"PROBE last diff vs orig: ace @0x{LastDiff(origSpan, aceSpan):X3}" @@ -338,7 +355,8 @@ private void Report(string label, byte[] o, byte[] a, byte[] n, byte[] l, Dictio // The decisive question for tail handling: past each side's own last live byte, does the // page still hold what was there before, or has it been cleared? - int aceEnd = PageSize - U(aceSpan, 0x02), libEnd = PageSize - U(libSpan, 0x02); + int aceEnd = pageSize - IndexTree.ReadFreeSpace(aceSpan, format), + libEnd = pageSize - IndexTree.ReadFreeSpace(libSpan, format); output.WriteLine($"PROBE live ends: ace @0x{aceEnd:X3} libred @0x{libEnd:X3}" + $" tail preserved from orig? ace={origSpan[aceEnd..].SequenceEqual(aceSpan[aceEnd..])}" + $" libred={origSpan[libEnd..].SequenceEqual(libSpan[libEnd..])}" @@ -367,7 +385,6 @@ private static string Hex(ReadOnlySpan page, int at, int count) => Convert.ToHexString(page.Slice(Math.Max(0, at), Math.Min(count, page.Length - Math.Max(0, at)))); private static int I(ReadOnlySpan page, int at) => BinaryPrimitives.ReadInt32LittleEndian(page[at..]); - private static ushort U(ReadOnlySpan page, int at) => BinaryPrimitives.ReadUInt16LittleEndian(page[at..]); /// The byte ranges where two pages differ, as 0xSTART-0xEND(length). private static string Ranges(ReadOnlySpan left, ReadOnlySpan right) @@ -434,4 +451,4 @@ private static void DaoOpenClose(object engine, string path, string table) progId = "(none)"; return null; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/ComplexWriteAceReadbackTests.cs b/test/LibRed.Engine.AccessTests/ComplexWriteAceReadbackTests.cs index 5502c325..acbf9e35 100644 --- a/test/LibRed.Engine.AccessTests/ComplexWriteAceReadbackTests.cs +++ b/test/LibRed.Engine.AccessTests/ComplexWriteAceReadbackTests.cs @@ -145,12 +145,12 @@ private static string Remains(string path, string? table = null) using var db = JetDatabase.Open(path); if (table is not null) { - TableDef owner = db.Catalog.FindTable(table)!; + TableDefinition owner = db.Catalog.FindTable(table)!; string columns = string.Join(",", owner.Columns.Select(c => c.Name)); string indexes = string.Join(",", owner.Indexes.Select(i => i.Name.Split('_')[0]).Order(StringComparer.Ordinal)); return $"columns=[{columns}] indexes=[{indexes}] {Remains(path)}"; } - TableDef complexColumns = db.Catalog.FindTable("MSysComplexColumns")!; + TableDefinition complexColumns = db.Catalog.FindTable("MSysComplexColumns")!; int name = complexColumns.FindColumn("ColumnName")!.Index; var rows = db.OpenTable("MSysComplexColumns").Rows() .Select(r => Convert.ToString(r[name]) ?? "?") @@ -185,7 +185,7 @@ public void Ace_reads_back_a_table_libred_inserted_into_and_deleted_from() using (var db = JetDatabase.Open(path, readOnly: false)) { var engine = new QueryEngine(db); - TableDef table = db.Catalog.FindTable("Table1")!; + TableDefinition table = db.Catalog.FindTable("Table1")!; ColumnDef idColumn = table.Columns[0]; complexColumnCount = db.Catalog.ComplexColumns.Count(c => c.OwnerTable.Name == "Table1"); @@ -219,7 +219,7 @@ public void Ace_reads_back_a_table_libred_inserted_into_and_deleted_from() } // The new row took one complex id, shared by every complex column. - TableDef table = db.Catalog.FindTable("Table1")!; + TableDefinition table = db.Catalog.FindTable("Table1")!; object?[] newest = db.OpenTable("Table1").Rows() .Last(r => !idsBefore.Contains(Convert.ToInt32(r[table.Columns[0].Index]))); var ids = db.Catalog.ComplexColumns.Where(c => c.OwnerTable.Name == "Table1") @@ -247,4 +247,4 @@ public void Ace_reads_back_a_table_libred_inserted_into_and_deleted_from() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/CreateTableUsageMapOrderAccessTests.cs b/test/LibRed.Engine.AccessTests/CreateTableUsageMapOrderAccessTests.cs index 8a50e4b4..81afc610 100644 --- a/test/LibRed.Engine.AccessTests/CreateTableUsageMapOrderAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/CreateTableUsageMapOrderAccessTests.cs @@ -1,7 +1,6 @@ using System.Data.OleDb; using LibRed; using LibRed.Catalog; -using LibRed.Formats; using LibRed.IO; using LibRed.Pages; using Xunit; @@ -76,33 +75,15 @@ public void Usage_map_rows_follow_the_order_the_statement_declares_them(string l private static string MapLayout(string path) { using var channel = PageChannel.Open(path, readOnly: true); - TableDef table = new JetCatalog(channel).FindTable("T")!; - var definition = new TableDefinitionPage(); + TableDefinition table = new JetCatalog(channel).FindTable("T")!; + var definition = new TableDefinition(); definition.Read(channel, table.DefinitionPage); - JetFormatBase format = channel.Format; - PageBuffer tdef = channel.ReadPage(table.DefinitionPage); - int dataCount = tdef.ReadInt32(format.TdefIndexCountOffset); - int columnCount = tdef.ReadUInt16(format.TdefColumnCountOffset); - - // Walk the definition to the index-data blocks: statistics, column descriptors, then the column names. - int at = format.TdefRealIndexBlockOffset + dataCount * format.RealIndexEntrySize - + columnCount * format.ColumnDescriptorSize; - for (int i = 0; i < columnCount; i++) at += 2 + tdef.ReadUInt16(at); - var parts = new List(); - for (int i = 0; i < dataCount; i++) - parts.Add($"index{i}=row{tdef.ReadByte(at + i * IndexBlockFormat.DataBlockSize + UsageMapRowOffset)}"); + foreach (IndexDef index in definition.Indexes.OrderBy(i => i.RealIndexOrdinal)) + parts.Add($"index{index.RealIndexOrdinal}=row{index.UsageMap.Row}"); foreach ((int columnId, (int owned, int _)) in definition.LongValueOwnedMaps.OrderBy(e => e.Key)) parts.Add($"column{columnId}=rows{owned}/{definition.LongValueFreeMaps[columnId].Item1}"); return string.Join(" ", parts); } - - // IndexBlockFormat is internal to Core; this is its UsageMapRowOffset (see TdefByteParityAccessTests). - private const int UsageMapRowOffset = 0x22; - - private static class IndexBlockFormat - { - public const int DataBlockSize = 52; - } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/DateTime2CreatedDatabaseAccessTests.cs b/test/LibRed.Engine.AccessTests/DateTime2CreatedDatabaseAccessTests.cs index b9fb1b8d..b7c2a15f 100644 --- a/test/LibRed.Engine.AccessTests/DateTime2CreatedDatabaseAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/DateTime2CreatedDatabaseAccessTests.cs @@ -44,10 +44,10 @@ public void Ace_opens_a_database_libred_upgraded_in_place_and_reads_the_datetime { // Created at the DEFAULT format — ACE 12, the one that cannot hold the type. LibRedConnection.CreateDatabase($"Data Source={path}"); - Assert.Equal(0x02, VersionByte(path)); using (var db = JetDatabase.Open(path, readOnly: false)) { + Assert.Equal(JetVersion.Version12_2007, db.Format.Version); var engine = new QueryEngine(db); engine.ExecuteNonQuery("CREATE TABLE `E` (`Id` INTEGER PRIMARY KEY, `V` DATETIME2 NULL)"); Assert.Equal(JetVersion.Version17_2019, db.Format.Version); @@ -56,7 +56,8 @@ public void Ace_opens_a_database_libred_upgraded_in_place_and_reads_the_datetime new Dictionary { ["v"] = value }); } - Assert.Equal(0x06, VersionByte(path)); + using (var reopened = JetDatabase.Open(path)) + Assert.Equal(JetVersion.Version17_2019, reopened.Format.Version); using var connection = AceTestDatabase.Open(path); using var command = connection.CreateCommand(); @@ -72,13 +73,6 @@ public void Ace_opens_a_database_libred_upgraded_in_place_and_reads_the_datetime finally { TemporaryDatabase.Delete(path); } } - private static byte VersionByte(string path) - { - using var stream = File.OpenRead(path); - stream.Seek(0x14, SeekOrigin.Begin); - return (byte)stream.ReadByte(); - } - [Fact] public void Ace_reads_a_datetime2_value_libred_wrote_into_a_database_libred_created() { @@ -114,4 +108,4 @@ public void Ace_reads_a_datetime2_value_libred_wrote_into_a_database_libred_crea } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/DateTime2LocaleAccessTests.cs b/test/LibRed.Engine.AccessTests/DateTime2LocaleAccessTests.cs index 6b7d0d6d..aafb06bf 100644 --- a/test/LibRed.Engine.AccessTests/DateTime2LocaleAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/DateTime2LocaleAccessTests.cs @@ -1,8 +1,5 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.Catalog; -using LibRed.Formats; -using LibRed.IO; using Xunit; namespace LibRed.Engine.Tests; @@ -46,11 +43,17 @@ public void Ace_gives_date_time_extended_only_the_primary_language_id(string loc foreach ((string name, byte[] bytes) in columns) output.WriteLine($"{locale} {name}: {Convert.ToHexString(bytes)}"); - byte[] text = columns["T"], extended = columns["V"]; - Assert.Equal(langId, text[0x0B] | (text[0x0C] << 8)); // the control: whole LANGID - Assert.Equal(langId & 0x00FF, extended[0x0B] | (extended[0x0C] << 8)); - Assert.Equal(0, extended[0x0D]); - Assert.Equal(0, extended[0x0E]); + Collation text, extended; + using (var database = JetDatabase.Open(path, readOnly: true)) + { + TableDefinition dt = database.Catalog.FindTable("DT")!; + text = dt.RequireColumn("T").Collation; + extended = dt.RequireColumn("V").Collation; + } + Assert.Equal(langId, (int)text.Order); // the control: whole LANGID + Assert.Equal(langId & 0x00FF, (int)extended.Order); + Assert.Equal(0, extended.SortId); + Assert.Equal(0, extended.Version); } finally { TemporaryDatabase.Delete(path); } } @@ -114,33 +117,12 @@ private static string Recollated(object engine, string locale) finally { TemporaryDatabase.Delete(source); } } - /// Every column's descriptor bytes, by name. A three-column table's definition fits one page. - /// + /// Every column's descriptor bytes, by name, as read from the table's definition. private static Dictionary Descriptors(string path, string table) { - int definitionPage; - using (var database = JetDatabase.Open(path, readOnly: true)) - definitionPage = database.Catalog.FindTable(table)!.DefinitionPage; - - using var channel = PageChannel.Open(path, readOnly: true); - JetFormatBase format = channel.Format; - byte[] page = channel.ReadPage(definitionPage).Span.ToArray(); - - int dataCount = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(format.TdefIndexCountOffset, 4)); - int colCount = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.TdefColumnCountOffset, 2)); - int start = format.TdefRealIndexBlockOffset + dataCount * format.RealIndexEntrySize; - int namePos = start + colCount * format.ColumnDescriptorSize; - - var result = new Dictionary(StringComparer.Ordinal); - for (int i = 0; i < colCount; i++) - { - int len = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(namePos, 2)); - string name = System.Text.Encoding.Unicode.GetString(page.AsSpan(namePos + 2, len)); - namePos += 2 + len; - result[name] = page.AsSpan(start + i * format.ColumnDescriptorSize, - format.ColumnDescriptorSize).ToArray(); - } - return result; + using var database = JetDatabase.Open(path, readOnly: true); + return database.Catalog.FindTable(table)!.Columns + .ToDictionary(c => c.Name, c => c.RawDescriptor!, StringComparer.Ordinal); } /// A DAO engine, once this ACE is known to take DATETIME2. An ACE below 17 cannot create the column @@ -155,4 +137,4 @@ private static object DateTime2AndDao() Assert.SkipWhen(engine is null, "DAO is unavailable in this process."); return engine!; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/DeleteByteParityTests.cs b/test/LibRed.Engine.AccessTests/DeleteByteParityTests.cs index 9abd1b6f..4603d40d 100644 --- a/test/LibRed.Engine.AccessTests/DeleteByteParityTests.cs +++ b/test/LibRed.Engine.AccessTests/DeleteByteParityTests.cs @@ -17,8 +17,6 @@ namespace LibRed.Engine.Tests; [Collection(AceCollection.Name)] public class DeleteByteParityTests(ITestOutputHelper output) { - private const int PageSize = 4096; - [Fact] public void A_delete_writes_the_same_bytes_ace_writes() { @@ -52,13 +50,17 @@ public void A_delete_writes_the_same_bytes_ace_writes() } finally { Invoke(quiet, "Close"); } + int pageSize; using (var db = JetDatabase.Open(libredCopy, readOnly: false)) + { + pageSize = db.Format.PageSize; new QueryEngine(db).ExecuteNonQuery($"DELETE FROM [{Table}] WHERE {Where}"); + } byte[] o = File.ReadAllBytes(orig), a = File.ReadAllBytes(aceCopy), n = File.ReadAllBytes(noiseCopy), l = File.ReadAllBytes(libredCopy); - HashSet aceWrote = Changed(o, a), housekeeping = Changed(o, n), libredWrote = Changed(o, l); + HashSet aceWrote = Changed(o, a, pageSize), housekeeping = Changed(o, n, pageSize), libredWrote = Changed(o, l, pageSize); var aceDelete = aceWrote.Except(housekeeping).ToHashSet(); // Nothing LibRed touched is a page ACE left alone. @@ -70,8 +72,8 @@ public void A_delete_writes_the_same_bytes_ace_writes() Assert.NotEmpty(both); foreach (int page in both) Assert.True( - a.AsSpan(page * PageSize, PageSize).SequenceEqual(l.AsSpan(page * PageSize, PageSize)), - $"page {page} (type 0x{o[page * PageSize]:X2}) differs from ACE's"); + a.AsSpan(page * pageSize, pageSize).SequenceEqual(l.AsSpan(page * pageSize, pageSize)), + $"page {page} (type 0x{o[page * pageSize]:X2}) differs from ACE's"); output.WriteLine($"{both.Length} pages written by both engines, all identical"); } @@ -124,15 +126,19 @@ public void A_delete_that_frees_space_in_a_full_page_writes_what_ace_writes() } finally { Invoke(quiet, "Close"); } + int pageSize; using (var db = JetDatabase.Open(libredCopy, readOnly: false)) + { + pageSize = db.Format.PageSize; new QueryEngine(db).ExecuteNonQuery("DELETE FROM Filled WHERE Id = 0"); + } byte[] o = File.ReadAllBytes(orig), a = File.ReadAllBytes(aceCopy), n = File.ReadAllBytes(noiseCopy), l = File.ReadAllBytes(libredCopy); // Page 0 is left out: its modification counter moves for reasons that have nothing to do with the // delete, which is why the whole-file comparisons skip it too. - var aceDelete = Changed(o, a).Except(Changed(o, n)).Where(p => p != 0).ToHashSet(); - var libredWrote = Changed(o, l).Where(p => p != 0).ToHashSet(); + var aceDelete = Changed(o, a, pageSize).Except(Changed(o, n, pageSize)).Where(p => p != 0).ToHashSet(); + var libredWrote = Changed(o, l, pageSize).Where(p => p != 0).ToHashSet(); output.WriteLine($"ACE wrote [{string.Join(",", aceDelete.Order())}], " + $"LibRed wrote [{string.Join(",", libredWrote.Order())}]"); @@ -140,20 +146,20 @@ public void A_delete_that_frees_space_in_a_full_page_writes_what_ace_writes() Assert.Empty(aceDelete.Except(libredWrote).Order()); foreach (int page in aceDelete) Assert.True( - a.AsSpan(page * PageSize, PageSize).SequenceEqual(l.AsSpan(page * PageSize, PageSize)), - $"page {page} (type 0x{o[page * PageSize]:X2}) differs from ACE's"); + a.AsSpan(page * pageSize, pageSize).SequenceEqual(l.AsSpan(page * pageSize, pageSize)), + $"page {page} (type 0x{o[page * pageSize]:X2}) differs from ACE's"); } finally { TemporaryDatabase.Delete(orig); } } - private static HashSet Changed(byte[] left, byte[] right) + private static HashSet Changed(byte[] left, byte[] right, int pageSize) { var changed = new HashSet(); - int pages = Math.Min(left.Length, right.Length) / PageSize; + int pages = Math.Min(left.Length, right.Length) / pageSize; for (int p = 0; p < pages; p++) - if (!left.AsSpan(p * PageSize, PageSize).SequenceEqual(right.AsSpan(p * PageSize, PageSize))) + if (!left.AsSpan(p * pageSize, pageSize).SequenceEqual(right.AsSpan(p * pageSize, pageSize))) changed.Add(p); - for (int p = pages; p < Math.Max(left.Length, right.Length) / PageSize; p++) changed.Add(p); + for (int p = pages; p < Math.Max(left.Length, right.Length) / pageSize; p++) changed.Add(p); return changed; } @@ -172,4 +178,4 @@ private static HashSet Changed(byte[] left, byte[] right) } return null; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/DeletedRowSpaceAccessTests.cs b/test/LibRed.Engine.AccessTests/DeletedRowSpaceAccessTests.cs index 9df26c0f..c3de2101 100644 --- a/test/LibRed.Engine.AccessTests/DeletedRowSpaceAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/DeletedRowSpaceAccessTests.cs @@ -1,7 +1,7 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.Formats; using LibRed.IO; +using LibRed.Pages; using Xunit; namespace LibRed.Engine.Tests; @@ -100,18 +100,18 @@ private static string Directory(string[] statements, Action ru for (int page = 1; page < channel.PageCount; page++) { byte[] bytes = channel.ReadPage(page).Span.ToArray(); - if (bytes[0] != 0x01) continue; - if (BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4, 4)) != definitionPage) continue; + if (PageHeader.ReadType(bytes) != PageType.DataPage) continue; + if ((int)DataPage.ReadOwner(bytes, format) != definitionPage) continue; - int slots = BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(format.DataRowCountOffset, 2)); + int slots = DataPage.ReadRowCount(bytes, format); var entries = Enumerable.Range(0, slots) - .Select(i => BinaryPrimitives.ReadUInt16LittleEndian( - bytes.AsSpan(format.DataRowDirectoryOffset + i * 2, 2)).ToString("X4")); - return $"free={BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(format.DataFreeSpaceOffset, 2))} " + .Select(i => DataPage.ReadSlot(bytes, format, i)) + .Select(s => ((int)s.Flags | s.Offset).ToString("X4")); + return $"free={DataPage.ReadFreeSpace(bytes, format)} " + $"dir=[{string.Join(" ", entries)}]"; } return "no data page"; } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/GuidColumnStorageAccessTests.cs b/test/LibRed.Engine.AccessTests/GuidColumnStorageAccessTests.cs index 24bab77c..7312446e 100644 --- a/test/LibRed.Engine.AccessTests/GuidColumnStorageAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/GuidColumnStorageAccessTests.cs @@ -22,7 +22,7 @@ public void Guid_and_uniqueidentifier_both_declare_a_variable_column() new QueryEngine(database).ExecuteNonQuery( "CREATE TABLE `W` (`Id` INTEGER PRIMARY KEY, `G` GUID NULL, `H` UNIQUEIDENTIFIER NULL)"); - TableDef table = database.Catalog.FindTable("W")!; + TableDefinition table = database.Catalog.FindTable("W")!; Assert.All(table.Columns.Where(c => c.Name is "G" or "H"), column => { Assert.False(column.IsFixedLength); @@ -55,4 +55,4 @@ public void A_wide_guid_table_no_longer_spends_fixed_record_budget() private static string Copy(string prefix) => TemporaryDatabase.CopyPath( Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), prefix); -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/IndexRootSplitByteParityTests.cs b/test/LibRed.Engine.AccessTests/IndexRootSplitByteParityTests.cs index bcbe8d12..4c41e54e 100644 --- a/test/LibRed.Engine.AccessTests/IndexRootSplitByteParityTests.cs +++ b/test/LibRed.Engine.AccessTests/IndexRootSplitByteParityTests.cs @@ -1,6 +1,8 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed; +using LibRed.Formats; +using LibRed.Pages; +using LibRed.Storage; using Xunit; namespace LibRed.Engine.Tests; @@ -18,8 +20,6 @@ namespace LibRed.Engine.Tests; [Collection(AceCollection.Name)] public class IndexRootSplitByteParityTests { - private const int PageSize = 4096; - // 204-character keys, so a leaf holds 17 and splits on the 18th. private static string Wide(int i) => $"K{i:D4}" + new string('x', 200); @@ -45,7 +45,9 @@ public void A_splitting_root_node_keeps_its_page() try { byte[] before = File.ReadAllBytes(seeded); - Assert.True(IndexPages(before, Tdef(seeded, "NodeSplit")).Count(p => before[p * PageSize] == 0x03) == 1, + int pageSize = JetFormatBase.Detect(new MemoryStream(before)).PageSize; + Assert.True(IndexPages(before, Tdef(seeded, "NodeSplit")) + .Count(p => PageHeader.ReadType(before.AsSpan(p * pageSize)) == PageType.IntermediateIndexPage) == 1, "The seed was meant to leave a single root node."); AssertInsertMatchesAce(seeded, "NodeSplit", $"INSERT INTO NodeSplit (k) VALUES ('{Spread(227)}')"); } @@ -99,15 +101,18 @@ public void An_ascending_load_splits_nodes_at_the_right_edge_as_ace_does() cmd.CommandText = sql; cmd.ExecuteNonQuery(); } + int pageSize; using (var db = JetDatabase.Open(libred, readOnly: false)) { + pageSize = db.Format.PageSize; var engine = new QueryEngine(db); foreach (string sql in inserts) engine.ExecuteNonQuery(sql); } int tdef = Tdef(empty, "Ascending"); byte[] a = File.ReadAllBytes(ace), l = File.ReadAllBytes(libred); - Assert.True(IndexPages(a, tdef).Count(p => a[p * PageSize] == 0x03) >= 3, "The load was meant to split a node."); + Assert.True(IndexPages(a, tdef).Count(p => PageHeader.ReadType(a.AsSpan(p * pageSize)) == PageType.IntermediateIndexPage) >= 3, + "The load was meant to split a node."); Assert.Equal(Shape(a, tdef), Shape(l, tdef)); } finally @@ -141,16 +146,21 @@ public void Create_index_writes_what_ace_writes(int rows) cmd.CommandText = ddl; cmd.ExecuteNonQuery(); } + int pageSize; using (var db = JetDatabase.Open(libred, readOnly: false)) + { + pageSize = db.Format.PageSize; new QueryEngine(db).ExecuteNonQuery(ddl); + } byte[] a = File.ReadAllBytes(ace), l = File.ReadAllBytes(libred); - Assert.True(a.Length == l.Length, $"ACE's file has {a.Length / PageSize} pages, LibRed's {l.Length / PageSize}."); - Assert.True(IndexPages(a, tdef).Any(p => a[p * PageSize] == 0x03), "The index was meant to have a node."); + Assert.True(a.Length == l.Length, $"ACE's file has {a.Length / pageSize} pages, LibRed's {l.Length / pageSize}."); + Assert.True(IndexPages(a, tdef).Any(p => PageHeader.ReadType(a.AsSpan(p * pageSize)) == PageType.IntermediateIndexPage), + "The index was meant to have a node."); // The index's pages, and the definition that points at its root. foreach (int page in IndexPages(a, tdef).Union(IndexPages(l, tdef)).Append(tdef)) { - int differ = Enumerable.Range(0, PageSize).FirstOrDefault(b => a[page * PageSize + b] != l[page * PageSize + b], -1); + int differ = Enumerable.Range(0, pageSize).FirstOrDefault(b => a[page * pageSize + b] != l[page * pageSize + b], -1); Assert.True(differ < 0, $"{rows} rows: page {page} differs from ACE's at 0x{differ:X3}."); } } @@ -196,17 +206,21 @@ private static void AssertInsertMatchesAce(string seeded, string table, string i cmd.CommandText = insert; cmd.ExecuteNonQuery(); } + int pageSize; using (var db = JetDatabase.Open(libred, readOnly: false)) + { + pageSize = db.Format.PageSize; new QueryEngine(db).ExecuteNonQuery(insert); + } byte[] a = File.ReadAllBytes(ace), l = File.ReadAllBytes(libred); Assert.True(IndexPages(a, tdef).Count > IndexPages(File.ReadAllBytes(seeded), tdef).Count, $"{insert[..Math.Min(60, insert.Length)]}…: the insert was meant to split a page."); - Assert.True(a.Length == l.Length, $"{insert}: ACE's file has {a.Length / PageSize} pages, LibRed's {l.Length / PageSize}."); + Assert.True(a.Length == l.Length, $"{insert}: ACE's file has {a.Length / pageSize} pages, LibRed's {l.Length / pageSize}."); foreach (int page in IndexPages(a, tdef).Union(IndexPages(l, tdef))) { - int differ = Enumerable.Range(0, PageSize).FirstOrDefault( - b => a[page * PageSize + b] != l[page * PageSize + b], -1); + int differ = Enumerable.Range(0, pageSize).FirstOrDefault( + b => a[page * pageSize + b] != l[page * pageSize + b], -1); Assert.True(differ < 0, $"{insert[..Math.Min(60, insert.Length)]}…: page {page} differs from ACE's at 0x{differ:X3}."); } } @@ -222,18 +236,22 @@ private static void AssertInsertMatchesAce(string seeded, string table, string i private static string Shape(byte[] file, int tdef) { List pages = IndexPages(file, tdef); - int At(int page, int offset) => BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(page * PageSize + offset, 4)); + JetFormatBase format = JetFormatBase.Detect(new MemoryStream(file)); + int pageSize = format.PageSize; var levels = new List(); - foreach (IGrouping level in pages.GroupBy(p => file[p * PageSize + 0x1A]).OrderByDescending(g => g.Key)) + foreach (IGrouping level in pages.GroupBy(p => file[p * pageSize + format.IndexLevelOffset]).OrderByDescending(g => g.Key)) { var byPage = level.ToHashSet(); var shapes = new List(); - for (int p = level.Single(q => At(q, 0x0C) == 0); p != 0 && byPage.Contains(p); p = At(p, 0x10)) + for (int p = level.Single(q => IndexTree.ReadSiblings(file.AsSpan(q * pageSize, pageSize), format).Previous == 0); + p != 0 && byPage.Contains(p); + p = IndexTree.ReadSiblings(file.AsSpan(p * pageSize, pageSize), format).Next) { int entries = 0; - for (int i = 0x1B; i < 0x1E0; i++) entries += System.Numerics.BitOperations.PopCount(file[p * PageSize + i]); - shapes.Add($"{entries}/{BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(p * PageSize + 0x18))}/" + - $"{BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(p * PageSize + 2))}"); + for (int i = format.IndexEntryMaskOffset; i < format.IndexEntryDataOffset; i++) + entries += System.Numerics.BitOperations.PopCount(file[p * pageSize + i]); + shapes.Add($"{entries}/{IndexTree.ReadCompressedByteCount(file.AsSpan(p * pageSize, pageSize), format)}/" + + $"{IndexTree.ReadFreeSpace(file.AsSpan(p * pageSize, pageSize), format)}"); } levels.Add($"level {level.Key}: {string.Join(" ", shapes)}"); } @@ -242,11 +260,13 @@ private static string Shape(byte[] file, int tdef) private static List IndexPages(byte[] file, int tdef) { + JetFormatBase format = JetFormatBase.Detect(new MemoryStream(file)); + int pageSize = format.PageSize; var pages = new List(); - for (int p = 0; p < file.Length / PageSize; p++) - if (file[p * PageSize] is 0x03 or 0x04 - && BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(p * PageSize + 4, 4)) == tdef) + for (int p = 0; p < file.Length / pageSize; p++) + if (PageHeader.ReadType(file.AsSpan(p * pageSize)) is PageType.IntermediateIndexPage or PageType.LeafIndexPage + && IndexTree.ReadOwner(file.AsSpan(p * pageSize, pageSize), format) == tdef) pages.Add(p); return pages; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/IndexSplitByteParityTests.cs b/test/LibRed.Engine.AccessTests/IndexSplitByteParityTests.cs index dc351f89..c71d6a00 100644 --- a/test/LibRed.Engine.AccessTests/IndexSplitByteParityTests.cs +++ b/test/LibRed.Engine.AccessTests/IndexSplitByteParityTests.cs @@ -1,6 +1,8 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed; +using LibRed.Formats; +using LibRed.Pages; +using LibRed.Storage; using Xunit; namespace LibRed.Engine.Tests; @@ -18,7 +20,6 @@ namespace LibRed.Engine.Tests; [Collection(AceCollection.Name)] public class IndexSplitByteParityTests(ITestOutputHelper output) { - private const int PageSize = 4096; private const int EvenKeys = 900; [Fact] @@ -67,28 +68,34 @@ private void AssertSplitMatchesAce(string seeded, int key) new QueryEngine(db).ExecuteNonQuery($"INSERT INTO SplitGuard (k) VALUES ({key})"); int tdef; - using (var db = JetDatabase.Open(seeded)) tdef = db.Catalog.FindTable("SplitGuard")!.DefinitionPage; + JetFormatBase format; + using (var db = JetDatabase.Open(seeded)) + { + tdef = db.Catalog.FindTable("SplitGuard")!.DefinitionPage; + format = db.Format; + } + int pageSize = format.PageSize; byte[] a = File.ReadAllBytes(aceCopy), l = File.ReadAllBytes(libredCopy); - int pages = Math.Min(a.Length, l.Length) / PageSize; + int pages = Math.Min(a.Length, l.Length) / pageSize; int checkedPages = 0; for (int p = 0; p < pages; p++) { // Only this index's own pages: the rest of the file is ACE's bookkeeping. - if (!IsOurIndexPage(a, p, tdef) && !IsOurIndexPage(l, p, tdef)) continue; + if (!IsOurIndexPage(a, p, tdef, format) && !IsOurIndexPage(l, p, tdef, format)) continue; - int aceFree = BinaryPrimitives.ReadUInt16LittleEndian(a.AsSpan(p * PageSize + 2, 2)); - int libFree = BinaryPrimitives.ReadUInt16LittleEndian(l.AsSpan(p * PageSize + 2, 2)); + int aceFree = IndexTree.ReadFreeSpace(a.AsSpan(p * pageSize, pageSize), format); + int libFree = IndexTree.ReadFreeSpace(l.AsSpan(p * pageSize, pageSize), format); Assert.True(aceFree == libFree, $"key {key}, page {p}: free space {libFree} where ACE wrote {aceFree} — the leaf was cut elsewhere."); // Past the live end a page keeps whatever it held, and a page ACE appended past the old // end-of-file keeps ACE's uninitialised buffer, which is not reproducible. Compare the live // region, which is the split itself. - int live = PageSize - aceFree; + int live = pageSize - aceFree; Assert.True( - a.AsSpan(p * PageSize, live).SequenceEqual(l.AsSpan(p * PageSize, live)), + a.AsSpan(p * pageSize, live).SequenceEqual(l.AsSpan(p * pageSize, live)), $"key {key}, page {p}: live bytes differ from ACE's."); checkedPages++; } @@ -103,8 +110,8 @@ private void AssertSplitMatchesAce(string seeded, int key) } } - private static bool IsOurIndexPage(byte[] file, int page, int tdef) => - (page + 1) * PageSize <= file.Length - && file[page * PageSize] is 0x03 or 0x04 - && BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(page * PageSize + 4, 4)) == tdef; -} + private static bool IsOurIndexPage(byte[] file, int page, int tdef, JetFormatBase format) => + (page + 1) * format.PageSize <= file.Length + && PageHeader.ReadType(file.AsSpan(page * format.PageSize)) is PageType.IntermediateIndexPage or PageType.LeafIndexPage + && IndexTree.ReadOwner(file.AsSpan(page * format.PageSize, format.PageSize), format) == tdef; +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/IndexSplitPackingAccessTests.cs b/test/LibRed.Engine.AccessTests/IndexSplitPackingAccessTests.cs index cd2e6314..45919071 100644 --- a/test/LibRed.Engine.AccessTests/IndexSplitPackingAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/IndexSplitPackingAccessTests.cs @@ -1,6 +1,7 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.IO; +using LibRed.Pages; +using LibRed.Storage; using Xunit; namespace LibRed.Engine.Tests; @@ -152,12 +153,12 @@ private static (int Count, string Free, int Used, string Prefix) Leaves(string w for (int page = 1; page < channel.PageCount; page++) { byte[] bytes = channel.ReadPage(page).Span.ToArray(); - if (bytes[0] != 0x04) continue; - if (BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4, 4)) != definitionPage) continue; + if (PageHeader.ReadType(bytes) != PageType.LeafIndexPage) continue; + if (IndexTree.ReadOwner(bytes, channel.Format) != definitionPage) continue; - int pageFree = BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(2, 2)); + int pageFree = IndexTree.ReadFreeSpace(bytes, channel.Format); free.Add(pageFree); - prefix.Add(BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(0x18, 2))); + prefix.Add(IndexTree.ReadCompressedByteCount(bytes, channel.Format)); used += channel.Format.PageSize - pageFree; } // Both lists stay in PAGE order. Sorting one and not the other made the two columns disagree @@ -167,4 +168,4 @@ private static (int Count, string Free, int Used, string Prefix) Leaves(string w } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/IndexSplitPointProbeTest.cs b/test/LibRed.Engine.AccessTests/IndexSplitPointProbeTest.cs index a3555c83..cf19a9fd 100644 --- a/test/LibRed.Engine.AccessTests/IndexSplitPointProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/IndexSplitPointProbeTest.cs @@ -1,8 +1,10 @@ -using System.Buffers.Binary; using System.Data.OleDb; using System.Globalization; using LibRed; using LibRed.Catalog; +using LibRed.Formats; +using LibRed.Pages; +using LibRed.Storage; using Xunit; namespace LibRed.Engine.Tests; @@ -24,7 +26,6 @@ namespace LibRed.Engine.Tests; [Collection(AceCollection.Name)] public class IndexSplitPointProbeTest(ITestOutputHelper output) { - private const int PageSize = 4096; private const int EvenKeys = 900; // enough for several full leaves at ~9 bytes an entry [Theory] @@ -62,16 +63,22 @@ public void Probe_where_ace_cuts_a_full_leaf(int oddKey) byte[] o = File.ReadAllBytes(seeded), a = File.ReadAllBytes(aceCopy), l = File.ReadAllBytes(libredCopy); int tdef; - using (var db = JetDatabase.Open(seeded)) tdef = db.Catalog.FindTable("SplitProbe")!.DefinitionPage; + JetFormatBase format; + using (var db = JetDatabase.Open(seeded)) + { + tdef = db.Catalog.FindTable("SplitProbe")!.DefinitionPage; + format = db.Format; + } + int pageSize = format.PageSize; output.WriteLine($"PROBE key {oddKey}: sizes orig={o.Length} ace={a.Length} libred={l.Length}"); - foreach (int page in Union(Changed(o, a), Changed(o, l)).Order()) + foreach (int page in Union(Changed(o, a, pageSize), Changed(o, l, pageSize)).Order()) { - if (!Owned(o, page, tdef) && !Owned(a, page, tdef) && !Owned(l, page, tdef)) continue; - output.WriteLine($"PROBE page {page} (was 0x{Type(o, page):X2}):" - + $" free orig={Free(o, page)} ace={Free(a, page)} libred={Free(l, page)}" - + $" | prefix orig={Prefix(o, page)} ace={Prefix(a, page)} libred={Prefix(l, page)}" - + $" | {(Same(a, l, page) ? "IDENTICAL" : $"differs first @0x{FirstDiff(a, l, page):X3}")}"); + if (!Owned(o, page, tdef, format) && !Owned(a, page, tdef, format) && !Owned(l, page, tdef, format)) continue; + output.WriteLine($"PROBE page {page} (was 0x{Type(o, page, pageSize):X2}):" + + $" free orig={Free(o, page, format)} ace={Free(a, page, format)} libred={Free(l, page, format)}" + + $" | prefix orig={Prefix(o, page, format)} ace={Prefix(a, page, format)} libred={Prefix(l, page, format)}" + + $" | {(Same(a, l, page, pageSize) ? "IDENTICAL" : $"differs first @0x{FirstDiff(a, l, page, pageSize):X3}")}"); } } finally @@ -88,40 +95,41 @@ private static void Exec(OleDbConnection conn, string sql) cmd.ExecuteNonQuery(); } - private static byte Type(byte[] f, int p) => (p + 1) * PageSize <= f.Length ? f[p * PageSize] : (byte)0xFF; + private static byte Type(byte[] f, int p, int pageSize) => (p + 1) * pageSize <= f.Length ? f[p * pageSize] : (byte)0xFF; - private static bool Owned(byte[] f, int p, int tdef) => - (p + 1) * PageSize <= f.Length && f[p * PageSize] is 0x03 or 0x04 - && BinaryPrimitives.ReadInt32LittleEndian(f.AsSpan(p * PageSize + 4, 4)) == tdef; + private static bool Owned(byte[] f, int p, int tdef, JetFormatBase format) => + (p + 1) * format.PageSize <= f.Length + && PageHeader.ReadType(f.AsSpan(p * format.PageSize)) is PageType.IntermediateIndexPage or PageType.LeafIndexPage + && IndexTree.ReadOwner(f.AsSpan(p * format.PageSize, format.PageSize), format) == tdef; - private static int Free(byte[] f, int p) => (p + 1) * PageSize <= f.Length - ? BinaryPrimitives.ReadUInt16LittleEndian(f.AsSpan(p * PageSize + 2, 2)) : -1; + private static int Free(byte[] f, int p, JetFormatBase format) => (p + 1) * format.PageSize <= f.Length + ? IndexTree.ReadFreeSpace(f.AsSpan(p * format.PageSize, format.PageSize), format) : -1; - private static int Prefix(byte[] f, int p) => (p + 1) * PageSize <= f.Length - ? BinaryPrimitives.ReadUInt16LittleEndian(f.AsSpan(p * PageSize + 0x18, 2)) : -1; + private static int Prefix(byte[] f, int p, JetFormatBase format) => (p + 1) * format.PageSize <= f.Length + ? IndexTree.ReadCompressedByteCount(f.AsSpan(p * format.PageSize, format.PageSize), format) : -1; - private static bool Same(byte[] x, byte[] y, int p) => - (p + 1) * PageSize <= Math.Min(x.Length, y.Length) - && x.AsSpan(p * PageSize, PageSize).SequenceEqual(y.AsSpan(p * PageSize, PageSize)); + private static bool Same(byte[] x, byte[] y, int p, int pageSize) => + (p + 1) * pageSize <= Math.Min(x.Length, y.Length) + && x.AsSpan(p * pageSize, pageSize).SequenceEqual(y.AsSpan(p * pageSize, pageSize)); private static IEnumerable Union(HashSet x, HashSet y) => x.Union(y); - private static int FirstDiff(byte[] x, byte[] y, int p) + private static int FirstDiff(byte[] x, byte[] y, int p, int pageSize) { - int limit = Math.Min(Math.Min(x.Length, y.Length) - p * PageSize, PageSize); + int limit = Math.Min(Math.Min(x.Length, y.Length) - p * pageSize, pageSize); for (int i = 0; i < limit; i++) - if (x[p * PageSize + i] != y[p * PageSize + i]) return i; + if (x[p * pageSize + i] != y[p * pageSize + i]) return i; return -1; } - private static HashSet Changed(byte[] left, byte[] right) + private static HashSet Changed(byte[] left, byte[] right, int pageSize) { var changed = new HashSet(); - int pages = Math.Min(left.Length, right.Length) / PageSize; + int pages = Math.Min(left.Length, right.Length) / pageSize; for (int p = 0; p < pages; p++) - if (!left.AsSpan(p * PageSize, PageSize).SequenceEqual(right.AsSpan(p * PageSize, PageSize))) + if (!left.AsSpan(p * pageSize, pageSize).SequenceEqual(right.AsSpan(p * pageSize, pageSize))) changed.Add(p); - for (int p = pages; p < Math.Max(left.Length, right.Length) / PageSize; p++) changed.Add(p); + for (int p = pages; p < Math.Max(left.Length, right.Length) / pageSize; p++) changed.Add(p); return changed; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/IndexUniqueEntryCountAccessTests.cs b/test/LibRed.Engine.AccessTests/IndexUniqueEntryCountAccessTests.cs index c5279ee7..85d57d2a 100644 --- a/test/LibRed.Engine.AccessTests/IndexUniqueEntryCountAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/IndexUniqueEntryCountAccessTests.cs @@ -1,5 +1,8 @@ using System.Data.OleDb; +using LibRed.Catalog; using LibRed; +using LibRed.IO; +using LibRed.Pages; using Xunit; namespace LibRed.Engine.Tests; @@ -238,15 +241,17 @@ private static string Statistics(string path, params string[] tables) { if (tables.Length == 0) tables = ["S"]; using var db = JetDatabase.Open(path); - byte[] file = File.ReadAllBytes(path); return string.Join(" ", tables.SelectMany(name => { var table = db.Catalog.FindTable(name)!; - int block = table.DefinitionPage * 4096 + 0x3F; + PageBuffer tdef = TableDefinition.ReadChain(db.Channel, table.DefinitionPage).Buffer; return table.Indexes.GroupBy(i => i.RealIndexOrdinal).Select(g => g.First()) .OrderBy(i => i.Name, StringComparer.Ordinal) - .Select(i => $"{name}.{i.Name}={BitConverter.ToInt32(file, block + i.RealIndexOrdinal * 12)}" - + $"/{BitConverter.ToInt32(file, block + i.RealIndexOrdinal * 12 + 4)}"); + .Select(i => + { + (int total, int unique) = TableDefinition.ReadIndexCounts(tdef.Span, db.Format, i.RealIndexOrdinal); + return $"{name}.{i.Name}={total}/{unique}"; + }); })); } @@ -270,4 +275,4 @@ private static string Counts(string path, string table = "S") return string.Join(" ", db.Catalog.FindTable(table)!.Indexes.OrderBy(i => i.Name, StringComparer.Ordinal) .Select(i => $"{i.Name}={i.UniqueEntryCount}")); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/LikeCandidates.cs b/test/LibRed.Engine.AccessTests/LikeCandidates.cs index b3ba3e0e..c36bbaa5 100644 --- a/test/LibRed.Engine.AccessTests/LikeCandidates.cs +++ b/test/LibRed.Engine.AccessTests/LikeCandidates.cs @@ -58,7 +58,7 @@ public static HashSet AceMatches(Collation collation) string path = TemporaryDatabase.CreatePath("like-pairs-", ".accdb"); try { - DatabaseCreator.CreateEmpty(path, collation: collation); + JetDatabase.Create(path, collation: collation); using OleDbConnection ace = AceTestDatabase.Open(path); using (OleDbCommand create = ace.CreateCommand()) { @@ -99,7 +99,7 @@ public static HashSet LibRedMatches(Collation collation) string path = TemporaryDatabase.CreatePath("like-pairs-libred-", ".accdb"); try { - DatabaseCreator.CreateEmpty(path, collation: collation); + JetDatabase.Create(path, collation: collation); using var db = JetDatabase.Open(path, readOnly: false); var engine = new QueryEngine(db); engine.ExecuteNonQuery("CREATE TABLE Pairs (Id LONG, V TEXT(10), P TEXT(10))"); @@ -112,4 +112,4 @@ public static HashSet LibRedMatches(Collation collation) } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/LikeCollationProbeTests.cs b/test/LibRed.Engine.AccessTests/LikeCollationProbeTests.cs index 8b180c1c..35e76770 100644 --- a/test/LibRed.Engine.AccessTests/LikeCollationProbeTests.cs +++ b/test/LibRed.Engine.AccessTests/LikeCollationProbeTests.cs @@ -117,7 +117,7 @@ private static string[] Ace(Collation collation) string path = TemporaryDatabase.CreatePath("like-probe-", ".accdb"); try { - DatabaseCreator.CreateEmpty(path, collation: collation); + JetDatabase.Create(path, collation: collation); using OleDbConnection ace = AceTestDatabase.Open(path); Exec(ace, "CREATE TABLE One (Id LONG)"); Exec(ace, "INSERT INTO One (Id) VALUES (1)"); @@ -140,7 +140,7 @@ private static string[] LibRed(Collation collation) string path = TemporaryDatabase.CreatePath("like-probe-libred-", ".accdb"); try { - DatabaseCreator.CreateEmpty(path, collation: collation); + JetDatabase.Create(path, collation: collation); using var db = JetDatabase.Open(path, readOnly: false); var engine = new QueryEngine(db); engine.ExecuteNonQuery("CREATE TABLE One (Id LONG)"); @@ -160,4 +160,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/LongValueColumnLayoutAccessTests.cs b/test/LibRed.Engine.AccessTests/LongValueColumnLayoutAccessTests.cs index f05bf384..7aa7f9ed 100644 --- a/test/LibRed.Engine.AccessTests/LongValueColumnLayoutAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/LongValueColumnLayoutAccessTests.cs @@ -1,4 +1,3 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.Catalog; using LibRed.Engine; @@ -88,8 +87,9 @@ private void Dump(string path, string stage) { using var channel = PageChannel.Open(path, readOnly: true); var table = new JetCatalog(channel).FindTable("LayoutProbe")!; - var header = channel.ReadPage(table.DefinitionPage); - output.WriteLine($"{stage}: highWater={BinaryPrimitives.ReadUInt16LittleEndian(header.Span.Slice(channel.Format.TdefMaxColumnsOffset, 2))}, varCount={BinaryPrimitives.ReadUInt16LittleEndian(header.Span.Slice(channel.Format.TdefVariableColumnsOffset, 2))}"); + var header = new TableDefinition(); + header.Read(channel, table.DefinitionPage); + output.WriteLine($"{stage}: highWater={header.ColumnIdHighWater}, varCount={header.VariableColumnCount}"); foreach (var c in table.Columns) output.WriteLine($"{c.Name}: id={c.ColumnId}, ordinal={c.Index}, var={c.VariableIndex}, fixedOffset={c.FixedOffset}, descriptor={Convert.ToHexString(c.RawDescriptor!)}"); foreach (int number in new UsageMap(channel, table).DataPages()) @@ -100,4 +100,4 @@ private void Dump(string path, string stage) if (!page.Rows[row].IsDeleted) output.WriteLine($"row {number}:{row}: {Convert.ToHexString(page.GetRow(row))}"); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/LongValueFreeMapAccessTests.cs b/test/LibRed.Engine.AccessTests/LongValueFreeMapAccessTests.cs index 89eaa3e7..e519b5e0 100644 --- a/test/LibRed.Engine.AccessTests/LongValueFreeMapAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/LongValueFreeMapAccessTests.cs @@ -1,4 +1,5 @@ using System.Data.OleDb; +using LibRed.Catalog; using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -59,13 +60,13 @@ private static string Maps(string path) { using var db = JetDatabase.Open(path, readOnly: true); Table table = db.OpenTable("L"); - var tdef = new TableDefinitionPage(); + var tdef = new TableDefinition(); tdef.Read(table.Channel.ReadPage(table.Definition.DefinitionPage), table.Channel.Format); int id = table.Definition.RequireColumn("M").ColumnId; var maps = new UsageMap(table.Channel, table.Definition); int page = maps.PagesInMap(tdef.LongValueOwnedMaps[id].Row, tdef.LongValueOwnedMaps[id].Page).Single(); bool free = maps.PagesInMap(tdef.LongValueFreeMaps[id].Row, tdef.LongValueFreeMaps[id].Page).Contains(page); - int space = BitConverter.ToUInt16(table.Channel.ReadPage(page).Span[2..4]); + int space = DataPage.ReadFreeSpace(table.Channel.ReadPage(page).Span, table.Channel.Format); return $"{space} bytes free, {(free ? "in the free map" : "out of it")}"; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/LongValueOrderProbeTest.cs b/test/LibRed.Engine.AccessTests/LongValueOrderProbeTest.cs index 1fbb3440..11990f6d 100644 --- a/test/LibRed.Engine.AccessTests/LongValueOrderProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/LongValueOrderProbeTest.cs @@ -1,8 +1,9 @@ -using System.Buffers.Binary; using System.Data.OleDb; using System.Globalization; using LibRed; using LibRed.Catalog; +using LibRed.Formats; +using LibRed.IO; using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -55,10 +56,14 @@ public void Long_value_pages_of_one_row(string insert) command.CommandText = sql; command.ExecuteNonQuery(); } + int pageSize; using (var database = JetDatabase.Open(libred, readOnly: false)) + { + pageSize = database.Format.PageSize; new QueryEngine(database).ExecuteNonQuery(sql); + } - output.WriteLine($"{insert}\nbefore {new FileInfo(origin).Length / 4096} pages\n" + + output.WriteLine($"{insert}\nbefore {new FileInfo(origin).Length / pageSize} pages\n" + $"ACE: {Describe(ace)}\nLibRed: {Describe(libred)}"); } finally @@ -107,29 +112,32 @@ public void Compressed_value_page_bytes(string statements) command.CommandText = statement; command.ExecuteNonQuery(); } + JetFormatBase format; using (var database = JetDatabase.Open(libred, readOnly: false)) { + format = database.Format; var queries = new QueryEngine(database); foreach (string statement in sql) queries.ExecuteNonQuery(statement); } + int pageSize = format.PageSize; byte[] a = File.ReadAllBytes(ace), l = File.ReadAllBytes(libred); - var report = new System.Text.StringBuilder($"{statements}\nACE {a.Length / 4096} pages, LibRed {l.Length / 4096} pages\n"); - for (int page = 1; page < Math.Min(a.Length, l.Length) / 4096; page++) + var report = new System.Text.StringBuilder($"{statements}\nACE {a.Length / pageSize} pages, LibRed {l.Length / pageSize} pages\n"); + for (int page = 1; page < Math.Min(a.Length, l.Length) / pageSize; page++) { - int at = page * 4096; - if (!a.AsSpan(at + 4, 4).SequenceEqual("LVAL"u8)) continue; + int at = page * pageSize; + if (DataPage.ReadOwner(a.AsSpan(at, pageSize), format) != JetFormatBase.LongValuePageMarker) continue; var ranges = new List(); - for (int i = 0; i < 4096; i++) + for (int i = 0; i < pageSize; i++) { if (a[at + i] == l[at + i]) continue; int start = i; - while (i + 1 < 4096 && (a[at + i + 1] != l[at + i + 1] || (i + 2 < 4096 && a[at + i + 2] != l[at + i + 2]))) i++; + while (i + 1 < pageSize && (a[at + i + 1] != l[at + i + 1] || (i + 2 < pageSize && a[at + i + 2] != l[at + i + 2]))) i++; ranges.Add($"0x{start:X3}-0x{i:X3} ACE {Convert.ToHexString(a, at + start, Math.Min(8, i - start + 1))}"); } - int rows = BinaryPrimitives.ReadUInt16LittleEndian(a.AsSpan(at + 12)); + int rows = DataPage.ReadRowCount(a.AsSpan(at, pageSize), format); string slots = string.Join(",", Enumerable.Range(0, rows) - .Select(r => $"0x{BinaryPrimitives.ReadUInt16LittleEndian(a.AsSpan(at + 14 + r * 2)) & 0x1FFF:X3}")); + .Select(r => $"0x{DataPage.ReadSlot(a.AsSpan(at, pageSize), format, r).Offset:X3}")); report.AppendLine(CultureInfo.InvariantCulture, $" LVAL page {page}: ACE rows at [{slots}]; {(ranges.Count == 0 ? "identical" : string.Join("; ", ranges))}"); } @@ -152,44 +160,40 @@ private static string Describe(string path) page.Read(doc.Channel.ReadPageShared(id.Page), doc.Channel.Format); byte[] row = page.GetRow(id.Row).ToArray(); var parts = new List(); - foreach ((int index, byte[] descriptor) in RowDecoder.LongValueDescriptors(doc.Definition.Columns, doc.Channel.Format, row) + foreach ((int index, byte[] descriptor) in RowCodec.LongValueDescriptors(doc.Definition.Columns, doc.Channel.Format, row) .OrderBy(d => d.Key)) { ColumnDef column = doc.Definition.Columns[index]; - byte flags = (byte)(descriptor[3] & 0xC0); - int length = BinaryPrimitives.ReadInt32LittleEndian(descriptor) & 0x3FFFFFFF; - string where = flags switch + var value = LongValueStore.Read(descriptor, doc.Channel.Format); + string where = value.Storage switch { - 0x80 => "inline", - 0x40 => $"single {Pointer(descriptor.AsSpan(4))}", + LongValueStore.StorageKind.Inline => "inline", + LongValueStore.StorageKind.SinglePage => $"single {value.Page}:{value.Row}", _ => $"chained {string.Join(">", Chain(doc, descriptor))}", }; - parts.Add($"{column.Name}({length}) {where}"); + parts.Add($"{column.Name}({value.Length}) {where}"); } - return $"{new FileInfo(path).Length / 4096} pages, row on {id.Page}; " + string.Join("; ", parts); + return $"{new FileInfo(path).Length / database.Format.PageSize} pages, row on {id.Page}; " + string.Join("; ", parts); } private static List Chain(Table table, byte[] descriptor) { var pages = new List(); - int row = descriptor[4], pageNumber = descriptor[5] | (descriptor[6] << 8) | (descriptor[7] << 16); + var (_, _, row, pageNumber, _) = LongValueStore.Read(descriptor, table.Channel.Format); while (pageNumber != 0 && pages.Count < 64) { pages.Add($"{pageNumber}:{row}"); var page = new DataPage(); page.Read(table.Channel.ReadPageShared(pageNumber), table.Channel.Format); ReadOnlySpan record = page.GetRow(row); - row = record[0]; - pageNumber = record[1] | (record[2] << 8) | (record[3] << 16); + (row, pageNumber) = PageBuffer.ReadRecordPointer(record, 0); } return pages; } - private static string Pointer(ReadOnlySpan at) => $"{at[1] | (at[2] << 8) | (at[3] << 16)}:{at[0]}"; - private static string Hex(int bytes) => Convert.ToHexString([.. Enumerable.Range(0, bytes).Select(i => (byte)((i * 7 + 1) % 251))]); private static object? Invoke(object target, string member, params object?[] args) => target.GetType().InvokeMember(member, System.Reflection.BindingFlags.InvokeMethod, null, target, args); -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/LvPropByteParityAccessTests.cs b/test/LibRed.Engine.AccessTests/LvPropByteParityAccessTests.cs index 422c2658..dd1a0226 100644 --- a/test/LibRed.Engine.AccessTests/LvPropByteParityAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/LvPropByteParityAccessTests.cs @@ -68,7 +68,7 @@ private static void LibRedRun(string path, string[] statements) catch (OleDbException) { return null; } using var database = JetDatabase.Open(path, readOnly: true); - TableDef objects = database.Catalog.FindTable("MSysObjects")!; + TableDefinition objects = database.Catalog.FindTable("MSysObjects")!; int nameCol = objects.Columns.Single(c => c.Name == "Name").Index; int lvCol = objects.Columns.Single(c => c.Name == "LvProp").Index; @@ -79,4 +79,4 @@ private static void LibRedRun(string path, string[] statements) } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/LvPropStorageAccessTests.cs b/test/LibRed.Engine.AccessTests/LvPropStorageAccessTests.cs index 5b87026e..5e34fe5a 100644 --- a/test/LibRed.Engine.AccessTests/LvPropStorageAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/LvPropStorageAccessTests.cs @@ -71,14 +71,14 @@ private static string Stored(string path, string table) { using var database = JetDatabase.Open(path, readOnly: true); Table objects = database.OpenTable("MSysObjects"); - TableDef definition = objects.Definition; + TableDefinition definition = objects.Definition; int name = definition.RequireColumn("Name").Index, lvProp = definition.RequireColumn("LvProp").Index; foreach ((RowId id, object?[] values) in objects.Rows().WithIds()) { if (values[name] as string != table) continue; var page = new DataPage(); page.Read(objects.Channel.ReadPageShared(id.Page), objects.Channel.Format); - byte[] descriptor = RowDecoder.LongValueDescriptors( + byte[] descriptor = RowCodec.LongValueDescriptors( definition.Columns, objects.Channel.Format, page.GetRow(id.Row))[lvProp]; string form = descriptor[3] switch { 0x80 => "inline", 0x40 => "on a page", _ => $"0x{descriptor[3]:X2}" }; return $"{form}, {((byte[])values[lvProp]!).Length} bytes"; @@ -99,4 +99,4 @@ private static void LibRedRun(string path, string sql) using var database = JetDatabase.Open(path, readOnly: false); new QueryEngine(database).ExecuteNonQuery(sql); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/MemoCompressionAccessTests.cs b/test/LibRed.Engine.AccessTests/MemoCompressionAccessTests.cs index 63b1a26e..11f875a0 100644 --- a/test/LibRed.Engine.AccessTests/MemoCompressionAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/MemoCompressionAccessTests.cs @@ -1,8 +1,9 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.Catalog; using LibRed.Formats; +using LibRed.Storage; using LibRed.IO; +using LibRed.Pages; using Xunit; namespace LibRed.Engine.Tests; @@ -178,14 +179,13 @@ public void A_text_column_breaks_the_tie_the_other_way(string value, string expe for (int page = 1; page < channel.PageCount; page++) { byte[] bytes = channel.ReadPage(page).Span.ToArray(); - if (bytes[0] != 0x01) continue; - if (BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4, 4)) != definitionPage) continue; - if (BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(format.DataRowCountOffset, 2)) == 0) + if (PageHeader.ReadType(bytes) != PageType.DataPage) continue; + if ((int)DataPage.ReadOwner(bytes, format) != definitionPage) continue; + if (DataPage.ReadRowCount(bytes, format) == 0) continue; - int start = BinaryPrimitives.ReadUInt16LittleEndian( - bytes.AsSpan(format.DataRowDirectoryOffset, 2)) & 0x1FFF; - int at = start + 2 + 4; + int start = DataPage.ReadSlot(bytes, format, 0).Offset; + int at = start + format.RowColumnCountSize + 4; return Convert.ToHexString(bytes.AsSpan(at, format.PageSize - 7 - at)); } return null; @@ -238,16 +238,15 @@ public void Libred_reads_back_what_ace_wrote(string value) for (int page = 1; page < channel.PageCount; page++) { byte[] bytes = channel.ReadPage(page).Span.ToArray(); - if (bytes[0] != 0x01) continue; - if (BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4, 4)) != definitionPage) continue; - if (BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(format.DataRowCountOffset, 2)) == 0) + if (PageHeader.ReadType(bytes) != PageType.DataPage) continue; + if ((int)DataPage.ReadOwner(bytes, format) != definitionPage) continue; + if (DataPage.ReadRowCount(bytes, format) == 0) continue; - int start = BinaryPrimitives.ReadUInt16LittleEndian( - bytes.AsSpan(format.DataRowDirectoryOffset, 2)) & 0x1FFF; - int at = start + 2 + 4; - int length = (int)(BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(at, 4)) & 0x3FFFFFFF); - return bytes.AsSpan(at + 12, length).ToArray(); + int start = DataPage.ReadSlot(bytes, format, 0).Offset; + int at = start + format.RowColumnCountSize + 4; + int length = LongValueStore.Read(bytes.AsSpan(at), format).Length; + return bytes.AsSpan(at + format.LongValueDescriptorSize, length).ToArray(); } return null; } @@ -291,21 +290,19 @@ private static void LibRedRun(string path, string[] statements) for (int page = 1; page < channel.PageCount; page++) { byte[] bytes = channel.ReadPage(page).Span.ToArray(); - if (bytes[0] != 0x01) continue; - if (BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4, 4)) != definitionPage) continue; - if (BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(format.DataRowCountOffset, 2)) == 0) + if (PageHeader.ReadType(bytes) != PageType.DataPage) continue; + if ((int)DataPage.ReadOwner(bytes, format) != definitionPage) continue; + if (DataPage.ReadRowCount(bytes, format) == 0) continue; - int start = BinaryPrimitives.ReadUInt16LittleEndian( - bytes.AsSpan(format.DataRowDirectoryOffset, 2)) & 0x1FFF; - int at = start + 2 + 4; // past the row's column count and the LONG column - uint header = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(at, 4)); - byte flags = (byte)(bytes[at + 3] & 0xC0); - return $"{(flags == 0x80 ? "inline" : flags == 0x40 ? "page" : "chained")} " - + $"len={header & 0x3FFFFFFF}"; + int start = DataPage.ReadSlot(bytes, format, 0).Offset; + int at = start + format.RowColumnCountSize + 4; // past the row's column count and the LONG column + (int length, LongValueStore.StorageKind storage, _, _, _) = LongValueStore.Read(bytes.AsSpan(at), format); + return $"{(storage == LongValueStore.StorageKind.Inline ? "inline" : storage == LongValueStore.StorageKind.SinglePage ? "page" : "chained")} " + + $"len={length}"; } return null; } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/PageGroupProbeTest.cs b/test/LibRed.Engine.AccessTests/PageGroupProbeTest.cs index a8cd4c6a..e88baefb 100644 --- a/test/LibRed.Engine.AccessTests/PageGroupProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/PageGroupProbeTest.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Text; using LibRed; +using LibRed.Formats; using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -22,8 +23,6 @@ namespace LibRed.Engine.Tests; [Collection(AceCollection.Name)] public class PageGroupProbeTest(ITestOutputHelper output) { - private const int PageSize = 4096; - [Theory(Explicit = true)] [InlineData(0)] [InlineData(40)] @@ -35,6 +34,8 @@ public void Bulk_insert_page_map(int paddingRows) object workspace = Invoke(engine, "CreateWorkspace", "", "admin", "", 2)!; object db = Invoke(workspace, "CreateDatabase", origin, ";LANGID=0x0409;CP=1252;COUNTRY=0", 128)!; Invoke(db, "Close"); + int pageSize; + using (FileStream stream = File.OpenRead(origin)) pageSize = JetFormatBase.Detect(stream).PageSize; var setup = new List { @@ -76,7 +77,7 @@ public void Bulk_insert_page_map(int paddingRows) new QueryEngine(database).ExecuteNonQuery(bulk); var report = new StringBuilder(); - report.AppendLine(CultureInfo.InvariantCulture, $"before: {new FileInfo(origin).Length / PageSize} pages"); + report.AppendLine(CultureInfo.InvariantCulture, $"before: {new FileInfo(origin).Length / pageSize} pages"); Describe(report, "ACE", ace); Describe(report, "LibRed", libred); output.WriteLine(report.ToString()); @@ -223,7 +224,9 @@ public void Growth_alignment_follows_the_session() command.CommandText = statement; command.ExecuteNonQuery(); } - report.AppendLine(CultureInfo.InvariantCulture, $"after setup: {new FileInfo(origin).Length / PageSize} pages"); + int pageSize; + using (FileStream stream = File.OpenRead(origin)) pageSize = JetFormatBase.Detect(stream).PageSize; + report.AppendLine(CultureInfo.InvariantCulture, $"after setup: {new FileInfo(origin).Length / pageSize} pages"); Dictionary previous = Kinds(origin); foreach ((string label, string[] sql) in runs) @@ -247,7 +250,7 @@ public void Growth_alignment_follows_the_session() } Dictionary kinds = Kinds(open); - int pages = (int)(new FileInfo(open).Length / PageSize); + int pages = (int)(new FileInfo(open).Length / pageSize); HashSet free; string owners; using (var database = JetDatabase.Open(open, readOnly: true)) @@ -316,6 +319,8 @@ private void Reservations(string columns, string index, int preloaded, int last, command.ExecuteNonQuery(); } + int pageSize; + using (FileStream stream = File.OpenRead(origin)) pageSize = JetFormatBase.Detect(stream).PageSize; Dictionary previous = []; string previousReserved = ""; foreach (int rows in samples) @@ -338,13 +343,13 @@ private void Reservations(string columns, string index, int preloaded, int last, Dictionary used = Used(open); byte[] file = File.ReadAllBytes(open); - int pages = file.Length / PageSize; + int pages = file.Length / pageSize; HashSet free; using (var database = JetDatabase.Open(open, readOnly: true)) free = GlobalFree(database.OpenTable("Bulk"), 512); int first = used.Keys.Min(); string reserved = Ranges(Enumerable.Range(first, 512 - first) - .Where(p => !free.Contains(p) && (p >= pages || file[p * PageSize] == 0 && file[p * PageSize + 1] == 0))); + .Where(p => !free.Contains(p) && (p >= pages || PageHeader.ReadType(file.AsSpan(p * pageSize)) == 0))); string freeInFile = Ranges(free.Where(p => p < pages)); var changes = new List(); @@ -450,7 +455,9 @@ public void Setup_session_reservations() runs.Add(($"preload {rows}", [.. steps.Select(s => s.Sql), $"{select}{rows} ORDER BY 1"])); var report = new StringBuilder(); - report.AppendLine(CultureInfo.InvariantCulture, $"DAO-made origin: {new FileInfo(origin).Length / PageSize} pages"); + int pageSize; + using (FileStream stream = File.OpenRead(origin)) pageSize = JetFormatBase.Detect(stream).PageSize; + report.AppendLine(CultureInfo.InvariantCulture, $"DAO-made origin: {new FileInfo(origin).Length / pageSize} pages"); try { Dictionary previous = Kinds(origin); @@ -476,7 +483,7 @@ public void Setup_session_reservations() Dictionary kinds = Kinds(open); byte[] file = File.ReadAllBytes(open); - int pages = file.Length / PageSize; + int pages = file.Length / pageSize; HashSet free; using (var database = JetDatabase.Open(open, readOnly: true)) free = GlobalFree(database.OpenTable("MSysObjects"), 512); @@ -505,12 +512,16 @@ public void Setup_session_reservations() private static Dictionary Kinds(string path) { byte[] file = File.ReadAllBytes(path); + JetFormatBase format = JetFormatBase.Detect(new MemoryStream(file)); + int pageSize = format.PageSize; var kinds = new Dictionary(); - for (int page = 1; page < file.Length / PageSize; page++) + for (int page = 1; page < file.Length / pageSize; page++) { - ReadOnlySpan p = file.AsSpan(page * PageSize, PageSize); - if (p[0] == 0 && p[1] == 0) continue; - int owner = BinaryPrimitives.ReadInt32LittleEndian(p[4..]); + ReadOnlySpan p = file.AsSpan(page * pageSize, pageSize); + if (PageHeader.ReadType(p) == 0) continue; + int owner = PageHeader.ReadType(p) == PageType.DataPage + ? (int)DataPage.ReadOwner(p, format) + : IndexTree.ReadOwner(p, format); kinds[page] = PageHeader.ReadType(p) switch { PageType.DataPage => $"D@{owner}", @@ -523,28 +534,34 @@ private static Dictionary Kinds(string path) return kinds; } - /// File length, pages whose type byte is 0 (never written), and the pages set in the global free - /// (page 1 row 0) and released (page 1 row 1) maps, whole map range. + /// File length, pages whose type byte is 0 (never written), and the pages set in the global free and + /// released maps — wherever page 0's pointers put them — whole map range. private static string Maps(string path) { byte[] file = File.ReadAllBytes(path); - int pages = file.Length / PageSize; using var database = JetDatabase.Open(path, readOnly: true); + JetFormatBase format = database.Format; + int pages = file.Length / format.PageSize; Table any = database.OpenTable("Bulk"); - var holder = new DataPage(); - holder.Read(any.Channel.ReadPageShared(1), any.Channel.Format); - var zero = Enumerable.Range(1, pages - 1).Where(p => file[p * PageSize] == 0 && file[p * PageSize + 1] == 0); - return $"{pages} pages; zero [{Ranges(zero)}]; free [{Ranges(Bits(holder.GetRow(0)))}]; " + - $"released [{Ranges(Bits(holder.GetRow(1)))}]"; - - static List Bits(ReadOnlySpan map) + (int freeRow, int freePage) = database.DefinitionPage.FreePagesMap; + (int releasedRow, int releasedPage) = database.DefinitionPage.ReleasedPagesMap; + var freeHolder = new DataPage(); + freeHolder.Read(any.Channel.ReadPageShared(freePage), any.Channel.Format); + var releasedHolder = new DataPage(); + releasedHolder.Read(any.Channel.ReadPageShared(releasedPage), any.Channel.Format); + var zero = Enumerable.Range(1, pages - 1).Where(p => PageHeader.ReadType(file.AsSpan(p * format.PageSize)) == 0); + return $"{pages} pages; zero [{Ranges(zero)}]; free [{Ranges(Bits(freeHolder.GetRow(freeRow), format))}]; " + + $"released [{Ranges(Bits(releasedHolder.GetRow(releasedRow), format))}]"; + + static List Bits(ReadOnlySpan map, JetFormatBase format) { var set = new List(); - if (map[0] != 0) { set.Add(-1); return set; } - int start = BinaryPrimitives.ReadInt32LittleEndian(map[1..]); - for (int i = 5; i < map.Length; i++) + if (UsageMap.RecordType(map) != UsageMapType.Inline) { set.Add(-1); return set; } + int start = UsageMap.StartPage(map, format); + int header = format.UsageMapInlineHeaderSize; + for (int i = header; i < map.Length; i++) for (int bit = 0; bit < 8; bit++) - if ((map[i] & (1 << bit)) != 0) set.Add(start + (i - 5) * 8 + bit); + if ((map[i] & (1 << bit)) != 0) set.Add(start + (i - header) * 8 + bit); return set; } } @@ -591,7 +608,9 @@ private void Snapshots(string columns, string index, int preloaded) command.CommandText = statement; command.ExecuteNonQuery(); } - report.AppendLine(CultureInfo.InvariantCulture, $"before: {new FileInfo(origin).Length / PageSize} pages"); + int pageSize; + using (FileStream stream = File.OpenRead(origin)) pageSize = JetFormatBase.Detect(stream).PageSize; + report.AppendLine(CultureInfo.InvariantCulture, $"before: {new FileInfo(origin).Length / pageSize} pages"); Dictionary previous = []; for (int rows = preloaded + 1; rows <= 1000; rows++) @@ -612,7 +631,7 @@ private void Snapshots(string columns, string index, int preloaded) foreach (int page in previous.Keys.Where(p => !used.ContainsKey(p)).Order()) changes.Add($"-{page}"); if (changes.Count > 0) report.AppendLine(CultureInfo.InvariantCulture, - $"{rows,5} ({new FileInfo(ace).Length / PageSize}): {string.Join(" ", changes)}"); + $"{rows,5} ({new FileInfo(ace).Length / pageSize}): {string.Join(" ", changes)}"); previous = used; if (rows % 100 == 0) report.AppendLine(CultureInfo.InvariantCulture, $" {Runs(ace)}"); } @@ -631,19 +650,21 @@ private static string Runs(string path) using var database = JetDatabase.Open(path, readOnly: true); Table bulk = database.OpenTable("Bulk"); int tdef = bulk.Definition.DefinitionPage; - HashSet free = GlobalFree(bulk, file.Length / PageSize); + JetFormatBase format = database.Format; + int pageSize = format.PageSize; + HashSet free = GlobalFree(bulk, file.Length / pageSize); var parts = new List(); int start = -1, startId = -1, nextId = -1; string kind = ""; - for (int page = tdef + 1; page <= file.Length / PageSize; page++) + for (int page = tdef + 1; page <= file.Length / pageSize; page++) { string k; int first = -1, last = -1; - if (page == file.Length / PageSize) k = "end"; + if (page == file.Length / pageSize) k = "end"; else { - ReadOnlySpan p = file.AsSpan(page * PageSize, PageSize); + ReadOnlySpan p = file.AsSpan(page * pageSize, pageSize); k = PageHeader.ReadType(p) switch { - PageType.DataPage when BinaryPrimitives.ReadInt32LittleEndian(p[4..]) == tdef => "D", + PageType.DataPage when (int)DataPage.ReadOwner(p, format) == tdef => "D", PageType.LeafIndexPage => "L", PageType.IntermediateIndexPage => "I", _ when free.Contains(page) => ".", @@ -659,7 +680,7 @@ _ when free.Contains(page) => ".", } if (k == "D") nextId = last + 1; } - return $"{file.Length / PageSize} pages | " + string.Join(" ", parts); + return $"{file.Length / pageSize} pages | " + string.Join(" ", parts); string Run(int from, int to, string what) => (from == to ? $"{from}" : $"{from}-{to}") + (what == "D" ? $"D{startId}" : what); @@ -672,13 +693,15 @@ private static Dictionary Used(string path) byte[] file = File.ReadAllBytes(path); using var database = JetDatabase.Open(path, readOnly: true); int tdef = database.OpenTable("Bulk").Definition.DefinitionPage; + JetFormatBase format = database.Format; + int pageSize = format.PageSize; var used = new Dictionary(); - for (int page = tdef + 1; page < file.Length / PageSize; page++) + for (int page = tdef + 1; page < file.Length / pageSize; page++) { - ReadOnlySpan p = file.AsSpan(page * PageSize, PageSize); + ReadOnlySpan p = file.AsSpan(page * pageSize, pageSize); string? kind = PageHeader.ReadType(p) switch { - PageType.DataPage when BinaryPrimitives.ReadInt32LittleEndian(p[4..]) == tdef => "D", + PageType.DataPage when (int)DataPage.ReadOwner(p, format) == tdef => "D", PageType.LeafIndexPage => "L", PageType.IntermediateIndexPage => "I", _ when p[0] == 0 => null, @@ -696,7 +719,7 @@ private static (int First, int Last) IdRange(Table bulk, int page) var ids = new List(); for (int row = 0; row < data.RowCount; row++) if (data.Rows[row] is { IsDeleted: false, HasOverflow: false }) - ids.Add(BinaryPrimitives.ReadInt32LittleEndian(data.GetRow(row)[2..])); + ids.Add(BinaryPrimitives.ReadInt32LittleEndian(data.GetRow(row)[bulk.Channel.Format.RowColumnCountSize..])); return ids.Count == 0 ? (-2, -2) : (ids.Min(), ids.Max()); } @@ -708,21 +731,23 @@ private static void Describe(StringBuilder report, string name, string path) int bulkTdef = bulk.Definition.DefinitionPage; var owned = new HashSet(bulk.UsageMap.DataPages()); var freeSpace = new HashSet(bulk.UsageMap.FreeDataPages()); - HashSet globalFree = GlobalFree(bulk, file.Length / PageSize); + JetFormatBase format = database.Format; + int pageSize = format.PageSize; + HashSet globalFree = GlobalFree(bulk, file.Length / pageSize); report.AppendLine(CultureInfo.InvariantCulture, - $"==== {name}: {file.Length / PageSize} pages; Bulk tdef {bulkTdef}; owned [{Ranges(owned)}]; " + + $"==== {name}: {file.Length / pageSize} pages; Bulk tdef {bulkTdef}; owned [{Ranges(owned)}]; " + $"free-space [{Ranges(freeSpace)}]; global free (in file) [{Ranges(globalFree)}]"); - for (int page = 1; page < file.Length / PageSize; page++) + for (int page = 1; page < file.Length / pageSize; page++) { - ReadOnlySpan p = file.AsSpan(page * PageSize, PageSize); + ReadOnlySpan p = file.AsSpan(page * pageSize, pageSize); PageType type = PageHeader.ReadType(p); - int tdef = BinaryPrimitives.ReadInt32LittleEndian(p[4..]); + int tdef = type == PageType.DataPage ? (int)DataPage.ReadOwner(p, format) : IndexTree.ReadOwner(p, format); string what = type switch { PageType.DataPage when tdef == bulkTdef => $"D {BulkIds(bulk, page)}", PageType.DataPage => $"d tdef {tdef}", - PageType.LeafIndexPage => $"{(tdef == bulkTdef ? "L" : "l")} tdef {tdef} entries-end 0x{BinaryPrimitives.ReadUInt16LittleEndian(p[2..]):X}", + PageType.LeafIndexPage => $"{(tdef == bulkTdef ? "L" : "l")} tdef {tdef} entries-end 0x{IndexTree.ReadFreeSpace(p, format):X}", PageType.IntermediateIndexPage => $"{(tdef == bulkTdef ? "I" : "i")} tdef {tdef}", _ => $"type 0x{p[0]:X2}", }; @@ -741,24 +766,28 @@ private static string BulkIds(Table bulk, int page) for (int row = 0; row < data.RowCount; row++) { if (data.Rows[row] is not { IsDeleted: false, HasOverflow: false }) continue; - ids.Add(BinaryPrimitives.ReadInt32LittleEndian(data.GetRow(row)[2..])); + ids.Add(BinaryPrimitives.ReadInt32LittleEndian(data.GetRow(row)[bulk.Channel.Format.RowColumnCountSize..])); } return ids.Count == 0 ? "empty" : $"{ids.Count} rows ids {ids.Min()}-{ids.Max()}"; } - /// The global free-pages map: page 1 row 0 in every file ACE writes, an inline map with a set bit - /// for a free page. + /// The global free-pages map, wherever page 0's 0x18 puts it: an inline map with a set bit for + /// a free page. private static HashSet GlobalFree(Table any, int pages) { + var page0 = new DatabaseDefinitionPage(); + page0.Read(any.Channel.ReadPageShared(0), any.Channel.Format); + (int row, int mapPage) = page0.FreePagesMap; var holder = new DataPage(); - holder.Read(any.Channel.ReadPageShared(1), any.Channel.Format); - ReadOnlySpan map = holder.GetRow(0); + holder.Read(any.Channel.ReadPageShared(mapPage), any.Channel.Format); + ReadOnlySpan map = holder.GetRow(row); var free = new HashSet(); - if (map[0] != 0) return free; - int start = BinaryPrimitives.ReadInt32LittleEndian(map[1..]); - for (int i = 5; i < map.Length; i++) + if (UsageMap.RecordType(map) != UsageMapType.Inline) return free; + int start = UsageMap.StartPage(map, any.Channel.Format); + int header = any.Channel.Format.UsageMapInlineHeaderSize; + for (int i = header; i < map.Length; i++) for (int bit = 0; bit < 8; bit++) - if ((map[i] & (1 << bit)) != 0 && start + (i - 5) * 8 + bit < pages) free.Add(start + (i - 5) * 8 + bit); + if ((map[i] & (1 << bit)) != 0 && start + (i - header) * 8 + bit < pages) free.Add(start + (i - header) * 8 + bit); return free; } @@ -778,4 +807,4 @@ private static string Ranges(IEnumerable pages) private static object? Invoke(object target, string member, params object?[] args) => target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs b/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs index dc023764..9e956702 100644 --- a/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/PermissionRowsAccessTests.cs @@ -68,7 +68,7 @@ private static string Grants(string path) .ToDictionary(r => (int)r[idCol]!, r => (string)r[nameCol]!); Table aces = db.OpenTable("MSysACEs"); - TableDef def = aces.Definition; + TableDefinition def = aces.Definition; int oid = def.RequireColumn("ObjectId").Index, sid = def.RequireColumn("SID").Index, acm = def.RequireColumn("ACM").Index, inherit = def.RequireColumn("FInheritable").Index; return string.Join("; ", aces.Rows() @@ -90,4 +90,4 @@ private static string CreateEmptyThroughDao() private static object? Invoke(object target, string member, params object?[] args) => target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs b/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs index fec7d2d1..1adbf3c7 100644 --- a/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/QueryNamesAccessTests.cs @@ -136,12 +136,12 @@ public void A_stored_query_declaring_a_form_control_round_trips_with_ace() string aceSql, libredSql; using (var db = JetDatabase.Open(byAce, readOnly: true)) { - aceSql = db.Catalog.Views["ByCity"]; - Assert.Equal("Forms!frmMenu!txtCity", db.Catalog.QueryParameters["ByCity"].Single().Name); + aceSql = db.Catalog.FindQuery("ByCity")!.Sql!; + Assert.Equal("Forms!frmMenu!txtCity", db.Catalog.FindQuery("ByCity")!.Parameters.Single().Name); Assert.Equal(["ALFKI"], new QueryEngine(db).ExecuteQuery("EXECUTE ByCity 'Berlin'").Rows.Select(r => r[0])); } using (var db = JetDatabase.Open(byLibRed, readOnly: true)) - libredSql = db.Catalog.Views["ByCity"]; + libredSql = db.Catalog.FindQuery("ByCity")!.Sql!; output.WriteLine($"ACE {aceSql}\nLibRed {libredSql}"); Assert.Equal(aceSql, libredSql); @@ -216,4 +216,4 @@ private static string Copy() => private static string Describe(IReadOnlyList columns, IEnumerable rows) => $"[{string.Join(", ", columns)}] " + string.Join(" | ", rows.Select(row => string.Join(", ", row.Select(v => v is null ? "NULL" : Convert.ToString(v, CultureInfo.InvariantCulture))))); -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/ReferencesAndIdentityAccessTests.cs b/test/LibRed.Engine.AccessTests/ReferencesAndIdentityAccessTests.cs index 74c7583c..10c680a3 100644 --- a/test/LibRed.Engine.AccessTests/ReferencesAndIdentityAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/ReferencesAndIdentityAccessTests.cs @@ -279,7 +279,7 @@ private static (string Description, string? Error) Run(string[] sql, string tabl string? counter; using (var db = JetDatabase.Open(path)) { - TableDef t = db.Catalog.FindTable(table)!; + TableDefinition t = db.Catalog.FindTable(table)!; description.AddRange(t.Columns.Select(c => $"{c.Name} {c.Type} autonumber={c.IsAutoNumber} required={!c.IsNullable}")); description.AddRange(db.Catalog.ForeignKeysOf(table).Select(fk => @@ -312,4 +312,4 @@ private static (string Description, string? Error) Run(string[] sql, string tabl return (string.Join("; ", description), null); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/RelationshipByteParityAccessTests.cs b/test/LibRed.Engine.AccessTests/RelationshipByteParityAccessTests.cs index 016aa228..41093c71 100644 --- a/test/LibRed.Engine.AccessTests/RelationshipByteParityAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/RelationshipByteParityAccessTests.cs @@ -1,7 +1,7 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.Catalog; using LibRed.IO; +using LibRed.Pages; using Xunit; namespace LibRed.Engine.Tests; @@ -82,7 +82,7 @@ private static void LibRedRun(string path, string[] statements) try { using var database = JetDatabase.Open(path, readOnly: true); - TableDef rel = database.Catalog.FindTable("MSysRelationships")!; + TableDefinition rel = database.Catalog.FindTable("MSysRelationships")!; int Col(string name) => rel.Columns.Single(c => c.Name == name).Index; int szObject = Col("szObject"), szReferenced = Col("szReferencedObject"); int szColumn = Col("szColumn"), szReferencedColumn = Col("szReferencedColumn"); @@ -112,16 +112,13 @@ private static (byte[] Bytes, string IndexNames)? Definition( string names; using (var database = JetDatabase.Open(path, readOnly: true)) { - TableDef def = database.Catalog.FindTable(table)!; + TableDefinition def = database.Catalog.FindTable(table)!; definitionPage = def.DefinitionPage; names = string.Join(", ", def.Indexes.Select(i => i.Name)); } using var channel = PageChannel.Open(path, readOnly: true); - byte[] page = channel.ReadPage(definitionPage).Span.ToArray(); - int length = BinaryPrimitives.ReadInt32LittleEndian( - page.AsSpan(channel.Format.TdefLengthOffset, 4)); - return (length > 0 && length <= page.Length ? page.AsSpan(0, length).ToArray() : page, names); + return (TableDefinition.ReadChain(channel, definitionPage).Buffer.Span.ToArray(), names); } finally { TemporaryDatabase.Delete(path); } } @@ -135,4 +132,4 @@ private static string Build(string childSql, Action create) catch (OleDbException) { TemporaryDatabase.Delete(path); return null!; } return path; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/RelocatedRowDeleteAccessTests.cs b/test/LibRed.Engine.AccessTests/RelocatedRowDeleteAccessTests.cs index cd9b776f..d9b51817 100644 --- a/test/LibRed.Engine.AccessTests/RelocatedRowDeleteAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/RelocatedRowDeleteAccessTests.cs @@ -1,7 +1,7 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.Formats; using LibRed.IO; +using LibRed.Pages; using Xunit; namespace LibRed.Engine.Tests; @@ -114,18 +114,18 @@ private static string Describe(string[] statements, Action run for (int page = 1; page < channel.PageCount; page++) { byte[] bytes = channel.ReadPage(page).Span.ToArray(); - if (bytes[0] != 0x01) continue; - if (BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4, 4)) != definitionPage) continue; + if (PageHeader.ReadType(bytes) != PageType.DataPage) continue; + if ((int)DataPage.ReadOwner(bytes, format) != definitionPage) continue; - int slots = BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(format.DataRowCountOffset, 2)); + int slots = DataPage.ReadRowCount(bytes, format); var entries = Enumerable.Range(0, slots) - .Select(i => BinaryPrimitives.ReadUInt16LittleEndian( - bytes.AsSpan(format.DataRowDirectoryOffset + i * 2, 2)).ToString("X4")); - pages.Add($"free={BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(format.DataFreeSpaceOffset, 2))}" + .Select(i => DataPage.ReadSlot(bytes, format, i)) + .Select(s => ((int)s.Flags | s.Offset).ToString("X4")); + pages.Add($"free={DataPage.ReadFreeSpace(bytes, format)}" + $" [{string.Join(" ", entries)}]"); } return string.Join(" | ", pages); } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/RelocatedRowGrowthAccessTests.cs b/test/LibRed.Engine.AccessTests/RelocatedRowGrowthAccessTests.cs index 5c3a2e99..e02ff92a 100644 --- a/test/LibRed.Engine.AccessTests/RelocatedRowGrowthAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/RelocatedRowGrowthAccessTests.cs @@ -1,7 +1,7 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.Formats; using LibRed.IO; +using LibRed.Pages; using Xunit; namespace LibRed.Engine.Tests; @@ -113,18 +113,18 @@ private static string Describe(Action run) for (int page = 1; page < channel.PageCount; page++) { byte[] bytes = channel.ReadPage(page).Span.ToArray(); - if (bytes[0] != 0x01) continue; - if (BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4, 4)) != definitionPage) continue; + if (PageHeader.ReadType(bytes) != PageType.DataPage) continue; + if ((int)DataPage.ReadOwner(bytes, format) != definitionPage) continue; - int slots = BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(format.DataRowCountOffset, 2)); + int slots = DataPage.ReadRowCount(bytes, format); var entries = Enumerable.Range(0, slots) - .Select(i => BinaryPrimitives.ReadUInt16LittleEndian( - bytes.AsSpan(format.DataRowDirectoryOffset + i * 2, 2)).ToString("X4")); - pages.Add($"free={BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(format.DataFreeSpaceOffset, 2))}" + .Select(i => DataPage.ReadSlot(bytes, format, i)) + .Select(s => ((int)s.Flags | s.Offset).ToString("X4")); + pages.Add($"free={DataPage.ReadFreeSpace(bytes, format)}" + $" [{string.Join(" ", entries)}]"); } return string.Join(" | ", pages); } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/RowByteParityAccessTests.cs b/test/LibRed.Engine.AccessTests/RowByteParityAccessTests.cs index 1b7b8bc7..b74736e1 100644 --- a/test/LibRed.Engine.AccessTests/RowByteParityAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/RowByteParityAccessTests.cs @@ -1,7 +1,7 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.Formats; using LibRed.IO; +using LibRed.Pages; using Xunit; namespace LibRed.Engine.Tests; @@ -159,21 +159,19 @@ private static void LibRedRun(string path, string[] statements) for (int page = 1; page < channel.PageCount; page++) { byte[] bytes = channel.ReadPage(page).Span.ToArray(); - if (bytes[0] != 0x01) continue; - if (BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(4, 4)) != definitionPage) continue; - if (BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(format.DataRowCountOffset, 2)) <= row) + if (PageHeader.ReadType(bytes) != PageType.DataPage) continue; + if ((int)DataPage.ReadOwner(bytes, format) != definitionPage) continue; + if (DataPage.ReadRowCount(bytes, format) <= row) continue; // Slot offsets are non-increasing, so a row runs from its own offset to the previous slot's // (the page end for row 0). - int Offset(int i) => BinaryPrimitives.ReadUInt16LittleEndian( - bytes.AsSpan(format.DataRowDirectoryOffset + i * 2, 2)) & 0x1FFF; - int start = Offset(row); - int end = row == 0 ? format.PageSize : Offset(row - 1); + int start = DataPage.ReadSlot(bytes, format, row).Offset; + int end = row == 0 ? format.PageSize : DataPage.ReadSlot(bytes, format, row - 1).Offset; return bytes.AsSpan(start, end - start).ToArray(); } return null; } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/SelectIntoAccessTests.cs b/test/LibRed.Engine.AccessTests/SelectIntoAccessTests.cs index dae9e98a..e6ebb051 100644 --- a/test/LibRed.Engine.AccessTests/SelectIntoAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/SelectIntoAccessTests.cs @@ -76,7 +76,7 @@ private static void AssertSameAsAce(string makeTable, string verify) private static string Describe(JetDatabase db, string table) { - TableDef? def = db.Catalog.Tables.FirstOrDefault(t => t.Name == table); + TableDefinition? def = db.Catalog.Tables.FirstOrDefault(t => t.Name == table); if (def is null) return "(not created)"; return string.Join(", ", def.Columns.Select(c => $"{c.Name} {c.Type}({c.Length})")) + " | indexes: " + (def.Indexes.Count == 0 ? "(none)" : string.Join(", ", def.Indexes.Select(i => i.Name))); @@ -106,4 +106,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/SelectIntoShapeProbeTest.cs b/test/LibRed.Engine.AccessTests/SelectIntoShapeProbeTest.cs index c5434471..5dc83afa 100644 --- a/test/LibRed.Engine.AccessTests/SelectIntoShapeProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/SelectIntoShapeProbeTest.cs @@ -84,7 +84,7 @@ public void Probe_make_table_shape() using var db = JetDatabase.Open(path); foreach (string table in (string[])["SiSrc", "SiA", "SiB", "SiC", "SiD", "SiE", "SiF"]) { - TableDef? def = db.Catalog.Tables.FirstOrDefault(t => t.Name == table); + TableDefinition? def = db.Catalog.Tables.FirstOrDefault(t => t.Name == table); if (def is null) { output.WriteLine($" {table}: not created"); continue; } output.WriteLine($" {table}: " + string.Join(", ", def.Columns.Select(c => @@ -106,4 +106,4 @@ private static void Exec(OleDbConnection connection, string sql) command.CommandText = sql; command.ExecuteNonQuery(); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs b/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs index cd2771f5..f5e4c542 100644 --- a/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs @@ -228,7 +228,9 @@ public void An_owneraccess_query_is_stored_as_ace_stores_it(string sql) // LibRed reads the declaration back with the query, and runs it. using var ours = TemporaryDatabase.OpenTracked(ourPath, readOnly: false); - string? stored = action ? ours.Catalog.ActionQueries["Q"].Sql : ours.Catalog.Views["Q"]; + StoredQuery query = ours.Catalog.FindQuery("Q")!; + Assert.Equal(action, query.IsAction); + string? stored = query.Sql; Assert.EndsWith(" WITH OWNERACCESS OPTION", stored); var engine = new QueryEngine(ours); if (action) engine.ExecuteStoredActionQuery("Q"); @@ -296,7 +298,7 @@ public void A_written_action_query_reads_back_as_the_statement_it_was_written_fr "CREATE PROCEDURE [P] AS UPDATE Customers SET ContactTitle = 'Owner' WHERE Country = 'UK'"); // Round trip: what was written is read back as runnable SQL, and running it by name works. - StoredActionQuery stored = db.Catalog.ActionQueries["P"]; + StoredQuery stored = db.Catalog.FindQuery("P")!; Assert.Null(stored.UnsupportedReason); Assert.Equal("UPDATE [Customers] SET [ContactTitle] = 'Owner' WHERE Country = 'UK'", stored.Sql); Assert.Equal(7, engine.ExecuteNonQuery("EXECUTE [P]")); @@ -316,7 +318,7 @@ public void A_written_action_query_reads_back_as_the_statement_it_was_written_fr private static List QueryRows(string path, string queryName) { using var db = JetDatabase.Open(path); - TableDef queries = db.Catalog.FindTable("MSysQueries")!; + TableDefinition queries = db.Catalog.FindTable("MSysQueries")!; int objectIdIndex = Index(queries, "ObjectId"); int attributeIndex = Index(queries, "Attribute"); int id = QueryObjectId(db, queryName); @@ -342,7 +344,7 @@ private static List QueryRows(string path, string queryName) private static int ObjectFlags(string path, string queryName) { using var db = JetDatabase.Open(path); - TableDef objects = db.Catalog.FindTable("MSysObjects")!; + TableDefinition objects = db.Catalog.FindTable("MSysObjects")!; int flagsIndex = Index(objects, "Flags"); int idIndex = Index(objects, "Id"); int id = QueryObjectId(db, queryName); @@ -351,11 +353,11 @@ private static int ObjectFlags(string path, string queryName) private static int QueryObjectId(JetDatabase db, string queryName) { - TableDef objects = db.Catalog.FindTable("MSysObjects")!; + TableDefinition objects = db.Catalog.FindTable("MSysObjects")!; int idIndex = Index(objects, "Id"), nameIndex = Index(objects, "Name"); return (int)db.OpenTable("MSysObjects").Rows() .Single(row => string.Equals(row[nameIndex] as string, queryName, StringComparison.OrdinalIgnoreCase))[idIndex]!; } - private static int Index(TableDef table, string column) => table.FindColumn(column)!.Index; -} + private static int Index(TableDefinition table, string column) => table.FindColumn(column)!.Index; +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/StoredParameterNameAccessTests.cs b/test/LibRed.Engine.AccessTests/StoredParameterNameAccessTests.cs index 2544966e..82f8c3a9 100644 --- a/test/LibRed.Engine.AccessTests/StoredParameterNameAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/StoredParameterNameAccessTests.cs @@ -42,7 +42,7 @@ public void A_parameter_is_stored_as_ace_stores_it_and_reads_back_by_its_name(st foreach (string path in new[] { acePath, libredPath }) { using var db = JetDatabase.Open(path); - Assert.Equal([name], db.Catalog.QueryParameters["P"].Select(p => p.Name)); + Assert.Equal([name], db.Catalog.FindQuery("P")!.Parameters.Select(p => p.Name)); var engine = new QueryEngine(db); Assert.Equal(["ALFKI"], engine.ExecuteQuery("EXECUTE P 'Maria Anders'").Rows.Select(r => r[0])); // As a table source, bound by the name reported for it — what a consumer reading the schema does. @@ -71,4 +71,4 @@ public void A_parameter_is_stored_as_ace_stores_it_and_reads_back_by_its_name(st return db.OpenTable("MSysQueries").Rows() .Single(r => r[attr] is byte a && a == 2 && Equals(r[owner], procId))[name1] as string; } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/TdefByteParityAccessTests.cs b/test/LibRed.Engine.AccessTests/TdefByteParityAccessTests.cs index 04c34e55..e53c4b92 100644 --- a/test/LibRed.Engine.AccessTests/TdefByteParityAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/TdefByteParityAccessTests.cs @@ -1,8 +1,8 @@ -using System.Buffers.Binary; using System.Data.OleDb; using LibRed.Catalog; using LibRed.Formats; using LibRed.IO; +using LibRed.Pages; using Xunit; namespace LibRed.Engine.Tests; @@ -16,7 +16,7 @@ namespace LibRed.Engine.Tests; // with one measured exception recorded in Usage_map_rows_follow_declaration_order below. // // Constraints are named deliberately. An unnamed primary key makes ACE generate `Index_` from nothing -// reproducible while LibRed picks the stable "PrimaryKey" — an engine choice documented in TableCreator — +// reproducible while LibRed picks the stable "PrimaryKey" — an engine choice documented in SchemaEditor — // so an unnamed key would only ever measure that known difference. [Collection(AceCollection.Name)] public class TdefByteParityAccessTests(ITestOutputHelper output) : TempDatabaseTest @@ -46,12 +46,12 @@ public void The_whole_definition_matches_ace(string label, string sql) var aceDef = Definition(sql, AceCreate); Assert.SkipWhen(aceDef is null, $"ACE would not create {label}."); - (byte[] ace, JetFormatBase format) = aceDef!.Value; + (byte[] ace, JetFormatBase format, TableDefinition aceParsed) = aceDef!.Value; byte[] libred = Definition(sql, LibRedCreate)!.Value.Bytes; // An inline key leaves ACE naming the index unreproducibly; compare those shapes by every region // except the names, and the named-constraint shapes whole. - bool generatedName = IndexNames(ace, format).Any(n => n.StartsWith("Index_", StringComparison.Ordinal)); + bool generatedName = IndexNames(aceParsed).Any(n => n.StartsWith("Index_", StringComparison.Ordinal)); foreach ((string region, int start, int end) in Regions(ace, format)) { if (generatedName && region is "index-names" or "header") continue; @@ -61,7 +61,7 @@ public void The_whole_definition_matches_ace(string label, string sql) } if (!generatedName) Assert.Equal(ace.Length, libred.Length); - output.WriteLine($"{label}: {ace.Length} bytes, index names [{string.Join(", ", IndexNames(ace, format))}]"); + output.WriteLine($"{label}: {ace.Length} bytes, index names [{string.Join(", ", IndexNames(aceParsed))}]"); } // Usage-map rows follow DECLARATION order: an inline PRIMARY KEY on the first column is declared before the @@ -103,77 +103,38 @@ private static void LibRedCreate(string path, string sql) /// Which usage-map row each index and long-value column was given. private static string Rows(string sql, Action create) { - (byte[] def, JetFormatBase format) = Definition(sql, create)!.Value; - int dataCount = BinaryPrimitives.ReadInt32LittleEndian(def.AsSpan(format.TdefIndexCountOffset, 4)); - (string _, int dataStart, int _) = Regions(def, format).Single(r => r.Name == "index-data-blocks"); - (string _, int lvalStart, int lvalEnd) = Regions(def, format).Single(r => r.Name == "long-value-region"); - - var indexes = Enumerable.Range(0, dataCount) - .Select(i => def[dataStart + i * DataBlockSize + UsageMapRowOffset].ToString()); - - var columns = new List(); - for (int pos = lvalStart; pos + 10 <= lvalEnd - && BinaryPrimitives.ReadUInt16LittleEndian(def.AsSpan(pos, 2)) != 0xFFFF; pos += 10) - columns.Add($"{def[pos + 2]},{def[pos + 6]}"); - + TableDefinition definition = Definition(sql, create)!.Value.Parsed; + var indexes = definition.Indexes.OrderBy(i => i.RealIndexOrdinal).Select(i => i.UsageMap.Row); + var columns = definition.LongValueOwnedMaps.OrderBy(e => e.Key) + .Select(e => $"{e.Value.Row},{definition.LongValueFreeMaps[e.Key].Row}"); return $"index [{string.Join(",", indexes)}] long-value [{string.Join(" ", columns)}]"; } - // IndexBlockFormat is internal to Core; these are its DataBlockSize / InfoBlockSize / UsageMapRowOffset. - private const int DataBlockSize = 52; - private const int InfoBlockSize = 28; - private const int UsageMapRowOffset = 0x22; + private static List IndexNames(TableDefinition definition) => + [.. definition.LogicalIndexes.Select(l => l.Name)]; - private static List IndexNames(byte[] def, JetFormatBase format) - { - (string _, int start, int end) = Regions(def, format).Single(r => r.Name == "index-names"); - var names = new List(); - for (int pos = start; pos + 2 <= end;) - { - int len = BinaryPrimitives.ReadUInt16LittleEndian(def.AsSpan(pos, 2)); - if (len == 0 || pos + 2 + len > end) break; - names.Add(System.Text.Encoding.Unicode.GetString(def.AsSpan(pos + 2, len))); - pos += 2 + len; - } - return names; - } - - /// The TDEF's regions in order, derived from its own counts. + /// The TDEF's regions in order, through the walk the engine reads and writes it with. private static IEnumerable<(string Name, int Start, int End)> Regions(byte[] def, JetFormatBase format) { - int dataCount = BinaryPrimitives.ReadInt32LittleEndian(def.AsSpan(format.TdefIndexCountOffset, 4)); - int logicalCount = BinaryPrimitives.ReadInt32LittleEndian( - def.AsSpan(format.TdefLogicalIndexCountOffset, 4)); - int colCount = BinaryPrimitives.ReadUInt16LittleEndian(def.AsSpan(format.TdefColumnCountOffset, 2)); - - int pos = format.TdefRealIndexBlockOffset; - yield return ("header", 0, pos); - - yield return ("index-stats", pos, pos + dataCount * format.RealIndexEntrySize); - pos += dataCount * format.RealIndexEntrySize; - - yield return ("column-descriptors", pos, pos + colCount * format.ColumnDescriptorSize); - pos += colCount * format.ColumnDescriptorSize; - - int namesStart = pos; - for (int i = 0; i < colCount; i++) pos += 2 + BinaryPrimitives.ReadUInt16LittleEndian(def.AsSpan(pos, 2)); - yield return ("column-names", namesStart, pos); - - yield return ("index-data-blocks", pos, pos + dataCount * DataBlockSize); - pos += dataCount * DataBlockSize; - - yield return ("index-info-blocks", pos, pos + logicalCount * InfoBlockSize); - pos += logicalCount * InfoBlockSize; - - int indexNames = pos; - for (int i = 0; i < logicalCount && pos + 2 <= def.Length; i++) - pos += 2 + BinaryPrimitives.ReadUInt16LittleEndian(def.AsSpan(pos, 2)); - yield return ("index-names", indexNames, pos); - - yield return ("long-value-region", pos, def.Length); + TableDefinition.Regions regions = TableDefinition.Regions.Of(def, format); + int columnNames = regions.ColumnDescriptors + regions.ColumnCount * format.ColumnDescriptorSize; + int indexNamesEnd = regions.IndexNames; + for (int i = 0; i < regions.LogicalCount; i++) + indexNamesEnd = TableDefinition.NameEntryEnd(def, indexNamesEnd, format, $"index {i}"); + + yield return ("header", 0, regions.Stats); + yield return ("index-stats", regions.Stats, regions.ColumnDescriptors); + yield return ("column-descriptors", regions.ColumnDescriptors, columnNames); + yield return ("column-names", columnNames, regions.DataBlocks); + yield return ("index-data-blocks", regions.DataBlocks, regions.InfoBlocks); + yield return ("index-info-blocks", regions.InfoBlocks, regions.IndexNames); + yield return ("index-names", regions.IndexNames, indexNamesEnd); + yield return ("long-value-region", indexNamesEnd, def.Length); } - private static (byte[] Bytes, JetFormatBase Format)? Definition(string sql, Action create) + /// Table W's whole definition, continuation pages stitched in, and that definition parsed. + private static (byte[] Bytes, JetFormatBase Format, TableDefinition Parsed)? Definition( + string sql, Action create) { string path = TemporaryDatabase.CopyPath( Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "tdef-parity-"); @@ -182,16 +143,12 @@ private static (byte[] Bytes, JetFormatBase Format)? Definition(string sql, Acti try { create(path, sql); } catch (OleDbException) { return null; } - int definitionPage; - using (var database = JetDatabase.Open(path, readOnly: true)) - definitionPage = database.Catalog.FindTable("W")!.DefinitionPage; - - using var channel = PageChannel.Open(path, readOnly: true); - JetFormatBase format = channel.Format; - byte[] page = channel.ReadPage(definitionPage).Span.ToArray(); - int length = BinaryPrimitives.ReadInt32LittleEndian(page.AsSpan(format.TdefLengthOffset, 4)); - return (length > 0 && length <= page.Length ? page.AsSpan(0, length).ToArray() : page, format); + using var database = JetDatabase.Open(path, readOnly: true); + (PageBuffer buffer, _) = TableDefinition.ReadChain(database.Channel, database.Catalog.FindTable("W")!.DefinitionPage); + var parsed = new TableDefinition(); + parsed.Read(buffer, database.Format); + return (buffer.Span.ToArray(), database.Format, parsed); } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/TransactionalDdlRollbackAccessTests.cs b/test/LibRed.Engine.AccessTests/TransactionalDdlRollbackAccessTests.cs index f272560b..033f33d1 100644 --- a/test/LibRed.Engine.AccessTests/TransactionalDdlRollbackAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/TransactionalDdlRollbackAccessTests.cs @@ -27,14 +27,14 @@ public void Full_rollback_removes_created_index_view_table_and_column_alter_byte e.ExecuteNonQuery("CREATE TABLE TransientDdl (Id LONG PRIMARY KEY)"); e.ExecuteNonQuery("ALTER TABLE DdlTxn ALTER COLUMN V TEXT(80)"); - TableDef changed = db.Catalog.FindTable("DdlTxn")!; + TableDefinition changed = db.Catalog.FindTable("DdlTxn")!; Assert.Contains(changed.Indexes, i => i.Name == "UX_DdlTxn_Code"); Assert.Equal(160, changed.FindColumn("V")!.Length); Assert.NotNull(db.Catalog.FindTable("TransientDdl")); Assert.Single(e.ExecuteQuery("SELECT Id FROM DdlTxnView").Rows); e.ExecuteNonQuery("ROLLBACK"); - TableDef restored = db.Catalog.FindTable("DdlTxn")!; + TableDefinition restored = db.Catalog.FindTable("DdlTxn")!; Assert.DoesNotContain(restored.Indexes, i => i.Name == "UX_DdlTxn_Code"); Assert.Equal(40, restored.FindColumn("V")!.Length); Assert.Null(db.Catalog.FindTable("TransientDdl")); @@ -75,7 +75,7 @@ public void Full_rollback_restores_dropped_objects_and_rebuilt_column_byte_for_b Assert.Throws(() => e.ExecuteQuery("SELECT Id FROM DdlTxnView")); e.ExecuteNonQuery("ROLLBACK"); - TableDef restored = db.Catalog.FindTable("DdlTxn")!; + TableDefinition restored = db.Catalog.FindTable("DdlTxn")!; Assert.Contains(restored.Indexes, i => i.Name == "UX_DdlTxn_Code"); Assert.Equal(JetDataType.Int32, restored.FindColumn("N")!.Type); Assert.NotNull(db.Catalog.FindTable("KeptTable")); @@ -113,7 +113,7 @@ public void Inner_ddl_rollback_restores_outer_created_objects_which_then_commit_ e.ExecuteNonQuery("CREATE TABLE InnerOnly (Id LONG PRIMARY KEY)"); e.ExecuteNonQuery("ROLLBACK"); - TableDef restoredOuter = db.Catalog.FindTable("DdlTxn")!; + TableDefinition restoredOuter = db.Catalog.FindTable("DdlTxn")!; Assert.Contains(restoredOuter.Indexes, i => i.Name == "UX_DdlTxn_Code"); Assert.Equal(JetDataType.Int32, restoredOuter.FindColumn("N")!.Type); Assert.Null(db.Catalog.FindTable("InnerOnly")); @@ -163,4 +163,4 @@ private static void Execute(OleDbConnection connection, string sql) private static void AssertScalar(OleDbConnection connection, string sql, int expected) => Assert.Equal(expected, Convert.ToInt32(ExecuteScalar(connection, sql))); -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs index a08fe974..a9d197ed 100644 --- a/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs +++ b/test/LibRed.Engine.AccessTests/WholeFileParityProbeTest.cs @@ -5,6 +5,7 @@ using System.Text; using LibRed; using LibRed.Catalog; +using LibRed.Formats; using LibRed.Pages; using LibRed.Storage; using Xunit; @@ -31,8 +32,6 @@ namespace LibRed.Engine.Tests; [Collection(AceCollection.Name)] public class WholeFileParityProbeTest(ITestOutputHelper output) { - private const int PageSize = 4096; - [Fact] public void The_whole_run_leaves_libred_and_ace_with_the_same_file() { @@ -316,28 +315,30 @@ private static (string Report, int Pages) Compare(string originPath, string aceP { byte[] origin = File.ReadAllBytes(originPath), ace = File.ReadAllBytes(acePath), noise = File.ReadAllBytes(noisePath), libred = File.ReadAllBytes(libredPath); + JetFormatBase format = JetFormatBase.Detect(new MemoryStream(origin)); + int pageSize = format.PageSize; Dictionary owners = Owners(acePath, libredPath); - HashSet[] masks = [.. Enumerable.Range(0, Math.Max(ace.Length, libred.Length) / PageSize).Select(_ => new HashSet())]; + HashSet[] masks = [.. Enumerable.Range(0, Math.Max(ace.Length, libred.Length) / pageSize).Select(_ => new HashSet())]; Mask(acePath, ace, masks); Mask(libredPath, libred, masks); - int acePages = ace.Length / PageSize, libredPages = libred.Length / PageSize; + int acePages = ace.Length / pageSize, libredPages = libred.Length / pageSize; int identical = 0, housekeeping = 0, differingPages = 0; var groups = new SortedDictionary>(StringComparer.Ordinal); for (int page = 0; page < Math.Max(acePages, libredPages); page++) { - int at = page * PageSize; + int at = page * pageSize; if (page >= acePages || page >= libredPages) { byte[] only = page >= acePages ? libred : ace; Add(groups, "pages in one file only", $"page {page}: {(page >= acePages ? "LibRed" : "ACE")} only, " + - $"type 0x{only[at]:X2} {Owner(owners, only, at)}"); + $"type 0x{only[at]:X2} {Owner(owners, format, only, at)}"); differingPages++; continue; } var differing = new List(); - for (int i = 0; i < PageSize; i++) + for (int i = 0; i < pageSize; i++) { if (ace[at + i] == libred[at + i] || masks[page].Contains(i)) continue; // A byte ACE writes merely for holding a session, which LibRed need not reproduce. @@ -349,13 +350,13 @@ private static (string Report, int Pages) Compare(string originPath, string aceP differingPages++; string detail = string.Join(" ", differing.Take(8).Select(i => $"+0x{i:X3}:{ace[at + i]:X2}/{libred[at + i]:X2}")); string kind = page == 0 ? "page 0" - : $"type 0x{ace[at]:X2}{(ace[at] != libred[at] ? $"/0x{libred[at]:X2}" : "")} {Owner(owners, ace, at)}"; + : $"type 0x{ace[at]:X2}{(ace[at] != libred[at] ? $"/0x{libred[at]:X2}" : "")} {Owner(owners, format, ace, at)}"; Add(groups, kind, $"page {page}: {differing.Count} bytes {detail}"); } var report = new StringBuilder(); report.AppendLine(CultureInfo.InvariantCulture, - $"pages: before {origin.Length / PageSize}, ACE {acePages}, LibRed {libredPages}; identical {identical}; " + + $"pages: before {origin.Length / pageSize}, ACE {acePages}, LibRed {libredPages}; identical {identical}; " + $"{housekeeping} byte(s) differ only where ACE's session alone writes (ace/libred shown)"); foreach (var (kind, lines) in groups) { @@ -373,14 +374,18 @@ private static void Add(SortedDictionary> groups, string ki /// What owns a page: for a data or index page the table whose TDEF page its header names, for a TDEF /// page its own table, where either file's catalog knows the name. - private static string Owner(Dictionary owners, byte[] file, int at) + private static string Owner(Dictionary owners, JetFormatBase format, byte[] file, int at) { if (PageHeader.ReadType(file.AsSpan(at)) is PageType.TableDefinition or PageType.ReleasedTableDefinition) - return owners.TryGetValue(at / PageSize, out string? self) ? $"({self})" : ""; + return owners.TryGetValue(at / format.PageSize, out string? self) ? $"({self})" : ""; if (PageHeader.ReadType(file.AsSpan(at)) is not (PageType.DataPage or PageType.IntermediateIndexPage or PageType.LeafIndexPage)) return ""; - if (file.AsSpan(at + 4, 4).SequenceEqual("LVAL"u8)) return "(long values)"; - int owner = BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(at + 4, 4)); + ReadOnlySpan page = file.AsSpan(at, format.PageSize); + uint ownerField = PageHeader.ReadType(page) == PageType.DataPage + ? DataPage.ReadOwner(page, format) + : (uint)IndexTree.ReadOwner(page, format); + if (ownerField == JetFormatBase.LongValuePageMarker) return "(long values)"; + int owner = (int)ownerField; return owners.TryGetValue(owner, out string? name) ? $"({name})" : $"(tdef {owner})"; } @@ -390,7 +395,7 @@ private static Dictionary Owners(params string[] paths) foreach (string path in paths) { using var database = JetDatabase.Open(path, readOnly: true); - foreach (TableDef table in database.Catalog.Tables) owners.TryAdd(table.DefinitionPage, table.Name); + foreach (TableDefinition table in database.Catalog.Tables) owners.TryAdd(table.DefinitionPage, table.Name); } return owners; } @@ -401,33 +406,36 @@ private static Dictionary Owners(params string[] paths) /// write stamp. private static void Mask(string path, byte[] file, HashSet[] masks) { - for (int i = 0xE00; i < PageSize; i++) masks[0].Add(i); - using var database = JetDatabase.Open(path, readOnly: true); - TableDef objects = database.Catalog.FindTable("MSysObjects")!; + JetFormatBase format = database.Format; + int pageSize = format.PageSize; + + for (int i = format.CommitByteTableOffset; i < pageSize; i++) masks[0].Add(i); + + TableDefinition objects = database.Catalog.FindTable("MSysObjects")!; int[] dates = [.. new[] { "DateCreate", "DateUpdate" }.Select(c => objects.FindColumn(c)!.FixedOffset)]; - for (int page = 1; page < file.Length / PageSize; page++) + for (int page = 1; page < file.Length / pageSize; page++) { - int at = page * PageSize; + int at = page * pageSize; if (PageHeader.ReadType(file.AsSpan(at)) != PageType.DataPage) continue; - for (int i = 0x08; i < 0x0C; i++) masks[page].Add(i); - if (BinaryPrimitives.ReadInt32LittleEndian(file.AsSpan(at + 4, 4)) != objects.DefinitionPage) continue; + for (int i = format.DataChainStampOffset; i < format.DataChainStampOffset + sizeof(int); i++) masks[page].Add(i); + ReadOnlySpan bytes = file.AsSpan(at, pageSize); + if ((int)DataPage.ReadOwner(bytes, format) != objects.DefinitionPage) continue; - int rows = BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(at + 0x0C, 2)); + int rows = DataPage.ReadRowCount(bytes, format); for (int row = 0; row < rows; row++) { - int slot = BinaryPrimitives.ReadUInt16LittleEndian(file.AsSpan(at + 0x0E + 2 * row, 2)); - if ((slot & 0xC000) != 0) continue; - int start = slot & 0x1FFF; + (int start, RowSlotFlags flags) = DataPage.ReadSlot(bytes, format, row); + if ((flags & (RowSlotFlags.Deleted | RowSlotFlags.Overflow)) != 0) continue; foreach (int offset in dates) - for (int i = 0; i < 8; i++) masks[page].Add(start + 2 + offset + i); + for (int i = 0; i < 8; i++) masks[page].Add(start + format.RowColumnCountSize + offset + i); } } // A chained long value's descriptor carries the same write stamp as the chain's first page, bytes 8–11 of // its twelve (long-values.md). - foreach (TableDef definition in database.Catalog.Tables) + foreach (TableDefinition definition in database.Catalog.Tables) { if (!definition.Columns.Any(c => c.Type is JetDataType.Memo or JetDataType.Ole)) continue; Table table = database.OpenTable(definition.Name); @@ -437,12 +445,14 @@ private static void Mask(string path, byte[] file, HashSet[] masks) page.Read(table.Channel.ReadPageShared(id.Page), table.Channel.Format); if (page.Rows[id.Row] is not { IsDeleted: false, HasOverflow: false } slot) continue; ReadOnlySpan row = page.GetRow(id.Row); - foreach (byte[] descriptor in RowDecoder.LongValueDescriptors(definition.Columns, table.Channel.Format, row).Values) + foreach (byte[] descriptor in RowCodec.LongValueDescriptors(definition.Columns, table.Channel.Format, row).Values) { - if (descriptor.Length < 12 || descriptor[3] != 0x00) continue; + if (descriptor.Length < format.LongValueDescriptorSize + || LongValueStore.Read(descriptor, format).Storage != LongValueStore.StorageKind.Chained) continue; int at = row.IndexOf(descriptor); + int stamp = format.LongValueDescriptorChainStampOffset; if (at >= 0) - for (int i = 8; i < 12; i++) masks[id.Page].Add(slot.Offset + at + i); + for (int i = stamp; i < stamp + sizeof(int); i++) masks[id.Page].Add(slot.Offset + at + i); } } } @@ -462,4 +472,4 @@ private static void Mask(string path, byte[] file, HashSet[] masks) private static object? Invoke(object target, string member, params object?[] args) => target.GetType().InvokeMember(member, BindingFlags.InvokeMethod, null, target, args); -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/CollationComparisonTests.cs b/test/LibRed.Engine.Tests/CollationComparisonTests.cs index 30cea8a8..84691efa 100644 --- a/test/LibRed.Engine.Tests/CollationComparisonTests.cs +++ b/test/LibRed.Engine.Tests/CollationComparisonTests.cs @@ -29,7 +29,7 @@ private static (QueryEngine Engine, string[] Order) Seeded(string order) { Collation collation = order == "croatian" ? CroatianV1 : GeneralV1; string path = TemporaryDatabase.CreatePath("collation-comparison-"); - DatabaseCreator.CreateEmpty(path, collation: collation); + JetDatabase.Create(path, collation: collation); var e = new QueryEngine(TemporaryDatabase.OpenTracked(path)); e.ExecuteNonQuery("CREATE TABLE T (Id LONG, S TEXT(20))"); e.ExecuteNonQuery("CREATE TABLE U (K TEXT(20), N LONG)"); @@ -112,7 +112,7 @@ public void Group_by_folds_what_the_collation_folds(byte version) // As ACE groups them (CollationQueryProbeTests): 'ß' is 'ss', case and trailing spaces fold, an accent // separates — cafe, café, the three spellings of strasse, and x. string path = TemporaryDatabase.CreatePath("collation-group-"); - DatabaseCreator.CreateEmpty(path, collation: new Collation(CollatingOrder.General, version)); + JetDatabase.Create(path, collation: new Collation(CollatingOrder.General, version)); var e = new QueryEngine(TemporaryDatabase.OpenTracked(path)); e.ExecuteNonQuery("CREATE TABLE G (Id LONG, S TEXT(20))"); string[] values = ["Straße", "STRASSE", "strasse ", "x", "X", "café", "cafe"]; @@ -131,4 +131,4 @@ public void The_comparer_for_a_collation_orders_as_that_collation() Assert.Equal(-1, croatian.Compare("cb", "ča")); Assert.Equal(1, JetTextComparer.For(GeneralV1).Compare("cb", "ča")); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/CreateIndexTests.cs b/test/LibRed.Engine.Tests/CreateIndexTests.cs index a82fa2de..224905fb 100644 --- a/test/LibRed.Engine.Tests/CreateIndexTests.cs +++ b/test/LibRed.Engine.Tests/CreateIndexTests.cs @@ -96,7 +96,7 @@ public void With_ignore_null_creates_sparse_index_and_skips_null_rows() } // A descending index: the index-data block records the column as descending (Ascending = false), - // and inserts encode reversed key bytes (IndexKeyEncoder handles the inversion). + // and inserts encode reversed key bytes (IndexKeyCodec handles the inversion). [Fact] public void Descending_index_records_direction_and_inserts() { @@ -147,4 +147,4 @@ public void Create_index_on_non_empty_table_backfills() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/CreateProcedureTests.cs b/test/LibRed.Engine.Tests/CreateProcedureTests.cs index 9f700e1f..18b03cdb 100644 --- a/test/LibRed.Engine.Tests/CreateProcedureTests.cs +++ b/test/LibRed.Engine.Tests/CreateProcedureTests.cs @@ -252,7 +252,7 @@ public void Action_query_body_round_trips_through_the_file(string body, string e using (var db = JetDatabase.Open(path, readOnly: false)) // fresh open: read from the file { - Assert.Equal(expected, db.Catalog.ActionQueries["P"].Sql); + Assert.Equal(expected, db.Catalog.FindQuery("P")!.Sql); Assert.Equal(affected, new QueryEngine(db).ExecuteNonQuery("EXECUTE [P]")); } } @@ -278,7 +278,7 @@ public void A_written_action_querys_parameters_bind_when_it_is_executed() Assert.Equal( "PARAMETERS [pCity] TEXT(50), [pCountry] TEXT(20); " + "UPDATE [Customers] SET [City] = pCity WHERE Country = pCountry", - db.Catalog.ActionQueries["ByCountry"].Sql); + db.Catalog.FindQuery("ByCountry")!.Sql); Assert.Equal(7, engine.ExecuteNonQuery("EXECUTE [ByCountry] 'Ankh-Morpork', 'UK'")); Assert.Equal( @@ -303,4 +303,4 @@ public void Procedure_name_colliding_with_an_object_throws() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/CreateViewTests.cs b/test/LibRed.Engine.Tests/CreateViewTests.cs index 12a14388..34de6803 100644 --- a/test/LibRed.Engine.Tests/CreateViewTests.cs +++ b/test/LibRed.Engine.Tests/CreateViewTests.cs @@ -42,7 +42,7 @@ public void A_from_less_body_is_stored_and_queryable(string sql) using (var db = JetDatabase.Open(path)) // fresh open: read from the file { - Assert.Equal("SELECT 1 AS [n]", db.Catalog.Views["Const"]); + Assert.Equal("SELECT 1 AS [n]", db.Catalog.FindQuery("Const")!.Sql); Assert.Equal(1, new QueryEngine(db).ExecuteQuery("SELECT `n` FROM `Const`").Rows.Single()[0]); } } @@ -60,7 +60,7 @@ public void A_leading_space_is_dropped_from_a_view_name_and_refused_in_a_procedu using var db = JetDatabase.Open(path, readOnly: false); var e = new QueryEngine(db); e.ExecuteNonQuery("CREATE VIEW [ Spaced] AS SELECT 1 AS [n]"); - Assert.Contains("Spaced", db.Catalog.Views.Keys); + Assert.Contains("Spaced", db.Catalog.Queries.Keys); Assert.Throws(() => e.ExecuteNonQuery("CREATE PROCEDURE [ Proc] AS SELECT 1 AS [n]")); } @@ -221,4 +221,4 @@ public void View_may_take_a_relationships_name() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/DdlDmlTests.cs b/test/LibRed.Engine.Tests/DdlDmlTests.cs index ab3b91b7..88a5b0c5 100644 --- a/test/LibRed.Engine.Tests/DdlDmlTests.cs +++ b/test/LibRed.Engine.Tests/DdlDmlTests.cs @@ -38,7 +38,8 @@ public void Creating_a_column_of_a_newer_type_raises_the_file_format( Assert.Equal(expectedByte, db.DefinitionPage.JetVersion); // page 0 was re-read, not left stale } - Assert.Equal(expectedByte, VersionByte(path)); // and it reached the file + using (var reopened = JetDatabase.Open(path)) + Assert.Equal(expected, reopened.Format.Version); // and it reached the file } finally { TemporaryDatabase.Delete(path); } } @@ -59,7 +60,8 @@ public void Altering_a_column_to_bigint_raises_the_file_format() Assert.Equal(JetVersion.Version16_2016, db.Format.Version); } - Assert.Equal(0x05, VersionByte(path)); + using (var reopened = JetDatabase.Open(path)) + Assert.Equal(JetVersion.Version16_2016, reopened.Format.Version); } finally { TemporaryDatabase.Delete(path); } } @@ -87,18 +89,12 @@ public void A_failed_statement_does_not_leave_the_format_raised() Assert.Equal(0x02, db.DefinitionPage.JetVersion); } - Assert.Equal(0x02, VersionByte(path)); + using (var reopened = JetDatabase.Open(path)) + Assert.Equal(JetVersion.Version12_2007, reopened.Format.Version); } finally { TemporaryDatabase.Delete(path); } } - private static byte VersionByte(string path) - { - using var stream = File.OpenRead(path); - stream.Seek(0x14, SeekOrigin.Begin); - return (byte)stream.ReadByte(); - } - // BIGINT written by LibRed rather than read from an ACE fixture, including through an index so the key // encoder runs on our own writes. Both extremes and both signs: the key transform is a sign-bit flip, so // positives alone would pass against almost any encoding. @@ -174,10 +170,9 @@ public void Datetime2_round_trips_through_libred_on_the_ace17_format() string path = CopyToTemp(); try { - SetVersionByte(path, 0x06); - using (var db = JetDatabase.Open(path, readOnly: false)) { + db.EnsureFormatAtLeast(JetVersion.Version17_2019); var e = new QueryEngine(db); e.ExecuteNonQuery("CREATE TABLE `E` (`Id` INTEGER PRIMARY KEY, `V` DATETIME2 NULL)"); foreach ((int id, DateTime? value) in cases) @@ -208,8 +203,8 @@ public void Datetime2_values_within_one_millisecond_stay_distinct() string path = CopyToTemp(); try { - SetVersionByte(path, 0x06); using var db = JetDatabase.Open(path, readOnly: false); + db.EnsureFormatAtLeast(JetVersion.Version17_2019); var e = new QueryEngine(db); e.ExecuteNonQuery("CREATE TABLE `E` (`Id` INTEGER PRIMARY KEY, `V` DATETIME2 NULL)"); foreach ((int id, int ticks) in new[] { (1, 3), (2, 1), (3, 2) }) @@ -225,15 +220,6 @@ public void Datetime2_values_within_one_millisecond_stay_distinct() finally { TemporaryDatabase.Delete(path); } } - /// Raises a copied file to the ACE 17 format. Page 0 offset 0x14 is the entire upgrade — see - /// docs/format/page-00-database.md and AceDateTime2UpgradeTests. - private static void SetVersionByte(string path, byte version) - { - using var stream = new FileStream(path, FileMode.Open, FileAccess.Write); - stream.Seek(0x14, SeekOrigin.Begin); - stream.WriteByte(version); - } - // Creating at a chosen format, rather than upgrading someone else's file. The default stays ACE 12 so an // ordinary database keeps opening in every Access from 2007; asking for ACE 17 up front is how you get a // DATETIME2 database without the file ever having been at an older version. @@ -253,7 +239,8 @@ public void A_natively_created_database_is_stamped_at_the_requested_format( try { LibRed.Data.LibRedConnection.CreateDatabase($"Data Source={path}", version: version); - Assert.Equal(createdByte, VersionByte(path)); + using (var created = JetDatabase.Open(path)) + Assert.Equal(createdByte, (byte)created.Format.Version); using (var db = JetDatabase.Open(path, readOnly: false)) { @@ -265,7 +252,8 @@ public void A_natively_created_database_is_stamped_at_the_requested_format( Assert.Equal(value, e.ExecuteQuery("SELECT `V` FROM `E`").Rows.Single()[0]); } - Assert.Equal(afterDatetime2, VersionByte(path)); + using (var reopened = JetDatabase.Open(path)) + Assert.Equal(afterDatetime2, (byte)reopened.Format.Version); } finally { TemporaryDatabase.Delete(path); } } @@ -530,4 +518,4 @@ public void Create_without_primary_key_is_allowed() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/DeleteCompactionTests.cs b/test/LibRed.Engine.Tests/DeleteCompactionTests.cs index 12746afc..b443f52d 100644 --- a/test/LibRed.Engine.Tests/DeleteCompactionTests.cs +++ b/test/LibRed.Engine.Tests/DeleteCompactionTests.cs @@ -4,11 +4,11 @@ namespace LibRed.Engine.Tests; // Deleting several rows from one page has to leave every surviving row readable. // -// Reclaiming a deleted row's space (RowInserter.ReclaimRow) closes the gap by sliding the rows below it up +// Reclaiming a deleted row's space (DataPage.ReclaimRow) closes the gap by sliding the rows below it up // and turning the emptied slot into a zero-length tombstone. The first version decided which slots to move // by comparing offsets, which leaves behind a tombstone sitting at exactly the deleted row's offset — it -// then absorbs that row's length and starves the next live row to zero, so a later scan fails with "Row is -// too short to be an inline record". Slot offsets are non-increasing with slot index, so the rows below are +// then absorbs that row's length and starves the next live row to zero, so a later scan fails reading it as +// a row too short for its own column count. Slot offsets are non-increasing with slot index, so the rows below are // simply the LATER slots; moving by index is what makes tombstones travel with them. // // Single deletes could not catch it: the two rules agree until a tombstone is already on the page. @@ -77,4 +77,4 @@ public void Churn_reuses_the_reclaimed_space() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/DropViewProcedureTests.cs b/test/LibRed.Engine.Tests/DropViewProcedureTests.cs index d57eb464..280e1fbc 100644 --- a/test/LibRed.Engine.Tests/DropViewProcedureTests.cs +++ b/test/LibRed.Engine.Tests/DropViewProcedureTests.cs @@ -18,10 +18,10 @@ public void Drop_view_removes_it_and_frees_the_name() { var e = Fresh(); e.ExecuteNonQuery("CREATE VIEW V AS SELECT ProductID FROM Products"); - Assert.True(e.Database.Catalog.Views.ContainsKey("V")); + Assert.NotNull(e.Database.Catalog.FindQuery("V")); e.ExecuteNonQuery("DROP VIEW V"); - Assert.False(e.Database.Catalog.Views.ContainsKey("V")); // gone + Assert.Null(e.Database.Catalog.FindQuery("V")); // gone Assert.Throws(() => e.ExecuteQuery("SELECT * FROM V")); Assert.Equal(0, e.ExecuteNonQuery("CREATE VIEW V AS SELECT ProductID FROM Products")); // name reusable } @@ -32,8 +32,7 @@ public void Drop_procedure_removes_it() var e = Fresh(); e.ExecuteNonQuery("CREATE PROCEDURE Pr n LONG AS SELECT ProductID FROM Products WHERE ProductID = n"); e.ExecuteNonQuery("DROP PROCEDURE Pr"); - Assert.False(e.Database.Catalog.ActionQueries.ContainsKey("Pr")); - Assert.False(e.Database.Catalog.Views.ContainsKey("Pr")); + Assert.Null(e.Database.Catalog.FindQuery("Pr")); } [Fact] @@ -43,8 +42,8 @@ public void Drop_view_and_procedure_are_interchangeable_and_missing_is_rejected( // ACE lets either statement drop either object; LibRed matches. e.ExecuteNonQuery("CREATE VIEW V2 AS SELECT ProductID FROM Products"); e.ExecuteNonQuery("DROP PROCEDURE V2"); // drop a view via DROP PROCEDURE - Assert.False(e.Database.Catalog.Views.ContainsKey("V2")); + Assert.Null(e.Database.Catalog.FindQuery("V2")); Assert.Throws(() => e.ExecuteNonQuery("DROP VIEW Nope")); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/IndexSplitTests.cs b/test/LibRed.Engine.Tests/IndexSplitTests.cs index 995b2915..a7e999ce 100644 --- a/test/LibRed.Engine.Tests/IndexSplitTests.cs +++ b/test/LibRed.Engine.Tests/IndexSplitTests.cs @@ -38,11 +38,12 @@ public void Leaf_splitting_keeps_every_key_in_order_and_findable() for (int i = 1; i <= N; i++) t.Insert([i, $"r{i}"]); } - using (var ch = PageChannel.Open(path, readOnly: true)) + using (var db = JetDatabase.Open(path)) { - var pk = new LibRed.Catalog.JetCatalog(ch).FindTable("Big")!.Indexes.Single(x => x.IsPrimaryKey); + var pk = db.OpenTable("Big").Definition.Indexes.Single(x => x.IsPrimaryKey); // The root grew into a node — the tree is genuinely multi-level, not a single fat leaf. + using var ch = PageChannel.Open(path, readOnly: true); Assert.Equal(PageType.IntermediateIndexPage, PageHeader.ReadType(ch.ReadPage(pk.RootPage).Span)); var cursor = new IndexCursor(ch, pk.RootPage); @@ -63,4 +64,4 @@ public void Leaf_splitting_keeps_every_key_in_order_and_findable() } finally { TemporaryDatabase.Delete(path); } } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/MemoIndexTests.cs b/test/LibRed.Engine.Tests/MemoIndexTests.cs index 53d39e58..fc80567e 100644 --- a/test/LibRed.Engine.Tests/MemoIndexTests.cs +++ b/test/LibRed.Engine.Tests/MemoIndexTests.cs @@ -5,7 +5,7 @@ namespace LibRed.Engine.Tests; // A Memo (Long Text) column is indexable in Access; its index key is the text collation key over the first -// 255 characters. Exercise the write paths (RowInserter → IndexKeyEncoder) end-to-end through the engine. +// 255 characters. Exercise the write paths (RowInserter → IndexKeyCodec) end-to-end through the engine. public class MemoIndexTests : TempDatabaseTest { private static QueryEngine Fresh() @@ -88,4 +88,4 @@ public void A_memo_indexed_row_updates_and_keeps_its_index(int before, int after Assert.Empty(e.ExecuteQuery($"SELECT Id FROM MK WHERE M = '{old}'").Rows); Assert.Single(e.ExecuteQuery("SELECT Id FROM MK WHERE M = 'second'").Rows); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/PreEpochDateOrderingTests.cs b/test/LibRed.Engine.Tests/PreEpochDateOrderingTests.cs index f701c628..6de1a777 100644 --- a/test/LibRed.Engine.Tests/PreEpochDateOrderingTests.cs +++ b/test/LibRed.Engine.Tests/PreEpochDateOrderingTests.cs @@ -18,7 +18,7 @@ namespace LibRed.Engine.Tests; /// live data in the suite. /// /// What matters most here is INTERNAL CONSISTENCY. LibRed has two paths to an ordered or filtered result: the -/// index (IndexKeyEncoder writes the raw OA serial as the sort key, so it inherits ACE's ordering) and the +/// index (IndexKeyCodec writes the raw OA serial as the sort key, so it inherits ACE's ordering) and the /// evaluator (which compares CLR DateTime values, and is therefore chronologically correct). If those two /// disagree, the same query returns different answers depending on whether the planner picks a seek or a scan. /// These tests pin that they agree, and record which convention the agreement follows. @@ -110,4 +110,4 @@ public void Comparison_within_a_pre_epoch_day_is_consistent_with_ordering() Assert.Equal(morningFirst, morningIsLess); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/QueryNamesTests.cs b/test/LibRed.Engine.Tests/QueryNamesTests.cs index 118f96f0..de2dcfaa 100644 --- a/test/LibRed.Engine.Tests/QueryNamesTests.cs +++ b/test/LibRed.Engine.Tests/QueryNamesTests.cs @@ -136,7 +136,7 @@ public void A_stored_query_declaring_a_form_control_executes() using (var db = JetDatabase.Open(path, readOnly: true)) { - Assert.Equal("Forms!frmMenu!txtCity", db.Catalog.QueryParameters["ByCity"].Single().Name); + Assert.Equal("Forms!frmMenu!txtCity", db.Catalog.FindQuery("ByCity")!.Parameters.Single().Name); Assert.Equal(["ALFKI"], Column(new QueryEngine(db), "EXECUTE ByCity 'Berlin'")); } } @@ -162,4 +162,4 @@ public void Yes_no_on_and_off_are_true_and_false() => public void A_qualified_yes_is_the_column() => Assert.Equal([true, 3, false, 4, true, 5, false, 6], Row(Seeded(), "SELECT Yes, K.Yes, No, K.No, On, K.On, Off, K!Off FROM K")); -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/ReferencesAndIdentityTests.cs b/test/LibRed.Engine.Tests/ReferencesAndIdentityTests.cs index 268977f3..e08072cd 100644 --- a/test/LibRed.Engine.Tests/ReferencesAndIdentityTests.cs +++ b/test/LibRed.Engine.Tests/ReferencesAndIdentityTests.cs @@ -121,7 +121,7 @@ public void Add_column_creates_its_primary_key_and_unique_index() JetDatabase db = Run("CREATE TABLE T (V TEXT(10))", "ALTER TABLE T ADD COLUMN Id LONG CONSTRAINT pk PRIMARY KEY", "ALTER TABLE T ADD COLUMN Code TEXT(10) CONSTRAINT uq UNIQUE"); - TableDef t = db.Catalog.FindTable("T")!; + TableDefinition t = db.Catalog.FindTable("T")!; IndexDef pk = Assert.Single(t.Indexes, i => i.IsPrimaryKey); Assert.Equal("pk", pk.Name); IndexDef uq = Assert.Single(t.Indexes, i => i.Name == "uq"); @@ -239,4 +239,4 @@ public void Alter_table_takes_identity_too() db = Run("CREATE TABLE T (Id INT, V TEXT(10))", "ALTER TABLE T ALTER COLUMN Id INT IDENTITY"); Assert.True(db.Catalog.FindTable("T")!.FindColumn("Id")!.IsAutoNumber); } -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/SchemaVisibilityTests.cs b/test/LibRed.Engine.Tests/SchemaVisibilityTests.cs index af8a801e..763c50dc 100644 --- a/test/LibRed.Engine.Tests/SchemaVisibilityTests.cs +++ b/test/LibRed.Engine.Tests/SchemaVisibilityTests.cs @@ -73,7 +73,7 @@ public void Ordinary_dml_on_one_connection_does_not_invalidate_another_catalog() first.ExecuteNonQuery("INSERT INTO Rows1 (Id) VALUES (1)"); - // Same TableDef instance: the DML did not force the second connection to re-read the catalog. + // Same TableDefinition instance: the DML did not force the second connection to re-read the catalog. Assert.Same(cached, secondDb.Catalog.Tables.Single(t => t.Name == "Rows1")); } finally { TemporaryDatabase.Delete(path); } @@ -81,4 +81,4 @@ public void Ordinary_dml_on_one_connection_does_not_invalidate_another_catalog() private static string Fresh(string prefix) => TemporaryDatabase.CopyPath(Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), prefix); -} +} \ No newline at end of file diff --git a/test/LibRed.Engine.Tests/SelectIntoTests.cs b/test/LibRed.Engine.Tests/SelectIntoTests.cs index 4751cb50..df8f5647 100644 --- a/test/LibRed.Engine.Tests/SelectIntoTests.cs +++ b/test/LibRed.Engine.Tests/SelectIntoTests.cs @@ -54,8 +54,8 @@ public void Does_not_copy_the_primary_key_or_indexes() QueryEngine engine = WithSource(); engine.ExecuteNonQuery("SELECT * INTO SiNew FROM SiSrc"); - TableDef source = engine.Database.Catalog.Tables.Single(t => t.Name == "SiSrc"); - TableDef made = engine.Database.Catalog.Tables.Single(t => t.Name == "SiNew"); + TableDefinition source = engine.Database.Catalog.Tables.Single(t => t.Name == "SiSrc"); + TableDefinition made = engine.Database.Catalog.Tables.Single(t => t.Name == "SiNew"); Assert.NotEmpty(source.Indexes); // the source has a PK and an index Assert.Empty(made.Indexes); // the copy has neither @@ -121,7 +121,7 @@ public void An_expression_column_is_typed_from_the_expression() QueryEngine engine = WithSource(); engine.ExecuteNonQuery("SELECT Id, Qty * 2 AS Doubled, Label & '!' AS Shout INTO SiNew FROM SiSrc"); - TableDef made = engine.Database.Catalog.Tables.Single(t => t.Name == "SiNew"); + TableDefinition made = engine.Database.Catalog.Tables.Single(t => t.Name == "SiNew"); Assert.Equal(["Id", "Doubled", "Shout"], made.Columns.Select(c => c.Name)); Assert.Equal(JetDataType.Int32, made.Columns[1].Type); Assert.Equal(JetDataType.Text, made.Columns[2].Type); @@ -152,4 +152,4 @@ public void Returns_no_rows_to_the_caller() Assert.Empty(result.Rows); Assert.Equal(2, Convert.ToInt32(engine.ExecuteQuery("SELECT COUNT(*) FROM SiNew").Rows.Single()[0])); } -} +} \ No newline at end of file diff --git a/test/LibRed.Shared/AceTestDatabase.cs b/test/LibRed.Shared/AceTestDatabase.cs index acc316d4..fbc0f407 100644 --- a/test/LibRed.Shared/AceTestDatabase.cs +++ b/test/LibRed.Shared/AceTestDatabase.cs @@ -66,7 +66,7 @@ public static OleDbConnection Open(string path, string? password = null, int att /// ACE faults when two threads are inside it at once (see AceCollection), and serialising the tests /// does not stop that on its own. DAO is apartment-threaded, so an engine created from a test's thread lives /// on a COM-created thread of its own, and the probes release none of what they create. Each Workspace, - /// Database, TableDef and Field is torn down when the finalizer gets to it — inside ACE, on that COM thread, + /// Database, TableDefinition and Field is torn down when the finalizer gets to it — inside ACE, on that COM thread, /// at whatever moment a GC happens to run, which is usually in the middle of a later test that is itself /// inside ACE. An OLE DB object left undisposed does the same the other way round, from the finalizer thread /// into a later DAO call. @@ -149,4 +149,4 @@ public static bool SupportsColumnType(string sourceDatabase, string typeName) public static string UnsupportedColumnTypeReason(string typeName) => $"The installed ACE cannot create a {typeName} column - it predates the type. " + $"DATETIME2 needs ACE 17 (Access 2019+/365); BIGINT needs ACE 16 (Access 2016)."; -} +} \ No newline at end of file diff --git a/test/LibRed.Shared/TestDatabases.cs b/test/LibRed.Shared/TestDatabases.cs index 7b2dc3c4..14bd510c 100644 --- a/test/LibRed.Shared/TestDatabases.cs +++ b/test/LibRed.Shared/TestDatabases.cs @@ -1,6 +1,9 @@ +using Xunit; + namespace LibRed.Core.Tests; -/// Paths to the real database files copied alongside the test assembly. +/// Paths to the real database files copied alongside the test assembly, and what reads and writes them +/// directly. internal static class TestDatabases { /// The path to a checked-in fixture by file name — every Data\*.accdb is copied @@ -9,6 +12,90 @@ internal static class TestDatabases public static string Data(string fileName) => Path.Combine(AppContext.BaseDirectory, "Data", fileName); + /// The format of the database at , read from its own page 0 as an open + /// reads it — never assumed from what the fixture is believed to be. + public static Formats.JetFormatBase FormatOf(string path) + { + // Shared for writing too: a test may ask while its own connection still holds the file open. + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + return Formats.JetFormatBase.Detect(stream); + } + + // Raw page I/O on a closed file, for a test that stands in for another writer — planting released pages, moving or + // breaking page 0's map pointers. A writable channel cannot do it: its close merges the released map and checks + // those very pointers, undoing or refusing the state being planted. + + /// Page of the file at , straight off disk. + public static byte[] ReadPage(string path, int page) + { + int pageSize = FormatOf(path).PageSize; + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + var bytes = new byte[pageSize]; + stream.Position = (long)page * pageSize; + stream.ReadExactly(bytes); + return bytes; + } + + /// Overwrites page of the file at . + public static void WritePage(string path, int page, byte[] bytes) + { + int pageSize = FormatOf(path).PageSize; + using var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite); + stream.Position = (long)page * pageSize; + stream.Write(bytes); + } + + /// Adds as a new page at the end of the file at . + public static void AppendPage(string path, byte[] bytes) + { + using var stream = new FileStream(path, FileMode.Append, FileAccess.Write); + stream.Write(bytes); + } + + /// Points page 0's global map pointer at at + /// :, under the header mask. + public static void WriteMapPointer(string path, int pointerOffset, int row, int page) + { + Formats.JetFormatBase format = FormatOf(path); + byte[] page0 = ReadPage(path, 0); + var pointer = new byte[IO.PageBuffer.RecordPointerSize]; + IO.PageBuffer.WriteRecordPointer(pointer, 0, row, page); + Pages.DatabaseDefinitionPage.WriteMasked(page0, pointerOffset, pointer, format); + WritePage(path, 0, page0); + } + + /// Whether 's bit is set in the inline usage map at of a + /// holder page's bytes. + public static bool MapBit(byte[] holder, Formats.JetFormatBase format, int row, int page) => + Storage.BitmapBits.Get(InlineMapBits(holder, format, row, page, out int bit), bit); + + /// Sets or clears 's bit in the inline usage map at of a + /// holder page's bytes. + public static void SetMapBit(byte[] holder, Formats.JetFormatBase format, int row, int page, bool set) => + Storage.BitmapBits.Set(InlineMapBits(holder, format, row, page, out int bit), bit, set); + + private static Span InlineMapBits(byte[] holder, Formats.JetFormatBase format, int row, int page, out int bit) + { + var parsed = new Pages.DataPage(); + parsed.Read(new IO.PageBuffer(holder, 0), format); + Pages.DataPage.RowSlot slot = parsed.Rows[row]; + Span record = holder.AsSpan(slot.Offset, slot.Length); + Assert.Equal(Formats.UsageMapType.Inline, Storage.UsageMap.RecordType(record)); + bit = page - Storage.UsageMap.StartPage(record, format); + Span bits = Storage.UsageMap.InlineBits(record, format); + Assert.InRange(bit, 0, bits.Length * 8 - 1); + return bits; + } + + /// The global usage map page 0's pointer at names, found as the allocator + /// finds it, wherever it is: the pointer, the holder page's bytes, and the record's slot on it. + public static (int Row, int Page, byte[] Holder, Pages.DataPage.RowSlot Slot) GlobalMap(IO.PageChannel channel, int pointerOffset) + { + (int row, int page) = Pages.DatabaseDefinitionPage.ReadMapPointer(channel.ReadPage(0).Span, pointerOffset, channel.Format); + (IO.PageBuffer holder, _, Pages.DataPage.RowSlot slot) = Storage.UsageMap.ReadRecord(channel, row, page, "Global map pointer"); + return (row, page, holder.Span.ToArray(), slot); + } + /// An Access 2007 (ACE 12 / ACCDB) Northwind sample. public static string NorthwindAccdb { get; } = Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"); @@ -55,4 +142,4 @@ public static string Data(string fileName) => /// The password for . public const string EncryptedPassword = "Test"; -} +} \ No newline at end of file